diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..e48fa823
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,17 @@
+.git
+.github
+default.etcd
+*.gz
+*.tar.gz
+*.bzip2
+*.zip
+browser/node_modules
+node_modules
+docs/debugging/s3-verify/s3-verify
+docs/debugging/xl-meta/xl-meta
+docs/debugging/s3-check-md5/s3-check-md5
+docs/debugging/hash-set/hash-set
+docs/debugging/healing-bin/healing-bin
+docs/debugging/inspect/inspect
+docs/debugging/pprofgoparser/pprofgoparser
+docs/debugging/reorder-disks/reorder-disks
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 00000000..978d9085
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,51 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: community, triage
+assignees: ''
+
+---
+
+## NOTE
+All GitHub issues are addressed on a best-effort basis at MinIO's sole discretion. There are no Service Level Agreements (SLA) or Objectives (SLO). Remember our [Code of Conduct](https://github.com/minio/minio/blob/master/code_of_conduct.md) when engaging with MinIO Engineers and the larger community.
+
+For urgent issues (e.g. production down, etc.), subscribe to [SUBNET](https://min.io/pricing?jmp=github) for direct to engineering support.
+
+
+
+## Expected Behavior
+
+
+
+## Current Behavior
+
+
+
+## Possible Solution
+
+
+
+## Steps to Reproduce (for bugs)
+
+
+
+
+1.
+2.
+3.
+4.
+
+## Context
+
+
+
+## Regression
+
+
+
+## Your Environment
+
+* Version used (`minio --version`):
+* Server setup and configuration:
+* Operating System and version (`uname -a`):
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..cd3f4153
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,54 @@
+---
+name: Bug report
+about: Report a bug in MinIO (community edition is source-only)
+title: ''
+labels: community, triage
+assignees: ''
+
+---
+
+## IMPORTANT NOTES
+
+**Community Edition**: MinIO community edition is now source-only. Install via `go install github.com/minio/minio@latest`
+
+**Feature Requests**: We are no longer accepting feature requests for the community edition. For feature requests and enterprise support, please subscribe to [MinIO Enterprise Support](https://min.io/pricing).
+
+**Urgent Issues**: If this case is urgent or affects production, please subscribe to [SUBNET](https://min.io/pricing) for 24/7 enterprise support.
+
+
+
+## Expected Behavior
+
+
+
+## Current Behavior
+
+
+
+## Possible Solution
+
+
+
+## Steps to Reproduce (for bugs)
+
+
+
+
+1.
+2.
+3.
+4.
+
+## Context
+
+
+
+## Regression
+
+
+
+## Your Environment
+
+* Version used (`minio --version`):
+* Server setup and configuration:
+* Operating System and version (`uname -a`):
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..28ed32af
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: false
+contact_links:
+ - name: MinIO Community Support
+ url: https://slack.min.io
+ about: Community support via Slack - for questions and discussions
+ - name: MinIO Enterprise Support (SUBNET)
+ url: https://min.io/pricing
+ about: Enterprise support with SLA - for production deployments and feature requests
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000..2ff68465
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,26 @@
+## Community Contribution License
+All community contributions in this pull request are licensed to the project maintainers
+under the terms of the [Apache 2 license](https://www.apache.org/licenses/LICENSE-2.0).
+By creating this pull request I represent that I have the right to license the
+contributions to the project maintainers under the Apache 2 license.
+
+## Description
+
+
+## Motivation and Context
+
+
+## How to test this PR?
+
+
+## Types of changes
+- [ ] Bug fix (non-breaking change which fixes an issue)
+- [ ] New feature (non-breaking change which adds functionality)
+- [ ] Optimization (provides speedup with no functional changes)
+- [ ] Breaking change (fix or feature that would cause existing functionality to change)
+
+## Checklist:
+- [ ] Fixes a regression (If yes, please add `commit-id` or `PR #` here)
+- [ ] Unit tests added/updated
+- [ ] Internal documentation updated
+- [ ] Create a documentation update request [here](https://github.com/minio/docs/issues/new?label=doc-change,title=Doc+Updated+Needed+For+PR+github.com%2fminio%2fminio%2fpull%2fNNNNN)
diff --git a/.github/logo.svg b/.github/logo.svg
new file mode 100644
index 00000000..e928e786
--- /dev/null
+++ b/.github/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.github/stale.yml b/.github/stale.yml
new file mode 100644
index 00000000..1085be6b
--- /dev/null
+++ b/.github/stale.yml
@@ -0,0 +1,60 @@
+# Configuration for probot-stale - https://github.com/probot/stale
+
+# Number of days of inactivity before an Issue or Pull Request becomes stale
+daysUntilStale: 30
+
+# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
+# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
+daysUntilClose: 15
+
+# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
+onlyLabels: []
+
+# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
+exemptLabels:
+ - "security"
+ - "pending discussion"
+ - "do-not-close"
+
+# Set to true to ignore issues in a project (defaults to false)
+exemptProjects: false
+
+# Set to true to ignore issues in a milestone (defaults to false)
+exemptMilestones: false
+
+# Set to true to ignore issues with an assignee (defaults to false)
+exemptAssignees: false
+
+# Label to use when marking as stale
+staleLabel: stale
+
+# Comment to post when marking as stale. Set to `false` to disable
+markComment: >-
+ This issue has been automatically marked as stale because it has not had
+ recent activity. It will be closed after 15 days if no further activity
+ occurs. Thank you for your contributions.
+# Comment to post when removing the stale label.
+# unmarkComment: >
+# Your comment here.
+
+# Comment to post when closing a stale Issue or Pull Request.
+# closeComment: >
+# Your comment here.
+
+# Limit the number of actions per hour, from 1-30. Default is 30
+limitPerRun: 1
+
+# Limit to only `issues` or `pulls`
+# only: issues
+
+# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
+# pulls:
+# daysUntilStale: 30
+# markComment: >
+# This pull request has been automatically marked as stale because it has not had
+# recent activity. It will be closed if no further activity occurs. Thank you
+# for your contributions.
+
+# issues:
+# exemptLabels:
+# - confirmed
diff --git a/.github/workflows/depsreview.yaml b/.github/workflows/depsreview.yaml
new file mode 100644
index 00000000..b9d6d20f
--- /dev/null
+++ b/.github/workflows/depsreview.yaml
@@ -0,0 +1,14 @@
+name: 'Dependency Review'
+on: [pull_request]
+
+permissions:
+ contents: read
+
+jobs:
+ dependency-review:
+ runs-on: ubuntu-latest
+ steps:
+ - name: 'Checkout Repository'
+ uses: actions/checkout@v4
+ - name: 'Dependency Review'
+ uses: actions/dependency-review-action@v4
diff --git a/.github/workflows/go-cross.yml b/.github/workflows/go-cross.yml
new file mode 100644
index 00000000..324af3e9
--- /dev/null
+++ b/.github/workflows/go-cross.yml
@@ -0,0 +1,39 @@
+name: Crosscompile
+
+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: Build Tests with Go ${{ matrix.go-version }} on ${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ go-version: [1.24.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 crosscompile
diff --git a/.github/workflows/go-healing.yml b/.github/workflows/go-healing.yml
new file mode 100644
index 00000000..0ea25051
--- /dev/null
+++ b/.github/workflows/go-healing.yml
@@ -0,0 +1,44 @@
+name: Healing 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.24.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
+ MINIO_KMS_SECRET_KEY: "my-minio-key:oyArl7zlPECEduNbB1KXgdzDn2Bdpvvw0l8VO51HQnY="
+ MINIO_KMS_AUTO_ENCRYPTION: on
+ run: |
+ sudo sysctl net.ipv6.conf.all.disable_ipv6=0
+ sudo sysctl net.ipv6.conf.default.disable_ipv6=0
+ make verify-healing
+ make verify-healing-inconsistent-versions
+ make verify-healing-with-root-disks
+ make verify-healing-with-rewrite
diff --git a/.github/workflows/go-lint.yml b/.github/workflows/go-lint.yml
new file mode 100644
index 00000000..6b43086a
--- /dev/null
+++ b/.github/workflows/go-lint.yml
@@ -0,0 +1,42 @@
+name: Linters and 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.24.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 apt install jq -y
+ sudo sysctl net.ipv6.conf.all.disable_ipv6=0
+ sudo sysctl net.ipv6.conf.default.disable_ipv6=0
+ make
+ make test
+ make test-race
diff --git a/.github/workflows/go-resiliency.yml b/.github/workflows/go-resiliency.yml
new file mode 100644
index 00000000..4eb2bbb9
--- /dev/null
+++ b/.github/workflows/go-resiliency.yml
@@ -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.24.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
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
new file mode 100644
index 00000000..9e4b9d07
--- /dev/null
+++ b/.github/workflows/go.yml
@@ -0,0 +1,42 @@
+name: 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 }} - healing
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ go-version: [1.24.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
+ MINIO_KMS_SECRET_KEY: "my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw="
+ MINIO_KMS_AUTO_ENCRYPTION: on
+ run: |
+ sudo sysctl net.ipv6.conf.all.disable_ipv6=0
+ sudo sysctl net.ipv6.conf.default.disable_ipv6=0
+ make verify
+ make test-timeout
diff --git a/.github/workflows/helm-lint.yml b/.github/workflows/helm-lint.yml
new file mode 100644
index 00000000..f444106f
--- /dev/null
+++ b/.github/workflows/helm-lint.yml
@@ -0,0 +1,30 @@
+name: Helm Chart linting
+
+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:
+ release:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Install Helm
+ uses: azure/setup-helm@v4
+
+ - name: Run helm lint
+ run: |
+ cd helm/minio
+ helm lint .
diff --git a/.github/workflows/iam-integrations.yaml b/.github/workflows/iam-integrations.yaml
new file mode 100644
index 00000000..cc57a77a
--- /dev/null
+++ b/.github/workflows/iam-integrations.yaml
@@ -0,0 +1,161 @@
+name: IAM integration
+
+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:
+ iam-matrix-test:
+ name: "[Go=${{ matrix.go-version }}|ldap=${{ matrix.ldap }}|etcd=${{ matrix.etcd }}|openid=${{ matrix.openid }}]"
+ runs-on: ubuntu-latest
+
+ services:
+ openldap:
+ image: quay.io/minio/openldap
+ ports:
+ - "389:389"
+ - "636:636"
+ env:
+ LDAP_ORGANIZATION: "MinIO Inc"
+ LDAP_DOMAIN: "min.io"
+ LDAP_ADMIN_PASSWORD: "admin"
+ etcd:
+ image: "quay.io/coreos/etcd:v3.5.1"
+ env:
+ ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379"
+ ETCD_ADVERTISE_CLIENT_URLS: "http://0.0.0.0:2379"
+ ports:
+ - "2379:2379"
+ options: >-
+ --health-cmd "etcdctl endpoint health"
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ openid:
+ image: quay.io/minio/dex
+ ports:
+ - "5556:5556"
+ env:
+ DEX_LDAP_SERVER: "openldap:389"
+ openid2:
+ image: quay.io/minio/dex
+ ports:
+ - "5557:5557"
+ env:
+ DEX_LDAP_SERVER: "openldap:389"
+ DEX_ISSUER: "http://127.0.0.1:5557/dex"
+ DEX_WEB_HTTP: "0.0.0.0:5557"
+
+ strategy:
+ # When ldap, etcd or openid vars are empty below, those external servers
+ # are turned off - i.e. if ldap="", then ldap server is not enabled for
+ # the tests.
+ matrix:
+ go-version: [1.24.x]
+ ldap: ["", "localhost:389"]
+ etcd: ["", "http://localhost:2379"]
+ openid: ["", "http://127.0.0.1:5556/dex"]
+ exclude:
+ # exclude combos where all are empty.
+ - ldap: ""
+ etcd: ""
+ openid: ""
+ # exclude combos where both ldap and openid IDPs are specified.
+ - ldap: "localhost:389"
+ openid: "http://127.0.0.1:5556/dex"
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version: ${{ matrix.go-version }}
+ check-latest: true
+ - name: Test LDAP/OpenID/Etcd combo
+ env:
+ _MINIO_LDAP_TEST_SERVER: ${{ matrix.ldap }}
+ _MINIO_ETCD_TEST_SERVER: ${{ matrix.etcd }}
+ _MINIO_OPENID_TEST_SERVER: ${{ matrix.openid }}
+ run: |
+ sudo sysctl net.ipv6.conf.all.disable_ipv6=0
+ sudo sysctl net.ipv6.conf.default.disable_ipv6=0
+ make test-iam
+ - name: Test with multiple OpenID providers
+ if: matrix.openid == 'http://127.0.0.1:5556/dex'
+ env:
+ _MINIO_LDAP_TEST_SERVER: ${{ matrix.ldap }}
+ _MINIO_ETCD_TEST_SERVER: ${{ matrix.etcd }}
+ _MINIO_OPENID_TEST_SERVER: ${{ matrix.openid }}
+ _MINIO_OPENID_TEST_SERVER_2: "http://127.0.0.1:5557/dex"
+ run: |
+ sudo sysctl net.ipv6.conf.all.disable_ipv6=0
+ sudo sysctl net.ipv6.conf.default.disable_ipv6=0
+ make test-iam
+ - name: Test with Access Management Plugin enabled
+ env:
+ _MINIO_LDAP_TEST_SERVER: ${{ matrix.ldap }}
+ _MINIO_ETCD_TEST_SERVER: ${{ matrix.etcd }}
+ _MINIO_OPENID_TEST_SERVER: ${{ matrix.openid }}
+ _MINIO_POLICY_PLUGIN_TEST_ENDPOINT: "http://127.0.0.1:8080"
+ run: |
+ sudo sysctl net.ipv6.conf.all.disable_ipv6=0
+ sudo sysctl net.ipv6.conf.default.disable_ipv6=0
+ go run docs/iam/access-manager-plugin.go &
+ make test-iam
+ - name: Test MinIO Old Version data to IAM import current version
+ if: matrix.ldap == 'ldaphost:389'
+ env:
+ _MINIO_LDAP_TEST_SERVER: ${{ matrix.ldap }}
+ run: |
+ make test-iam-ldap-upgrade-import
+ - name: Test LDAP for automatic site replication
+ if: matrix.ldap == 'localhost:389'
+ run: |
+ make test-site-replication-ldap
+ - name: Test OIDC for automatic site replication
+ if: matrix.openid == 'http://127.0.0.1:5556/dex'
+ run: |
+ make test-site-replication-oidc
+ iam-import-with-missing-entities:
+ name: Test IAM import in new cluster with missing entities
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version: ${{ matrix.go-version }}
+ check-latest: true
+ - name: Checkout minio-iam-testing
+ uses: actions/checkout@v4
+ with:
+ repository: minio/minio-iam-testing
+ path: minio-iam-testing
+ - name: Test import of IAM artifacts when in fresh cluster there are missing groups etc
+ run: |
+ make test-iam-import-with-missing-entities
+ iam-import-with-openid:
+ name: Test IAM import in new cluster with opendid configurations
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version: ${{ matrix.go-version }}
+ check-latest: true
+ - name: Checkout minio-iam-testing
+ uses: actions/checkout@v4
+ with:
+ repository: minio/minio-iam-testing
+ path: minio-iam-testing
+ - name: Test import of IAM artifacts when in fresh cluster with openid configurations
+ run: |
+ make test-iam-import-with-openid
diff --git a/.github/workflows/issues.yaml b/.github/workflows/issues.yaml
new file mode 100644
index 00000000..d91950ae
--- /dev/null
+++ b/.github/workflows/issues.yaml
@@ -0,0 +1,18 @@
+# @format
+
+name: Issue Workflow
+
+on:
+ issues:
+ types:
+ - opened
+
+jobs:
+ add-to-project:
+ name: Add issue to project
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/add-to-project@v0.5.0
+ with:
+ project-url: https://github.com/orgs/miniohq/projects/2
+ github-token: ${{ secrets.BOT_PAT }}
diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml
new file mode 100644
index 00000000..da3afe3c
--- /dev/null
+++ b/.github/workflows/lock.yml
@@ -0,0 +1,24 @@
+name: 'Lock Threads'
+
+on:
+ schedule:
+ - cron: '0 0 * * *'
+ workflow_dispatch:
+
+permissions:
+ issues: write
+
+concurrency:
+ group: lock
+
+jobs:
+ action:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: dessant/lock-threads@v3
+ with:
+ github-token: ${{ github.token }}
+ issue-inactive-days: '365'
+ exclude-any-issue-labels: 'do-not-close'
+ issue-lock-reason: 'resolved'
+ log-output: true
diff --git a/.github/workflows/mint.yml b/.github/workflows/mint.yml
new file mode 100644
index 00000000..77dc498c
--- /dev/null
+++ b/.github/workflows/mint.yml
@@ -0,0 +1,81 @@
+name: Mint 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:
+ mint-test:
+ runs-on: mint
+ timeout-minutes: 120
+ steps:
+ - name: cleanup #https://github.com/actions/checkout/issues/273
+ run: |
+ sudo -S rm -rf ${GITHUB_WORKSPACE}
+ mkdir ${GITHUB_WORKSPACE}
+ - name: checkout-step
+ uses: actions/checkout@v4
+
+ - name: setup-go-step
+ uses: actions/setup-go@v5
+ with:
+ go-version: 1.24.x
+
+ - name: github sha short
+ id: vars
+ run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
+
+ - name: build-minio
+ run: |
+ TAG="quay.io/minio/minio:${{ steps.vars.outputs.sha_short }}" make docker
+
+ - name: multipart uploads test
+ run: |
+ ${GITHUB_WORKSPACE}/.github/workflows/multipart/migrate.sh "${{ steps.vars.outputs.sha_short }}"
+
+ - name: compress and encrypt
+ run: |
+ ${GITHUB_WORKSPACE}/.github/workflows/run-mint.sh "compress-encrypt" "minio" "minio123" "${{ steps.vars.outputs.sha_short }}"
+
+ - name: multiple pools
+ run: |
+ ${GITHUB_WORKSPACE}/.github/workflows/run-mint.sh "pools" "minio" "minio123" "${{ steps.vars.outputs.sha_short }}"
+
+ - name: standalone erasure
+ run: |
+ ${GITHUB_WORKSPACE}/.github/workflows/run-mint.sh "erasure" "minio" "minio123" "${{ steps.vars.outputs.sha_short }}"
+
+ # FIXME: renable this back when we have a valid way to add deadlines for PUT()s (internode CreateFile)
+ # - name: resiliency
+ # run: |
+ # ${GITHUB_WORKSPACE}/.github/workflows/run-mint.sh "resiliency" "minio" "minio123" "${{ steps.vars.outputs.sha_short }}"
+
+ - name: The job must cleanup
+ if: ${{ always() }}
+ run: |
+ export JOB_NAME=${{ steps.vars.outputs.sha_short }}
+ for mode in $(echo compress-encrypt pools erasure); do
+ docker-compose -f ${GITHUB_WORKSPACE}/.github/workflows/mint/minio-${mode}.yaml down || true
+ docker-compose -f ${GITHUB_WORKSPACE}/.github/workflows/mint/minio-${mode}.yaml rm || true
+ done
+
+ docker-compose -f ${GITHUB_WORKSPACE}/.github/workflows/multipart/docker-compose-site1.yaml rm -s -f || true
+ docker-compose -f ${GITHUB_WORKSPACE}/.github/workflows/multipart/docker-compose-site2.yaml rm -s -f || true
+ for volume in $(docker volume ls -q | grep minio); do
+ docker volume rm ${volume} || true
+ done
+
+ docker rmi -f quay.io/minio/minio:${{ steps.vars.outputs.sha_short }}
+ docker system prune -f || true
+ docker volume prune -f || true
+ docker volume rm $(docker volume ls -q -f dangling=true) || true
diff --git a/.github/workflows/mint/minio-compress-encrypt.yaml b/.github/workflows/mint/minio-compress-encrypt.yaml
new file mode 100644
index 00000000..fe1238e8
--- /dev/null
+++ b/.github/workflows/mint/minio-compress-encrypt.yaml
@@ -0,0 +1,80 @@
+version: '3.7'
+
+# Settings and configurations that are common for all containers
+x-minio-common: &minio-common
+ image: quay.io/minio/minio:${JOB_NAME}
+ command: server --console-address ":9001" http://minio{1...4}/cdata{1...2}
+ expose:
+ - "9000"
+ - "9001"
+ environment:
+ MINIO_CI_CD: "on"
+ MINIO_ROOT_USER: "minio"
+ MINIO_ROOT_PASSWORD: "minio123"
+ MINIO_COMPRESSION_ENABLE: "on"
+ MINIO_COMPRESSION_MIME_TYPES: "*"
+ MINIO_COMPRESSION_ALLOW_ENCRYPTION: "on"
+ MINIO_KMS_SECRET_KEY: "my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw="
+ healthcheck:
+ test: ["CMD", "mc", "ready", "local"]
+ interval: 5s
+ timeout: 5s
+ retries: 5
+
+# starts 4 docker containers running minio server instances.
+# using nginx reverse proxy, load balancing, you can access
+# it through port 9000.
+services:
+ minio1:
+ <<: *minio-common
+ hostname: minio1
+ volumes:
+ - cdata1-1:/cdata1
+ - cdata1-2:/cdata2
+
+ minio2:
+ <<: *minio-common
+ hostname: minio2
+ volumes:
+ - cdata2-1:/cdata1
+ - cdata2-2:/cdata2
+
+ minio3:
+ <<: *minio-common
+ hostname: minio3
+ volumes:
+ - cdata3-1:/cdata1
+ - cdata3-2:/cdata2
+
+ minio4:
+ <<: *minio-common
+ hostname: minio4
+ volumes:
+ - cdata4-1:/cdata1
+ - cdata4-2:/cdata2
+
+ nginx:
+ image: nginx:1.19.2-alpine
+ hostname: nginx
+ volumes:
+ - ./nginx-4-node.conf:/etc/nginx/nginx.conf:ro
+ ports:
+ - "9000:9000"
+ - "9001:9001"
+ depends_on:
+ - minio1
+ - minio2
+ - minio3
+ - minio4
+
+## By default this config uses default local driver,
+## For custom volumes replace with volume driver configuration.
+volumes:
+ cdata1-1:
+ cdata1-2:
+ cdata2-1:
+ cdata2-2:
+ cdata3-1:
+ cdata3-2:
+ cdata4-1:
+ cdata4-2:
diff --git a/.github/workflows/mint/minio-erasure.yaml b/.github/workflows/mint/minio-erasure.yaml
new file mode 100644
index 00000000..a3fd6dbc
--- /dev/null
+++ b/.github/workflows/mint/minio-erasure.yaml
@@ -0,0 +1,51 @@
+version: '3.7'
+
+# Settings and configurations that are common for all containers
+x-minio-common: &minio-common
+ image: quay.io/minio/minio:${JOB_NAME}
+ command: server --console-address ":9001" edata{1...4}
+ expose:
+ - "9000"
+ - "9001"
+ environment:
+ MINIO_CI_CD: "on"
+ MINIO_ROOT_USER: "minio"
+ MINIO_ROOT_PASSWORD: "minio123"
+ MINIO_KMS_SECRET_KEY: "my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw="
+ healthcheck:
+ test: ["CMD", "mc", "ready", "local"]
+ interval: 5s
+ timeout: 5s
+ retries: 5
+
+# starts 4 docker containers running minio server instances.
+# using nginx reverse proxy, load balancing, you can access
+# it through port 9000.
+services:
+ minio1:
+ <<: *minio-common
+ hostname: minio1
+ volumes:
+ - edata1-1:/edata1
+ - edata1-2:/edata2
+ - edata1-3:/edata3
+ - edata1-4:/edata4
+
+ nginx:
+ image: nginx:1.19.2-alpine
+ hostname: nginx
+ volumes:
+ - ./nginx-1-node.conf:/etc/nginx/nginx.conf:ro
+ ports:
+ - "9000:9000"
+ - "9001:9001"
+ depends_on:
+ - minio1
+
+## By default this config uses default local driver,
+## For custom volumes replace with volume driver configuration.
+volumes:
+ edata1-1:
+ edata1-2:
+ edata1-3:
+ edata1-4:
diff --git a/.github/workflows/mint/minio-pools.yaml b/.github/workflows/mint/minio-pools.yaml
new file mode 100644
index 00000000..bd79fdae
--- /dev/null
+++ b/.github/workflows/mint/minio-pools.yaml
@@ -0,0 +1,117 @@
+version: '3.7'
+
+# Settings and configurations that are common for all containers
+x-minio-common: &minio-common
+ image: quay.io/minio/minio:${JOB_NAME}
+ command: server --console-address ":9001" http://minio{1...4}/pdata{1...2} http://minio{5...8}/pdata{1...2}
+ expose:
+ - "9000"
+ - "9001"
+ environment:
+ MINIO_CI_CD: "on"
+ MINIO_ROOT_USER: "minio"
+ MINIO_ROOT_PASSWORD: "minio123"
+ MINIO_KMS_SECRET_KEY: "my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw="
+ healthcheck:
+ test: ["CMD", "mc", "ready", "local"]
+ interval: 5s
+ timeout: 5s
+ retries: 5
+
+# starts 4 docker containers running minio server instances.
+# using nginx reverse proxy, load balancing, you can access
+# it through port 9000.
+services:
+ minio1:
+ <<: *minio-common
+ hostname: minio1
+ volumes:
+ - pdata1-1:/pdata1
+ - pdata1-2:/pdata2
+
+ minio2:
+ <<: *minio-common
+ hostname: minio2
+ volumes:
+ - pdata2-1:/pdata1
+ - pdata2-2:/pdata2
+
+ minio3:
+ <<: *minio-common
+ hostname: minio3
+ volumes:
+ - pdata3-1:/pdata1
+ - pdata3-2:/pdata2
+
+ minio4:
+ <<: *minio-common
+ hostname: minio4
+ volumes:
+ - pdata4-1:/pdata1
+ - pdata4-2:/pdata2
+
+ minio5:
+ <<: *minio-common
+ hostname: minio5
+ volumes:
+ - pdata5-1:/pdata1
+ - pdata5-2:/pdata2
+
+ minio6:
+ <<: *minio-common
+ hostname: minio6
+ volumes:
+ - pdata6-1:/pdata1
+ - pdata6-2:/pdata2
+
+ minio7:
+ <<: *minio-common
+ hostname: minio7
+ volumes:
+ - pdata7-1:/pdata1
+ - pdata7-2:/pdata2
+
+ minio8:
+ <<: *minio-common
+ hostname: minio8
+ volumes:
+ - pdata8-1:/pdata1
+ - pdata8-2:/pdata2
+
+ nginx:
+ image: nginx:1.19.2-alpine
+ hostname: nginx
+ volumes:
+ - ./nginx-8-node.conf:/etc/nginx/nginx.conf:ro
+ ports:
+ - "9000:9000"
+ - "9001:9001"
+ depends_on:
+ - minio1
+ - minio2
+ - minio3
+ - minio4
+ - minio5
+ - minio6
+ - minio7
+ - minio8
+
+## By default this config uses default local driver,
+## For custom volumes replace with volume driver configuration.
+volumes:
+ pdata1-1:
+ pdata1-2:
+ pdata2-1:
+ pdata2-2:
+ pdata3-1:
+ pdata3-2:
+ pdata4-1:
+ pdata4-2:
+ pdata5-1:
+ pdata5-2:
+ pdata6-1:
+ pdata6-2:
+ pdata7-1:
+ pdata7-2:
+ pdata8-1:
+ pdata8-2:
diff --git a/.github/workflows/mint/minio-resiliency.yaml b/.github/workflows/mint/minio-resiliency.yaml
new file mode 100644
index 00000000..9d569c59
--- /dev/null
+++ b/.github/workflows/mint/minio-resiliency.yaml
@@ -0,0 +1,78 @@
+version: '3.7'
+
+# Settings and configurations that are common for all containers
+x-minio-common: &minio-common
+ image: quay.io/minio/minio:${JOB_NAME}
+ command: server --console-address ":9001" http://minio{1...4}/rdata{1...2}
+ expose:
+ - "9000"
+ - "9001"
+ environment:
+ MINIO_CI_CD: "on"
+ MINIO_ROOT_USER: "minio"
+ MINIO_ROOT_PASSWORD: "minio123"
+ MINIO_KMS_SECRET_KEY: "my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw="
+ MINIO_DRIVE_MAX_TIMEOUT: "5s"
+ healthcheck:
+ test: ["CMD", "mc", "ready", "local"]
+ interval: 5s
+ timeout: 5s
+ retries: 5
+
+# starts 4 docker containers running minio server instances.
+# using nginx reverse proxy, load balancing, you can access
+# it through port 9000.
+services:
+ minio1:
+ <<: *minio-common
+ hostname: minio1
+ volumes:
+ - rdata1-1:/rdata1
+ - rdata1-2:/rdata2
+
+ minio2:
+ <<: *minio-common
+ hostname: minio2
+ volumes:
+ - rdata2-1:/rdata1
+ - rdata2-2:/rdata2
+
+ minio3:
+ <<: *minio-common
+ hostname: minio3
+ volumes:
+ - rdata3-1:/rdata1
+ - rdata3-2:/rdata2
+
+ minio4:
+ <<: *minio-common
+ hostname: minio4
+ volumes:
+ - rdata4-1:/rdata1
+ - rdata4-2:/rdata2
+
+ nginx:
+ image: nginx:1.19.2-alpine
+ hostname: nginx
+ volumes:
+ - ./nginx-4-node.conf:/etc/nginx/nginx.conf:ro
+ ports:
+ - "9000:9000"
+ - "9001:9001"
+ depends_on:
+ - minio1
+ - minio2
+ - minio3
+ - minio4
+
+## By default this config uses default local driver,
+## For custom volumes replace with volume driver configuration.
+volumes:
+ rdata1-1:
+ rdata1-2:
+ rdata2-1:
+ rdata2-2:
+ rdata3-1:
+ rdata3-2:
+ rdata4-1:
+ rdata4-2:
diff --git a/.github/workflows/mint/nginx-1-node.conf b/.github/workflows/mint/nginx-1-node.conf
new file mode 100644
index 00000000..bb13bde2
--- /dev/null
+++ b/.github/workflows/mint/nginx-1-node.conf
@@ -0,0 +1,100 @@
+user nginx;
+worker_processes auto;
+
+error_log /var/log/nginx/error.log warn;
+pid /var/run/nginx.pid;
+
+events {
+ worker_connections 4096;
+}
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ access_log /var/log/nginx/access.log main;
+ sendfile on;
+ keepalive_timeout 65;
+
+ # include /etc/nginx/conf.d/*.conf;
+
+ upstream minio {
+ server minio1:9000;
+ }
+
+ upstream console {
+ ip_hash;
+ server minio1:9001;
+ }
+
+ server {
+ listen 9000;
+ listen [::]:9000;
+ server_name localhost;
+
+ # To allow special characters in headers
+ ignore_invalid_headers off;
+ # Allow any size file to be uploaded.
+ # Set to a value such as 1000m; to restrict file size to a specific value
+ client_max_body_size 0;
+ # To disable buffering
+ proxy_buffering off;
+ proxy_request_buffering off;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ proxy_connect_timeout 300;
+ # Default is HTTP/1, keepalive is only enabled in HTTP/1.1
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ chunked_transfer_encoding off;
+
+ proxy_pass http://minio;
+ }
+ }
+
+ server {
+ listen 9001;
+ listen [::]:9001;
+ server_name localhost;
+
+ # To allow special characters in headers
+ ignore_invalid_headers off;
+ # Allow any size file to be uploaded.
+ # Set to a value such as 1000m; to restrict file size to a specific value
+ client_max_body_size 0;
+ # To disable buffering
+ proxy_buffering off;
+ proxy_request_buffering off;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-NginX-Proxy true;
+
+ # This is necessary to pass the correct IP to be hashed
+ real_ip_header X-Real-IP;
+
+ proxy_connect_timeout 300;
+
+ # To support websocket
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+
+ chunked_transfer_encoding off;
+
+ proxy_pass http://console;
+ }
+ }
+}
diff --git a/.github/workflows/mint/nginx-4-node.conf b/.github/workflows/mint/nginx-4-node.conf
new file mode 100644
index 00000000..b849940d
--- /dev/null
+++ b/.github/workflows/mint/nginx-4-node.conf
@@ -0,0 +1,105 @@
+user nginx;
+worker_processes auto;
+
+error_log /var/log/nginx/error.log warn;
+pid /var/run/nginx.pid;
+
+events {
+ worker_connections 4096;
+}
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ access_log /var/log/nginx/access.log main;
+ sendfile on;
+ keepalive_timeout 65;
+
+ # include /etc/nginx/conf.d/*.conf;
+
+ upstream minio {
+ server minio1:9000 max_fails=1 fail_timeout=10s;
+ server minio2:9000 max_fails=1 fail_timeout=10s;
+ server minio3:9000 max_fails=1 fail_timeout=10s;
+ }
+
+ upstream console {
+ ip_hash;
+ server minio1:9001;
+ server minio2:9001;
+ server minio3:9001;
+ server minio4:9001;
+ }
+
+ server {
+ listen 9000;
+ listen [::]:9000;
+ server_name localhost;
+
+ # To allow special characters in headers
+ ignore_invalid_headers off;
+ # Allow any size file to be uploaded.
+ # Set to a value such as 1000m; to restrict file size to a specific value
+ client_max_body_size 0;
+ # To disable buffering
+ proxy_buffering off;
+ proxy_request_buffering off;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ proxy_connect_timeout 300;
+ # Default is HTTP/1, keepalive is only enabled in HTTP/1.1
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ chunked_transfer_encoding off;
+
+ proxy_pass http://minio;
+ }
+ }
+
+ server {
+ listen 9001;
+ listen [::]:9001;
+ server_name localhost;
+
+ # To allow special characters in headers
+ ignore_invalid_headers off;
+ # Allow any size file to be uploaded.
+ # Set to a value such as 1000m; to restrict file size to a specific value
+ client_max_body_size 0;
+ # To disable buffering
+ proxy_buffering off;
+ proxy_request_buffering off;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-NginX-Proxy true;
+
+ # This is necessary to pass the correct IP to be hashed
+ real_ip_header X-Real-IP;
+
+ proxy_connect_timeout 300;
+
+ # To support websocket
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+
+ chunked_transfer_encoding off;
+
+ proxy_pass http://console;
+ }
+ }
+}
diff --git a/.github/workflows/mint/nginx-8-node.conf b/.github/workflows/mint/nginx-8-node.conf
new file mode 100644
index 00000000..278b5ae7
--- /dev/null
+++ b/.github/workflows/mint/nginx-8-node.conf
@@ -0,0 +1,114 @@
+user nginx;
+worker_processes auto;
+
+error_log /var/log/nginx/error.log warn;
+pid /var/run/nginx.pid;
+
+events {
+ worker_connections 4096;
+}
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ access_log /var/log/nginx/access.log main;
+ sendfile on;
+ keepalive_timeout 65;
+
+ # include /etc/nginx/conf.d/*.conf;
+
+ upstream minio {
+ server minio1:9000 max_fails=1 fail_timeout=10s;
+ server minio2:9000 max_fails=1 fail_timeout=10s;
+ server minio3:9000 max_fails=1 fail_timeout=10s;
+ server minio4:9000 max_fails=1 fail_timeout=10s;
+ server minio5:9000 max_fails=1 fail_timeout=10s;
+ server minio6:9000 max_fails=1 fail_timeout=10s;
+ server minio7:9000 max_fails=1 fail_timeout=10s;
+ server minio8:9000 max_fails=1 fail_timeout=10s;
+ }
+
+ upstream console {
+ ip_hash;
+ server minio1:9001;
+ server minio2:9001;
+ server minio3:9001;
+ server minio4:9001;
+ server minio5:9001;
+ server minio6:9001;
+ server minio7:9001;
+ server minio8:9001;
+ }
+
+ server {
+ listen 9000;
+ listen [::]:9000;
+ server_name localhost;
+
+ # To allow special characters in headers
+ ignore_invalid_headers off;
+ # Allow any size file to be uploaded.
+ # Set to a value such as 1000m; to restrict file size to a specific value
+ client_max_body_size 0;
+ # To disable buffering
+ proxy_buffering off;
+ proxy_request_buffering off;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ proxy_connect_timeout 300;
+ # Default is HTTP/1, keepalive is only enabled in HTTP/1.1
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ chunked_transfer_encoding off;
+
+ proxy_pass http://minio;
+ }
+ }
+
+ server {
+ listen 9001;
+ listen [::]:9001;
+ server_name localhost;
+
+ # To allow special characters in headers
+ ignore_invalid_headers off;
+ # Allow any size file to be uploaded.
+ # Set to a value such as 1000m; to restrict file size to a specific value
+ client_max_body_size 0;
+ # To disable buffering
+ proxy_buffering off;
+ proxy_request_buffering off;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-NginX-Proxy true;
+
+ # This is necessary to pass the correct IP to be hashed
+ real_ip_header X-Real-IP;
+
+ proxy_connect_timeout 300;
+
+ # To support websocket
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+
+ chunked_transfer_encoding off;
+
+ proxy_pass http://console;
+ }
+ }
+}
diff --git a/.github/workflows/mint/nginx.conf b/.github/workflows/mint/nginx.conf
new file mode 100644
index 00000000..1455a3e2
--- /dev/null
+++ b/.github/workflows/mint/nginx.conf
@@ -0,0 +1,106 @@
+user nginx;
+worker_processes auto;
+
+error_log /var/log/nginx/error.log warn;
+pid /var/run/nginx.pid;
+
+events {
+ worker_connections 4096;
+}
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ access_log /var/log/nginx/access.log main;
+ sendfile on;
+ keepalive_timeout 65;
+
+ # include /etc/nginx/conf.d/*.conf;
+
+ upstream minio {
+ server minio1:9000 max_fails=1 fail_timeout=10s;
+ server minio2:9000 max_fails=1 fail_timeout=10s;
+ server minio3:9000 max_fails=1 fail_timeout=10s;
+ server minio4:9000 max_fails=1 fail_timeout=10s;
+ }
+
+ upstream console {
+ ip_hash;
+ server minio1:9001;
+ server minio2:9001;
+ server minio3:9001;
+ server minio4:9001;
+ }
+
+ server {
+ listen 9000;
+ listen [::]:9000;
+ server_name localhost;
+
+ # To allow special characters in headers
+ ignore_invalid_headers off;
+ # Allow any size file to be uploaded.
+ # Set to a value such as 1000m; to restrict file size to a specific value
+ client_max_body_size 0;
+ # To disable buffering
+ proxy_buffering off;
+ proxy_request_buffering off;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ proxy_connect_timeout 300;
+ # Default is HTTP/1, keepalive is only enabled in HTTP/1.1
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ chunked_transfer_encoding off;
+
+ proxy_pass http://minio;
+ }
+ }
+
+ server {
+ listen 9001;
+ listen [::]:9001;
+ server_name localhost;
+
+ # To allow special characters in headers
+ ignore_invalid_headers off;
+ # Allow any size file to be uploaded.
+ # Set to a value such as 1000m; to restrict file size to a specific value
+ client_max_body_size 0;
+ # To disable buffering
+ proxy_buffering off;
+ proxy_request_buffering off;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-NginX-Proxy true;
+
+ # This is necessary to pass the correct IP to be hashed
+ real_ip_header X-Real-IP;
+
+ proxy_connect_timeout 300;
+
+ # To support websocket
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+
+ chunked_transfer_encoding off;
+
+ proxy_pass http://console;
+ }
+ }
+}
diff --git a/.github/workflows/multipart/docker-compose-site1.yaml b/.github/workflows/multipart/docker-compose-site1.yaml
new file mode 100644
index 00000000..b4a665ef
--- /dev/null
+++ b/.github/workflows/multipart/docker-compose-site1.yaml
@@ -0,0 +1,66 @@
+version: '3.7'
+
+# Settings and configurations that are common for all containers
+x-minio-common: &minio-common
+ image: quay.io/minio/minio:${RELEASE}
+ command: server http://site1-minio{1...4}/data{1...2}
+ environment:
+ - MINIO_PROMETHEUS_AUTH_TYPE=public
+ - CI=true
+
+# starts 4 docker containers running minio server instances.
+# using nginx reverse proxy, load balancing, you can access
+# it through port 9000.
+services:
+ site1-minio1:
+ <<: *minio-common
+ hostname: site1-minio1
+ volumes:
+ - site1-data1-1:/data1
+ - site1-data1-2:/data2
+
+ site1-minio2:
+ <<: *minio-common
+ hostname: site1-minio2
+ volumes:
+ - site1-data2-1:/data1
+ - site1-data2-2:/data2
+
+ site1-minio3:
+ <<: *minio-common
+ hostname: site1-minio3
+ volumes:
+ - site1-data3-1:/data1
+ - site1-data3-2:/data2
+
+ site1-minio4:
+ <<: *minio-common
+ hostname: site1-minio4
+ volumes:
+ - site1-data4-1:/data1
+ - site1-data4-2:/data2
+
+ site1-nginx:
+ image: nginx:1.19.2-alpine
+ hostname: site1-nginx
+ volumes:
+ - ./nginx-site1.conf:/etc/nginx/nginx.conf:ro
+ ports:
+ - "9001:9001"
+ depends_on:
+ - site1-minio1
+ - site1-minio2
+ - site1-minio3
+ - site1-minio4
+
+## By default this config uses default local driver,
+## For custom volumes replace with volume driver configuration.
+volumes:
+ site1-data1-1:
+ site1-data1-2:
+ site1-data2-1:
+ site1-data2-2:
+ site1-data3-1:
+ site1-data3-2:
+ site1-data4-1:
+ site1-data4-2:
diff --git a/.github/workflows/multipart/docker-compose-site2.yaml b/.github/workflows/multipart/docker-compose-site2.yaml
new file mode 100644
index 00000000..31a1f7a8
--- /dev/null
+++ b/.github/workflows/multipart/docker-compose-site2.yaml
@@ -0,0 +1,66 @@
+version: '3.7'
+
+# Settings and configurations that are common for all containers
+x-minio-common: &minio-common
+ image: quay.io/minio/minio:${RELEASE}
+ command: server http://site2-minio{1...4}/data{1...2}
+ environment:
+ - MINIO_PROMETHEUS_AUTH_TYPE=public
+ - CI=true
+
+# starts 4 docker containers running minio server instances.
+# using nginx reverse proxy, load balancing, you can access
+# it through port 9000.
+services:
+ site2-minio1:
+ <<: *minio-common
+ hostname: site2-minio1
+ volumes:
+ - site2-data1-1:/data1
+ - site2-data1-2:/data2
+
+ site2-minio2:
+ <<: *minio-common
+ hostname: site2-minio2
+ volumes:
+ - site2-data2-1:/data1
+ - site2-data2-2:/data2
+
+ site2-minio3:
+ <<: *minio-common
+ hostname: site2-minio3
+ volumes:
+ - site2-data3-1:/data1
+ - site2-data3-2:/data2
+
+ site2-minio4:
+ <<: *minio-common
+ hostname: site2-minio4
+ volumes:
+ - site2-data4-1:/data1
+ - site2-data4-2:/data2
+
+ site2-nginx:
+ image: nginx:1.19.2-alpine
+ hostname: site2-nginx
+ volumes:
+ - ./nginx-site2.conf:/etc/nginx/nginx.conf:ro
+ ports:
+ - "9002:9002"
+ depends_on:
+ - site2-minio1
+ - site2-minio2
+ - site2-minio3
+ - site2-minio4
+
+## By default this config uses default local driver,
+## For custom volumes replace with volume driver configuration.
+volumes:
+ site2-data1-1:
+ site2-data1-2:
+ site2-data2-1:
+ site2-data2-2:
+ site2-data3-1:
+ site2-data3-2:
+ site2-data4-1:
+ site2-data4-2:
diff --git a/.github/workflows/multipart/migrate.sh b/.github/workflows/multipart/migrate.sh
new file mode 100755
index 00000000..6058b008
--- /dev/null
+++ b/.github/workflows/multipart/migrate.sh
@@ -0,0 +1,147 @@
+#!/bin/bash
+
+set -x
+
+## change working directory
+cd .github/workflows/multipart/
+
+function cleanup() {
+ docker-compose -f docker-compose-site1.yaml rm -s -f || true
+ docker-compose -f docker-compose-site2.yaml rm -s -f || true
+ for volume in $(docker volume ls -q | grep minio); do
+ docker volume rm ${volume} || true
+ done
+
+ docker system prune -f || true
+ docker volume prune -f || true
+ docker volume rm $(docker volume ls -q -f dangling=true) || true
+}
+
+cleanup
+
+if [ ! -f ./mc ]; then
+ wget --quiet -O mc https://dl.minio.io/client/mc/release/linux-amd64/mc &&
+ chmod +x mc
+fi
+
+export RELEASE=RELEASE.2023-08-29T23-07-35Z
+
+docker-compose -f docker-compose-site1.yaml up -d
+docker-compose -f docker-compose-site2.yaml up -d
+
+sleep 30s
+
+./mc alias set site1 http://site1-nginx:9001 minioadmin minioadmin --api s3v4
+./mc alias set site2 http://site2-nginx:9002 minioadmin minioadmin --api s3v4
+
+./mc ready site1/
+./mc ready site2/
+
+./mc admin replicate add site1 site2
+./mc mb site1/testbucket/
+./mc cp -r --quiet /usr/bin site1/testbucket/
+
+sleep 5
+
+./s3-check-md5 -h
+
+failed_count_site1=$(./s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site1-nginx:9001 -bucket testbucket 2>&1 | grep FAILED | wc -l)
+failed_count_site2=$(./s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site2-nginx:9002 -bucket testbucket 2>&1 | grep FAILED | wc -l)
+
+if [ $failed_count_site1 -ne 0 ]; then
+ echo "failed with multipart on site1 uploads"
+ exit 1
+fi
+
+if [ $failed_count_site2 -ne 0 ]; then
+ echo "failed with multipart on site2 uploads"
+ exit 1
+fi
+
+./mc cp -r --quiet /usr/bin site1/testbucket/
+
+sleep 5
+
+failed_count_site1=$(./s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site1-nginx:9001 -bucket testbucket 2>&1 | grep FAILED | wc -l)
+failed_count_site2=$(./s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site2-nginx:9002 -bucket testbucket 2>&1 | grep FAILED | wc -l)
+
+## we do not need to fail here, since we are going to test
+## upgrading to master, healing and being able to recover
+## the last version.
+if [ $failed_count_site1 -ne 0 ]; then
+ echo "failed with multipart on site1 uploads ${failed_count_site1}"
+fi
+
+if [ $failed_count_site2 -ne 0 ]; then
+ echo "failed with multipart on site2 uploads ${failed_count_site2}"
+fi
+
+export RELEASE=${1}
+
+docker-compose -f docker-compose-site1.yaml up -d
+docker-compose -f docker-compose-site2.yaml up -d
+
+./mc ready site1/
+./mc ready site2/
+
+for i in $(seq 1 10); do
+ # mc admin heal -r --remove when used against a LB endpoint
+ # behaves flaky, let this run 10 times before giving up
+ ./mc admin heal -r --remove --json site1/ 2>&1 >/dev/null
+ ./mc admin heal -r --remove --json site2/ 2>&1 >/dev/null
+done
+
+failed_count_site1=$(./s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site1-nginx:9001 -bucket testbucket 2>&1 | grep FAILED | wc -l)
+failed_count_site2=$(./s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site2-nginx:9002 -bucket testbucket 2>&1 | grep FAILED | wc -l)
+
+if [ $failed_count_site1 -ne 0 ]; then
+ echo "failed with multipart on site1 uploads"
+ exit 1
+fi
+
+if [ $failed_count_site2 -ne 0 ]; then
+ echo "failed with multipart on site2 uploads"
+ exit 1
+fi
+
+# Add user group test
+./mc admin user add site1 site-replication-issue-user site-replication-issue-password
+./mc admin group add site1 site-replication-issue-group site-replication-issue-user
+
+max_wait_attempts=30
+wait_interval=5
+
+attempt=1
+while true; do
+ diff <(./mc admin group info site1 site-replication-issue-group) <(./mc admin group info site2 site-replication-issue-group)
+
+ if [[ $? -eq 0 ]]; then
+ echo "Outputs are consistent."
+ break
+ fi
+
+ remaining_attempts=$((max_wait_attempts - attempt))
+ if ((attempt >= max_wait_attempts)); then
+ echo "Outputs remain inconsistent after $max_wait_attempts attempts. Exiting with error."
+ exit 1
+ else
+ echo "Outputs are inconsistent. Waiting for $wait_interval seconds (attempt $attempt/$max_wait_attempts)."
+ sleep $wait_interval
+ fi
+
+ ((attempt++))
+done
+
+status=$(./mc admin group info site1 site-replication-issue-group --json | jq .groupStatus | tr -d '"')
+
+if [[ $status == "enabled" ]]; then
+ echo "Success"
+else
+ echo "Expected status: enabled, actual status: $status"
+ exit 1
+fi
+
+cleanup
+
+## change working directory
+cd ../../../
diff --git a/.github/workflows/multipart/nginx-site1.conf b/.github/workflows/multipart/nginx-site1.conf
new file mode 100644
index 00000000..1a37b9d2
--- /dev/null
+++ b/.github/workflows/multipart/nginx-site1.conf
@@ -0,0 +1,61 @@
+user nginx;
+worker_processes auto;
+
+error_log /var/log/nginx/error.log warn;
+pid /var/run/nginx.pid;
+
+events {
+ worker_connections 4096;
+}
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ access_log /var/log/nginx/access.log main;
+ sendfile on;
+ keepalive_timeout 65;
+
+ # include /etc/nginx/conf.d/*.conf;
+
+ upstream minio {
+ server site1-minio1:9000;
+ server site1-minio2:9000;
+ server site1-minio3:9000;
+ server site1-minio4:9000;
+ }
+
+ server {
+ listen 9001;
+ listen [::]:9001;
+ server_name localhost;
+
+ # To allow special characters in headers
+ ignore_invalid_headers off;
+ # Allow any size file to be uploaded.
+ # Set to a value such as 1000m; to restrict file size to a specific value
+ client_max_body_size 0;
+ # To disable buffering
+ proxy_buffering off;
+ proxy_request_buffering off;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ proxy_connect_timeout 300;
+ # Default is HTTP/1, keepalive is only enabled in HTTP/1.1
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ chunked_transfer_encoding off;
+
+ proxy_pass http://minio;
+ }
+ }
+}
diff --git a/.github/workflows/multipart/nginx-site2.conf b/.github/workflows/multipart/nginx-site2.conf
new file mode 100644
index 00000000..b93bba24
--- /dev/null
+++ b/.github/workflows/multipart/nginx-site2.conf
@@ -0,0 +1,61 @@
+user nginx;
+worker_processes auto;
+
+error_log /var/log/nginx/error.log warn;
+pid /var/run/nginx.pid;
+
+events {
+ worker_connections 4096;
+}
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ access_log /var/log/nginx/access.log main;
+ sendfile on;
+ keepalive_timeout 65;
+
+ # include /etc/nginx/conf.d/*.conf;
+
+ upstream minio {
+ server site2-minio1:9000;
+ server site2-minio2:9000;
+ server site2-minio3:9000;
+ server site2-minio4:9000;
+ }
+
+ server {
+ listen 9002;
+ listen [::]:9002;
+ server_name localhost;
+
+ # To allow special characters in headers
+ ignore_invalid_headers off;
+ # Allow any size file to be uploaded.
+ # Set to a value such as 1000m; to restrict file size to a specific value
+ client_max_body_size 0;
+ # To disable buffering
+ proxy_buffering off;
+ proxy_request_buffering off;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ proxy_connect_timeout 300;
+ # Default is HTTP/1, keepalive is only enabled in HTTP/1.1
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ chunked_transfer_encoding off;
+
+ proxy_pass http://minio;
+ }
+ }
+}
diff --git a/.github/workflows/replication.yaml b/.github/workflows/replication.yaml
new file mode 100644
index 00000000..18c32774
--- /dev/null
+++ b/.github/workflows/replication.yaml
@@ -0,0 +1,79 @@
+name: MinIO advanced 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:
+ replication-test:
+ name: Advanced Tests with Go ${{ matrix.go-version }}
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ go-version: [1.24.x]
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version: ${{ matrix.go-version }}
+ check-latest: true
+ - name: Test Decom
+ run: |
+ sudo sysctl net.ipv6.conf.all.disable_ipv6=0
+ sudo sysctl net.ipv6.conf.default.disable_ipv6=0
+ make test-decom
+
+ - name: Test ILM
+ run: |
+ sudo sysctl net.ipv6.conf.all.disable_ipv6=0
+ sudo sysctl net.ipv6.conf.default.disable_ipv6=0
+ make test-ilm
+ make test-ilm-transition
+
+ - name: Test PBAC
+ run: |
+ sudo sysctl net.ipv6.conf.all.disable_ipv6=0
+ sudo sysctl net.ipv6.conf.default.disable_ipv6=0
+ make test-pbac
+
+ - name: Test Config File
+ run: |
+ sudo sysctl net.ipv6.conf.all.disable_ipv6=0
+ sudo sysctl net.ipv6.conf.default.disable_ipv6=0
+ make test-configfile
+
+ - name: Test Replication
+ run: |
+ sudo sysctl net.ipv6.conf.all.disable_ipv6=0
+ sudo sysctl net.ipv6.conf.default.disable_ipv6=0
+ make test-replication
+
+ - name: Test MinIO IDP for automatic site replication
+ run: |
+ sudo sysctl net.ipv6.conf.all.disable_ipv6=0
+ sudo sysctl net.ipv6.conf.default.disable_ipv6=0
+ make test-site-replication-minio
+
+ - name: Test Versioning
+ run: |
+ sudo sysctl net.ipv6.conf.all.disable_ipv6=0
+ sudo sysctl net.ipv6.conf.default.disable_ipv6=0
+ make test-versioning
+
+ - name: Test Multipart upload with failures
+ run: |
+ sudo sysctl net.ipv6.conf.all.disable_ipv6=0
+ sudo sysctl net.ipv6.conf.default.disable_ipv6=0
+ make test-multipart
diff --git a/.github/workflows/root-disable.yml b/.github/workflows/root-disable.yml
new file mode 100644
index 00000000..c08fb8b1
--- /dev/null
+++ b/.github/workflows/root-disable.yml
@@ -0,0 +1,34 @@
+name: Root lockdown 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.24.x]
+ os: [ubuntu-latest]
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version: ${{ matrix.go-version }}
+ check-latest: true
+ - name: Start root lockdown tests
+ run: |
+ make test-root-disable
diff --git a/.github/workflows/root.cert b/.github/workflows/root.cert
new file mode 100644
index 00000000..5f220f79
--- /dev/null
+++ b/.github/workflows/root.cert
@@ -0,0 +1,9 @@
+-----BEGIN CERTIFICATE-----
+MIIBKDCB26ADAgECAhB6vebGMUfKnmBKyqoApRSOMAUGAytlcDAbMRkwFwYDVQQD
+DBByb290QHBsYXkubWluLmlvMB4XDTIwMDQzMDE1MjIyNVoXDTI1MDQyOTE1MjIy
+NVowGzEZMBcGA1UEAwwQcm9vdEBwbGF5Lm1pbi5pbzAqMAUGAytlcAMhALzn735W
+fmSH/ghKs+4iPWziZMmWdiWr/sqvqeW+WwSxozUwMzAOBgNVHQ8BAf8EBAMCB4Aw
+EwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAFBgMrZXADQQDZOrGK
+b2ATkDlu2pTcP3LyhSBDpYh7V4TvjRkBTRgjkacCzwFLm+mh+7US8V4dBpIDsJ4u
+uWoF0y6vbLVGIlkG
+-----END CERTIFICATE-----
diff --git a/.github/workflows/root.key b/.github/workflows/root.key
new file mode 100644
index 00000000..53a47e25
--- /dev/null
+++ b/.github/workflows/root.key
@@ -0,0 +1,3 @@
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEID9E7FSYWrMD+VjhI6q545cYT9YOyFxZb7UnjEepYDRc
+-----END PRIVATE KEY-----
diff --git a/.github/workflows/run-mint.sh b/.github/workflows/run-mint.sh
new file mode 100755
index 00000000..0bbc1cba
--- /dev/null
+++ b/.github/workflows/run-mint.sh
@@ -0,0 +1,64 @@
+#!/bin/bash
+
+set -ex
+
+export MODE="$1"
+export ACCESS_KEY="$2"
+export SECRET_KEY="$3"
+export JOB_NAME="$4"
+export MINT_MODE="full"
+
+docker system prune -f || true
+docker volume prune -f || true
+docker volume rm $(docker volume ls -f dangling=true) || true
+
+## change working directory
+cd .github/workflows/mint
+
+## always pull latest
+docker pull docker.io/minio/mint:edge
+
+docker-compose -f minio-${MODE}.yaml up -d
+sleep 1m
+
+docker system prune -f || true
+docker volume prune -f || true
+docker volume rm $(docker volume ls -q -f dangling=true) || true
+
+# Stop two nodes, one of each pool, to check that all S3 calls work while quorum is still there
+[ "${MODE}" == "pools" ] && docker-compose -f minio-${MODE}.yaml stop minio2
+[ "${MODE}" == "pools" ] && docker-compose -f minio-${MODE}.yaml stop minio6
+
+# Pause one node, to check that all S3 calls work while one node goes wrong
+[ "${MODE}" == "resiliency" ] && docker-compose -f minio-${MODE}.yaml pause minio4
+
+docker run --rm --net=mint_default \
+ --name="mint-${MODE}-${JOB_NAME}" \
+ -e SERVER_ENDPOINT="nginx:9000" \
+ -e ACCESS_KEY="${ACCESS_KEY}" \
+ -e SECRET_KEY="${SECRET_KEY}" \
+ -e ENABLE_HTTPS=0 \
+ -e MINT_MODE="${MINT_MODE}" \
+ docker.io/minio/mint:edge
+
+# FIXME: enable this after fixing aws-sdk-java-v2 tests
+# # unpause the node, to check that all S3 calls work while one node goes wrong
+# [ "${MODE}" == "resiliency" ] && docker-compose -f minio-${MODE}.yaml unpause minio4
+# [ "${MODE}" == "resiliency" ] && docker run --rm --net=mint_default \
+# --name="mint-${MODE}-${JOB_NAME}" \
+# -e SERVER_ENDPOINT="nginx:9000" \
+# -e ACCESS_KEY="${ACCESS_KEY}" \
+# -e SECRET_KEY="${SECRET_KEY}" \
+# -e ENABLE_HTTPS=0 \
+# -e MINT_MODE="${MINT_MODE}" \
+# docker.io/minio/mint:edge
+
+docker-compose -f minio-${MODE}.yaml down || true
+sleep 10s
+
+docker system prune -f || true
+docker volume prune -f || true
+docker volume rm $(docker volume ls -q -f dangling=true) || true
+
+## change working directory
+cd ../../../
diff --git a/.github/workflows/shfmt.yml b/.github/workflows/shfmt.yml
new file mode 100644
index 00000000..3e446306
--- /dev/null
+++ b/.github/workflows/shfmt.yml
@@ -0,0 +1,22 @@
+name: Shell formatting checks
+
+on:
+ pull_request:
+ branches:
+ - master
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ name: runner / shfmt
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: luizm/action-sh-checker@master
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SHFMT_OPTS: "-s"
+ with:
+ sh_checker_shellcheck_disable: true # disable for now
diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml
new file mode 100644
index 00000000..7addb314
--- /dev/null
+++ b/.github/workflows/typos.yml
@@ -0,0 +1,15 @@
+---
+name: Spelling
+on: [pull_request]
+
+jobs:
+ run:
+ name: Spell Check with Typos
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Actions Repository
+ uses: actions/checkout@v4
+
+ - name: Check spelling of repo
+ uses: crate-ci/typos@master
+
diff --git a/.github/workflows/upgrade-ci-cd.yaml b/.github/workflows/upgrade-ci-cd.yaml
new file mode 100644
index 00000000..d3e71ff5
--- /dev/null
+++ b/.github/workflows/upgrade-ci-cd.yaml
@@ -0,0 +1,34 @@
+name: Upgrade old version 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.24.x]
+ os: [ubuntu-latest]
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version: ${{ matrix.go-version }}
+ check-latest: true
+ - name: Start upgrade tests
+ run: |
+ make test-upgrade
diff --git a/.github/workflows/vulncheck.yml b/.github/workflows/vulncheck.yml
new file mode 100644
index 00000000..5dfcde04
--- /dev/null
+++ b/.github/workflows/vulncheck.yml
@@ -0,0 +1,31 @@
+name: VulnCheck
+on:
+ pull_request:
+ branches:
+ - master
+
+ push:
+ branches:
+ - master
+
+permissions:
+ contents: read # to fetch code (actions/checkout)
+
+jobs:
+ vulncheck:
+ name: Analysis
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out code into the Go module directory
+ uses: actions/checkout@v4
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: 1.24.x
+ cached: false
+ - name: Get official govulncheck
+ run: go install golang.org/x/vuln/cmd/govulncheck@latest
+ shell: bash
+ - name: Run govulncheck
+ run: govulncheck -show verbose ./...
+ shell: bash
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..3a51a0b2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,55 @@
+**/*.swp
+cover.out
+*~
+minio
+!*/
+site/
+**/*.test
+**/*.sublime-workspace
+/.idea/
+/Minio.iml
+**/access.log
+vendor/
+.DS_Store
+*.syso
+coverage.txt
+.vscode/
+*.tar.bz2
+parts/
+prime/
+stage/
+.sia_temp/
+config.json
+node_modules/
+mc.*
+s3-check-md5*
+xl-meta*
+healing-*
+inspect*.zip
+200M*
+hash-set
+minio.RELEASE*
+mc
+nancy
+inspects/*
+.bin/
+*.gz
+docs/debugging/s3-verify/s3-verify
+docs/debugging/xl-meta/xl-meta
+docs/debugging/s3-check-md5/s3-check-md5
+docs/debugging/hash-set/hash-set
+docs/debugging/healing-bin/healing-bin
+docs/debugging/inspect/inspect
+docs/debugging/pprofgoparser/pprofgoparser
+docs/debugging/reorder-disks/reorder-disks
+docs/debugging/populate-hard-links/populate-hardlinks
+docs/debugging/xattr/xattr
+hash-set
+healing-bin
+inspect
+pprofgoparser
+reorder-disks
+s3-check-md5
+s3-verify
+xattr
+xl-meta
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 00000000..0533d7cd
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,64 @@
+version: "2"
+linters:
+ default: none
+ enable:
+ - durationcheck
+ - forcetypeassert
+ - gocritic
+ - gomodguard
+ - govet
+ - ineffassign
+ - misspell
+ - revive
+ - staticcheck
+ - unconvert
+ - 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:
+ max-issues-per-linter: 100
+ max-same-issues: 100
+formatters:
+ enable:
+ - gofumpt
+ - goimports
+ exclusions:
+ generated: lax
+ paths:
+ - third_party$
+ - builtin$
+ - examples$
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 00000000..4adf1be9
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,18 @@
+# Generate CONTRIBUTORS.md: contributors.sh
+
+# Tip for finding duplicates (besides scanning the output of CONTRIBUTORS.md for name
+# duplicates that aren't also email duplicates): scan the output of:
+# git log --format='%aE - %aN' | sort -uf
+#
+# For explanation on this file format: man git-shortlog
+
+Anand Babu (AB) Periasamy Anand Babu (AB) Periasamy
+Anand Babu (AB) Periasamy
+Anis Elleuch
+Frederick F. Kautz IV
+Harshavardhana
+Harshavardhana
+Harshavardhana
+Krishna Srinivas
+Matthew Farrellee
+Nate Rosenblum
\ No newline at end of file
diff --git a/.typos.toml b/.typos.toml
new file mode 100644
index 00000000..3168e9dd
--- /dev/null
+++ b/.typos.toml
@@ -0,0 +1,45 @@
+[files]
+extend-exclude = [".git/", "docs/", "CREDITS", "go.mod", "go.sum"]
+ignore-hidden = false
+
+[default]
+extend-ignore-re = [
+ "Patrick Collison",
+ "Copyright 2014 Unknwon",
+ "[0-9A-Za-z/+=]{64}",
+ "ZXJuZXQxDjAMBgNVBA-some-junk-Q4wDAYDVQQLEwVNaW5pbzEOMAwGA1UEAxMF",
+ "eyJmb28iOiJiYXIifQ",
+ "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.*",
+ "MIIDBTCCAe2gAwIBAgIQWHw7h.*",
+ 'http\.Header\{"X-Amz-Server-Side-Encryptio":',
+ "ZoEoZdLlzVbOlT9rbhD7ZN7TLyiYXSAlB79uGEge",
+ "ERRO:",
+ "(?Rm)^.*(#|//)\\s*spellchecker:disable-line$", # ignore line
+]
+
+[default.extend-words]
+"encrypter" = "encrypter"
+"kms" = "kms"
+"requestor" = "requestor"
+
+[default.extend-identifiers]
+"HashiCorp" = "HashiCorp"
+
+[type.go.extend-identifiers]
+"bui" = "bui"
+"dm2nd" = "dm2nd"
+"ot" = "ot"
+"ParseND" = "ParseND"
+"ParseNDStream" = "ParseNDStream"
+"pn" = "pn"
+"TestGetPartialObjectMisAligned" = "TestGetPartialObjectMisAligned"
+"thr" = "thr"
+"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]*",
+]
diff --git a/CNAME b/CNAME
new file mode 100644
index 00000000..f5ea8034
--- /dev/null
+++ b/CNAME
@@ -0,0 +1 @@
+charts.min.io
diff --git a/COMPLIANCE.md b/COMPLIANCE.md
new file mode 100644
index 00000000..40a58640
--- /dev/null
+++ b/COMPLIANCE.md
@@ -0,0 +1,7 @@
+# AGPLv3 Compliance
+
+We have designed MinIO as an Open Source software for the Open Source software community. This requires applications to consider whether their usage of MinIO is in compliance with the GNU AGPLv3 [license](https://github.com/minio/minio/blob/master/LICENSE).
+
+MinIO cannot make the determination as to whether your application's usage of MinIO is in compliance with the AGPLv3 license requirements. You should instead rely on your own legal counsel or licensing specialists to audit and ensure your application is in compliance with the licenses of MinIO and all other open-source projects with which your application integrates or interacts. We understand that AGPLv3 licensing is complex and nuanced. It is for that reason we strongly encourage using experts in licensing to make any such determinations around compliance instead of relying on apocryphal or anecdotal advice.
+
+[MinIO Commercial Licensing](https://min.io/pricing) is the best option for applications that trigger AGPLv3 obligations (e.g. open sourcing your application). Applications using MinIO - or any other OSS-licensed code - without validating their usage do so at their own risk.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..c99df744
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,82 @@
+# MinIO Contribution Guide [](https://slack.min.io) [](https://hub.docker.com/r/minio/minio/)
+
+``MinIO`` community welcomes your contribution. To make the process as seamless as possible, we recommend you read this contribution guide.
+
+## Development Workflow
+
+Start by forking the MinIO GitHub repository, make changes in a branch and then send a pull request. We encourage pull requests to discuss code changes. Here are the steps in details:
+
+### Setup your MinIO GitHub Repository
+
+Fork [MinIO upstream](https://github.com/minio/minio/fork) source repository to your own personal repository. Copy the URL of your MinIO fork (you will need it for the `git clone` command below).
+
+```sh
+git clone https://github.com/minio/minio
+cd minio
+go install -v
+ls $(go env GOPATH)/bin/minio
+```
+
+### Set up git remote as ``upstream``
+
+```sh
+$ cd minio
+$ git remote add upstream https://github.com/minio/minio
+$ git fetch upstream
+$ git merge upstream/master
+...
+```
+
+### Create your feature branch
+
+Before making code changes, make sure you create a separate branch for these changes
+
+```
+git checkout -b my-new-feature
+```
+
+### Test MinIO server changes
+
+After your code changes, make sure
+
+- To add test cases for the new code. If you have questions about how to do it, please ask on our [Slack](https://slack.min.io) channel.
+- To run `make verifiers`
+- To squash your commits into a single commit. `git rebase -i`. It's okay to force update your pull request.
+- To run `make test` and `make build` completes.
+
+### Commit changes
+
+After verification, commit your changes. This is a [great post](https://chris.beams.io/posts/git-commit/) on how to write useful commit messages
+
+```
+git commit -am 'Add some feature'
+```
+
+### Push to the branch
+
+Push your locally committed changes to the remote origin (your fork)
+
+```
+git push origin my-new-feature
+```
+
+### Create a Pull Request
+
+Pull requests can be created via GitHub. Refer to [this document](https://help.github.com/articles/creating-a-pull-request/) for detailed steps on how to create a pull request. After a Pull Request gets peer reviewed and approved, it will be merged.
+
+## FAQs
+
+### How does ``MinIO`` manage dependencies?
+
+``MinIO`` uses `go mod` to manage its dependencies.
+
+- Run `go get foo/bar` in the source folder to add the dependency to `go.mod` file.
+
+To remove a dependency
+
+- Edit your code and remove the import reference.
+- Run `go mod tidy` in the source folder to remove dependency from `go.mod` file.
+
+### What are the coding guidelines for MinIO?
+
+``MinIO`` is fully conformant with Golang style. Refer: [Effective Go](https://github.com/golang/go/wiki/CodeReviewComments) article from Golang project. If you observe offending code, please feel free to send a pull request or ping us on [Slack](https://slack.min.io).
diff --git a/CREDITS b/CREDITS
new file mode 100644
index 00000000..c70aed83
--- /dev/null
+++ b/CREDITS
@@ -0,0 +1,35316 @@
+Go (the standard library)
+https://golang.org/
+----------------------------------------------------------------
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+aead.dev/mem
+https://aead.dev/mem
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2022 Andreas Auernhammer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+aead.dev/minisign
+https://aead.dev/minisign
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2021 Andreas Auernhammer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+cel.dev/expr
+https://cel.dev/expr
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+cloud.google.com/go
+https://cloud.google.com/go
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+cloud.google.com/go/auth
+https://cloud.google.com/go/auth
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+cloud.google.com/go/auth/oauth2adapt
+https://cloud.google.com/go/auth/oauth2adapt
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+cloud.google.com/go/compute/metadata
+https://cloud.google.com/go/compute/metadata
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+cloud.google.com/go/iam
+https://cloud.google.com/go/iam
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+cloud.google.com/go/logging
+https://cloud.google.com/go/logging
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+cloud.google.com/go/longrunning
+https://cloud.google.com/go/longrunning
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+cloud.google.com/go/monitoring
+https://cloud.google.com/go/monitoring
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+cloud.google.com/go/storage
+https://cloud.google.com/go/storage
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+cloud.google.com/go/trace
+https://cloud.google.com/go/trace
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+filippo.io/edwards25519
+https://filippo.io/edwards25519
+----------------------------------------------------------------
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/Azure/azure-sdk-for-go/sdk/azcore
+https://github.com/Azure/azure-sdk-for-go/sdk/azcore
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) Microsoft Corporation.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE
+
+================================================================
+
+github.com/Azure/azure-sdk-for-go/sdk/azidentity
+https://github.com/Azure/azure-sdk-for-go/sdk/azidentity
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) Microsoft Corporation.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE
+
+================================================================
+
+github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache
+https://github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) Microsoft Corporation.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE
+
+================================================================
+
+github.com/Azure/azure-sdk-for-go/sdk/internal
+https://github.com/Azure/azure-sdk-for-go/sdk/internal
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) Microsoft Corporation.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE
+
+================================================================
+
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage
+https://github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) Microsoft Corporation. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+================================================================
+
+github.com/Azure/azure-sdk-for-go/sdk/storage/azblob
+https://github.com/Azure/azure-sdk-for-go/sdk/storage/azblob
+----------------------------------------------------------------
+ MIT License
+
+ Copyright (c) Microsoft Corporation. All rights reserved.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE
+================================================================
+
+github.com/Azure/go-ntlmssp
+https://github.com/Azure/go-ntlmssp
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2016 Microsoft
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/AzureAD/microsoft-authentication-extensions-for-go/cache
+https://github.com/AzureAD/microsoft-authentication-extensions-for-go/cache
+----------------------------------------------------------------
+ MIT License
+
+ Copyright (c) Microsoft Corporation.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE
+
+================================================================
+
+github.com/AzureAD/microsoft-authentication-library-for-go
+https://github.com/AzureAD/microsoft-authentication-library-for-go
+----------------------------------------------------------------
+ MIT License
+
+ Copyright (c) Microsoft Corporation.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE
+
+================================================================
+
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp
+https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric
+https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock
+https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping
+https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/IBM/sarama
+https://github.com/IBM/sarama
+----------------------------------------------------------------
+# MIT License
+
+Copyright (c) 2013 Shopify
+
+Copyright (c) 2023 IBM Corporation
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+================================================================
+
+github.com/VividCortex/ewma
+https://github.com/VividCortex/ewma
+----------------------------------------------------------------
+The MIT License
+
+Copyright (c) 2013 VividCortex
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+================================================================
+
+github.com/acarl005/stripansi
+https://github.com/acarl005/stripansi
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2018 Andrew Carlson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/alecthomas/participle
+https://github.com/alecthomas/participle
+----------------------------------------------------------------
+Copyright (C) 2017 Alec Thomas
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/alexbrainman/sspi
+https://github.com/alexbrainman/sspi
+----------------------------------------------------------------
+Copyright (c) 2012 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/apache/thrift
+https://github.com/apache/thrift
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+--------------------------------------------------
+SOFTWARE DISTRIBUTED WITH THRIFT:
+
+The Apache Thrift software includes a number of subcomponents with
+separate copyright notices and license terms. Your use of the source
+code for the these subcomponents is subject to the terms and
+conditions of the following licenses.
+
+--------------------------------------------------
+Portions of the following files are licensed under the MIT License:
+
+ lib/erl/src/Makefile.am
+
+Please see doc/otp-base-license.txt for the full terms of this license.
+
+--------------------------------------------------
+For the aclocal/ax_boost_base.m4 and contrib/fb303/aclocal/ax_boost_base.m4 components:
+
+# Copyright (c) 2007 Thomas Porschberg
+#
+# Copying and distribution of this file, with or without
+# modification, are permitted in any medium without royalty provided
+# the copyright notice and this notice are preserved.
+
+--------------------------------------------------
+For the lib/nodejs/lib/thrift/json_parse.js:
+
+/*
+ json_parse.js
+ 2015-05-02
+ Public Domain.
+ NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
+
+*/
+(By Douglas Crockford )
+
+--------------------------------------------------
+For lib/cpp/src/thrift/windows/SocketPair.cpp
+
+/* socketpair.c
+ * Copyright 2007 by Nathan C. Myers ; some rights reserved.
+ * This code is Free Software. It may be copied freely, in original or
+ * modified form, subject only to the restrictions that (1) the author is
+ * relieved from all responsibilities for any use for any purpose, and (2)
+ * this copyright notice must be retained, unchanged, in its entirety. If
+ * for any reason the author might be held responsible for any consequences
+ * of copying or use, license is withheld.
+ */
+
+
+--------------------------------------------------
+For lib/py/compat/win32/stdint.h
+
+// ISO C9x compliant stdint.h for Microsoft Visual Studio
+// Based on ISO/IEC 9899:TC2 Committee draft (May 6, 2005) WG14/N1124
+//
+// Copyright (c) 2006-2008 Alexander Chemeris
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+// this list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// 3. The name of the author may be used to endorse or promote products
+// derived from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+
+--------------------------------------------------
+Codegen template in t_html_generator.h
+
+* Bootstrap v2.0.3
+*
+* Copyright 2012 Twitter, Inc
+* Licensed under the Apache License v2.0
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Designed and built with all the love in the world @twitter by @mdo and @fat.
+
+---------------------------------------------------
+For t_cl_generator.cc
+
+ * Copyright (c) 2008- Patrick Collison
+ * Copyright (c) 2006- Facebook
+
+---------------------------------------------------
+
+================================================================
+
+github.com/armon/go-metrics
+https://github.com/armon/go-metrics
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2013 Armon Dadgar
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+================================================================
+
+github.com/asaskevich/govalidator
+https://github.com/asaskevich/govalidator
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2014-2020 Alex Saskevich
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+================================================================
+
+github.com/aymanbagabas/go-osc52/v2
+https://github.com/aymanbagabas/go-osc52/v2
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2022 Ayman Bagabas
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/beevik/ntp
+https://github.com/beevik/ntp
+----------------------------------------------------------------
+Copyright © 2015-2023 Brett Vickers. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER ``AS IS'' AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/beorn7/perks
+https://github.com/beorn7/perks
+----------------------------------------------------------------
+Copyright (C) 2013 Blake Mizerany
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+================================================================
+
+github.com/buger/jsonparser
+https://github.com/buger/jsonparser
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2016 Leonid Bugaev
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/cespare/xxhash/v2
+https://github.com/cespare/xxhash/v2
+----------------------------------------------------------------
+Copyright (c) 2016 Caleb Spare
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+================================================================
+
+github.com/charmbracelet/bubbles
+https://github.com/charmbracelet/bubbles
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2020-2023 Charmbracelet, Inc
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/charmbracelet/bubbletea
+https://github.com/charmbracelet/bubbletea
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2020-2023 Charmbracelet, Inc
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/charmbracelet/lipgloss
+https://github.com/charmbracelet/lipgloss
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2021-2023 Charmbracelet, Inc
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/charmbracelet/x/ansi
+https://github.com/charmbracelet/x/ansi
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2023 Charmbracelet, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/charmbracelet/x/exp/golden
+https://github.com/charmbracelet/x/exp/golden
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2023 Charmbracelet, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/charmbracelet/x/term
+https://github.com/charmbracelet/x/term
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2023 Charmbracelet, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/cheggaaa/pb
+https://github.com/cheggaaa/pb
+----------------------------------------------------------------
+Copyright (c) 2012-2015, Sergey Cherepanov
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+* Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+================================================================
+
+github.com/cncf/xds/go
+https://github.com/cncf/xds/go
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/coreos/go-oidc/v3
+https://github.com/coreos/go-oidc/v3
+----------------------------------------------------------------
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+================================================================
+
+github.com/coreos/go-semver
+https://github.com/coreos/go-semver
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/coreos/go-systemd/v22
+https://github.com/coreos/go-systemd/v22
+----------------------------------------------------------------
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, "control" means (i) the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+"Object" form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+"submitted" means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+2. Grant of Copyright License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+3. Grant of Patent License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution.
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+If the Work includes a "NOTICE" text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+5. Submission of Contributions.
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+6. Trademarks.
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty.
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+8. Limitation of Liability.
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability.
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets "[]" replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same "printed page" as the copyright notice for easier identification within
+third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/cosnicolaou/pbzip2
+https://github.com/cosnicolaou/pbzip2
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/davecgh/go-spew
+https://github.com/davecgh/go-spew
+----------------------------------------------------------------
+ISC License
+
+Copyright (c) 2012-2016 Dave Collins
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+================================================================
+
+github.com/dchest/siphash
+https://github.com/dchest/siphash
+----------------------------------------------------------------
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+ HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+ i. the right to reproduce, adapt, distribute, perform, display,
+ communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+ likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+ subject to the limitations in paragraph 4(a), below;
+ v. rights protecting the extraction, dissemination, use and reuse of data
+ in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+ European Parliament and of the Council of 11 March 1996 on the legal
+ protection of databases, and under any national implementation
+ thereof, including any amended or successor version of such
+ directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+ world based on applicable law or treaty, and any national
+ implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+ surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+ warranties of any kind concerning the Work, express, implied,
+ statutory or otherwise, including without limitation warranties of
+ title, merchantability, fitness for a particular purpose, non
+ infringement, or the absence of latent or other defects, accuracy, or
+ the present or absence of errors, whether or not discoverable, all to
+ the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+ that may apply to the Work or any use thereof, including without
+ limitation any person's Copyright and Related Rights in the Work.
+ Further, Affirmer disclaims responsibility for obtaining any necessary
+ consents, permissions or other rights required for any use of the
+ Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+ party to this document and has no duty or obligation with respect to
+ this CC0 or use of the Work.
+
+================================================================
+
+github.com/decred/dcrd/dcrec/secp256k1/v4
+https://github.com/decred/dcrd/dcrec/secp256k1/v4
+----------------------------------------------------------------
+ISC License
+
+Copyright (c) 2013-2017 The btcsuite developers
+Copyright (c) 2015-2024 The Decred developers
+Copyright (c) 2017 The Lightning Network Developers
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+================================================================
+
+github.com/dgryski/go-rendezvous
+https://github.com/dgryski/go-rendezvous
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2017-2020 Damian Gryski
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+================================================================
+
+github.com/docker/go-units
+https://github.com/docker/go-units
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ https://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ Copyright 2015 Docker, Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/dustin/go-humanize
+https://github.com/dustin/go-humanize
+----------------------------------------------------------------
+Copyright (c) 2005-2008 Dustin Sallings
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+
+================================================================
+
+github.com/eapache/go-resiliency
+https://github.com/eapache/go-resiliency
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2014 Evan Huus
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+================================================================
+
+github.com/eapache/go-xerial-snappy
+https://github.com/eapache/go-xerial-snappy
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2016 Evan Huus
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/eapache/queue
+https://github.com/eapache/queue
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2014 Evan Huus
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+================================================================
+
+github.com/eclipse/paho.mqtt.golang
+https://github.com/eclipse/paho.mqtt.golang
+----------------------------------------------------------------
+Eclipse Public License - v 2.0 (EPL-2.0)
+
+This program and the accompanying materials
+are made available under the terms of the Eclipse Public License v2.0
+and Eclipse Distribution License v1.0 which accompany this distribution.
+
+The Eclipse Public License is available at
+ https://www.eclipse.org/legal/epl-2.0/
+and the Eclipse Distribution License is available at
+ http://www.eclipse.org/org/documents/edl-v10.php.
+
+For an explanation of what dual-licensing means to you, see:
+https://www.eclipse.org/legal/eplfaq.php#DUALLIC
+
+****
+The epl-2.0 is copied below in order to pass the pkg.go.dev license check (https://pkg.go.dev/license-policy).
+****
+Eclipse Public License - v 2.0
+
+ THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
+ PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
+ OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+ a) in the case of the initial Contributor, the initial content
+ Distributed under this Agreement, and
+
+ b) in the case of each subsequent Contributor:
+ i) changes to the Program, and
+ ii) additions to the Program;
+ where such changes and/or additions to the Program originate from
+ and are Distributed by that particular Contributor. A Contribution
+ "originates" from a Contributor if it was added to the Program by
+ such Contributor itself or anyone acting on such Contributor's behalf.
+ Contributions do not include changes or additions to the Program that
+ are not Modified Works.
+
+"Contributor" means any person or entity that Distributes the Program.
+
+"Licensed Patents" mean patent claims licensable by a Contributor which
+are necessarily infringed by the use or sale of its Contribution alone
+or when combined with the Program.
+
+"Program" means the Contributions Distributed in accordance with this
+Agreement.
+
+"Recipient" means anyone who receives the Program under this Agreement
+or any Secondary License (as applicable), including Contributors.
+
+"Derivative Works" shall mean any work, whether in Source Code or other
+form, that is based on (or derived from) the Program and for which the
+editorial revisions, annotations, elaborations, or other modifications
+represent, as a whole, an original work of authorship.
+
+"Modified Works" shall mean any work in Source Code or other form that
+results from an addition to, deletion from, or modification of the
+contents of the Program, including, for purposes of clarity any new file
+in Source Code form that contains any contents of the Program. Modified
+Works shall not include works that contain only declarations,
+interfaces, types, classes, structures, or files of the Program solely
+in each case in order to link to, bind by name, or subclass the Program
+or Modified Works thereof.
+
+"Distribute" means the acts of a) distributing or b) making available
+in any manner that enables the transfer of a copy.
+
+"Source Code" means the form of a Program preferred for making
+modifications, including but not limited to software source code,
+documentation source, and configuration files.
+
+"Secondary License" means either the GNU General Public License,
+Version 2.0, or any later versions of that license, including any
+exceptions or additional permissions as identified by the initial
+Contributor.
+
+2. GRANT OF RIGHTS
+
+ a) Subject to the terms of this Agreement, each Contributor hereby
+ grants Recipient a non-exclusive, worldwide, royalty-free copyright
+ license to reproduce, prepare Derivative Works of, publicly display,
+ publicly perform, Distribute and sublicense the Contribution of such
+ Contributor, if any, and such Derivative Works.
+
+ b) Subject to the terms of this Agreement, each Contributor hereby
+ grants Recipient a non-exclusive, worldwide, royalty-free patent
+ license under Licensed Patents to make, use, sell, offer to sell,
+ import and otherwise transfer the Contribution of such Contributor,
+ if any, in Source Code or other form. This patent license shall
+ apply to the combination of the Contribution and the Program if, at
+ the time the Contribution is added by the Contributor, such addition
+ of the Contribution causes such combination to be covered by the
+ Licensed Patents. The patent license shall not apply to any other
+ combinations which include the Contribution. No hardware per se is
+ licensed hereunder.
+
+ c) Recipient understands that although each Contributor grants the
+ licenses to its Contributions set forth herein, no assurances are
+ provided by any Contributor that the Program does not infringe the
+ patent or other intellectual property rights of any other entity.
+ Each Contributor disclaims any liability to Recipient for claims
+ brought by any other entity based on infringement of intellectual
+ property rights or otherwise. As a condition to exercising the
+ rights and licenses granted hereunder, each Recipient hereby
+ assumes sole responsibility to secure any other intellectual
+ property rights needed, if any. For example, if a third party
+ patent license is required to allow Recipient to Distribute the
+ Program, it is Recipient's responsibility to acquire that license
+ before distributing the Program.
+
+ d) Each Contributor represents that to its knowledge it has
+ sufficient copyright rights in its Contribution, if any, to grant
+ the copyright license set forth in this Agreement.
+
+ e) Notwithstanding the terms of any Secondary License, no
+ Contributor makes additional grants to any Recipient (other than
+ those set forth in this Agreement) as a result of such Recipient's
+ receipt of the Program under the terms of a Secondary License
+ (if permitted under the terms of Section 3).
+
+3. REQUIREMENTS
+
+3.1 If a Contributor Distributes the Program in any form, then:
+
+ a) the Program must also be made available as Source Code, in
+ accordance with section 3.2, and the Contributor must accompany
+ the Program with a statement that the Source Code for the Program
+ is available under this Agreement, and informs Recipients how to
+ obtain it in a reasonable manner on or through a medium customarily
+ used for software exchange; and
+
+ b) the Contributor may Distribute the Program under a license
+ different than this Agreement, provided that such license:
+ i) effectively disclaims on behalf of all other Contributors all
+ warranties and conditions, express and implied, including
+ warranties or conditions of title and non-infringement, and
+ implied warranties or conditions of merchantability and fitness
+ for a particular purpose;
+
+ ii) effectively excludes on behalf of all other Contributors all
+ liability for damages, including direct, indirect, special,
+ incidental and consequential damages, such as lost profits;
+
+ iii) does not attempt to limit or alter the recipients' rights
+ in the Source Code under section 3.2; and
+
+ iv) requires any subsequent distribution of the Program by any
+ party to be under a license that satisfies the requirements
+ of this section 3.
+
+3.2 When the Program is Distributed as Source Code:
+
+ a) it must be made available under this Agreement, or if the
+ Program (i) is combined with other material in a separate file or
+ files made available under a Secondary License, and (ii) the initial
+ Contributor attached to the Source Code the notice described in
+ Exhibit A of this Agreement, then the Program may be made available
+ under the terms of such Secondary Licenses, and
+
+ b) a copy of this Agreement must be included with each copy of
+ the Program.
+
+3.3 Contributors may not remove or alter any copyright, patent,
+trademark, attribution notices, disclaimers of warranty, or limitations
+of liability ("notices") contained within the Program from any copy of
+the Program which they Distribute, provided that Contributors may add
+their own appropriate notices.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain responsibilities
+with respect to end users, business partners and the like. While this
+license is intended to facilitate the commercial use of the Program,
+the Contributor who includes the Program in a commercial product
+offering should do so in a manner which does not create potential
+liability for other Contributors. Therefore, if a Contributor includes
+the Program in a commercial product offering, such Contributor
+("Commercial Contributor") hereby agrees to defend and indemnify every
+other Contributor ("Indemnified Contributor") against any losses,
+damages and costs (collectively "Losses") arising from claims, lawsuits
+and other legal actions brought by a third party against the Indemnified
+Contributor to the extent caused by the acts or omissions of such
+Commercial Contributor in connection with its distribution of the Program
+in a commercial product offering. The obligations in this section do not
+apply to any claims or Losses relating to any actual or alleged
+intellectual property infringement. In order to qualify, an Indemnified
+Contributor must: a) promptly notify the Commercial Contributor in
+writing of such claim, and b) allow the Commercial Contributor to control,
+and cooperate with the Commercial Contributor in, the defense and any
+related settlement negotiations. The Indemnified Contributor may
+participate in any such claim at its own expense.
+
+For example, a Contributor might include the Program in a commercial
+product offering, Product X. That Contributor is then a Commercial
+Contributor. If that Commercial Contributor then makes performance
+claims, or offers warranties related to Product X, those performance
+claims and warranties are such Commercial Contributor's responsibility
+alone. Under this section, the Commercial Contributor would have to
+defend claims against the other Contributors related to those performance
+claims and warranties, and if a court requires any other Contributor to
+pay any damages as a result, the Commercial Contributor must pay
+those damages.
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
+PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS"
+BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
+IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF
+TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR
+PURPOSE. Each Recipient is solely responsible for determining the
+appropriateness of using and distributing the Program and assumes all
+risks associated with its exercise of rights under this Agreement,
+including but not limited to the risks and costs of program errors,
+compliance with applicable laws, damage to or loss of data, programs
+or equipment, and unavailability or interruption of operations.
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
+PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS
+SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST
+PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
+EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+7. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under
+applicable law, it shall not affect the validity or enforceability of
+the remainder of the terms of this Agreement, and without further
+action by the parties hereto, such provision shall be reformed to the
+minimum extent necessary to make such provision valid and enforceable.
+
+If Recipient institutes patent litigation against any entity
+(including a cross-claim or counterclaim in a lawsuit) alleging that the
+Program itself (excluding combinations of the Program with other software
+or hardware) infringes such Recipient's patent(s), then such Recipient's
+rights granted under Section 2(b) shall terminate as of the date such
+litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if it
+fails to comply with any of the material terms or conditions of this
+Agreement and does not cure such failure in a reasonable period of
+time after becoming aware of such noncompliance. If all Recipient's
+rights under this Agreement terminate, Recipient agrees to cease use
+and distribution of the Program as soon as reasonably practicable.
+However, Recipient's obligations under this Agreement and any licenses
+granted by Recipient relating to the Program shall continue and survive.
+
+Everyone is permitted to copy and distribute copies of this Agreement,
+but in order to avoid inconsistency the Agreement is copyrighted and
+may only be modified in the following manner. The Agreement Steward
+reserves the right to publish new versions (including revisions) of
+this Agreement from time to time. No one other than the Agreement
+Steward has the right to modify this Agreement. The Eclipse Foundation
+is the initial Agreement Steward. The Eclipse Foundation may assign the
+responsibility to serve as the Agreement Steward to a suitable separate
+entity. Each new version of the Agreement will be given a distinguishing
+version number. The Program (including Contributions) may always be
+Distributed subject to the version of the Agreement under which it was
+received. In addition, after a new version of the Agreement is published,
+Contributor may elect to Distribute the Program (including its
+Contributions) under the new version.
+
+Except as expressly stated in Sections 2(a) and 2(b) above, Recipient
+receives no rights or licenses to the intellectual property of any
+Contributor under this Agreement, whether expressly, by implication,
+estoppel or otherwise. All rights in the Program not expressly granted
+under this Agreement are reserved. Nothing in this Agreement is intended
+to be enforceable by any entity that is not a Contributor or Recipient.
+No third-party beneficiary rights are created under this Agreement.
+
+Exhibit A - Form of Secondary Licenses Notice
+
+"This Source Code may also be made available under the following
+Secondary Licenses when the conditions for such availability set forth
+in the Eclipse Public License, v. 2.0 are satisfied: {name license(s),
+version(s), and exceptions or additional permissions here}."
+
+ Simply including a copy of this Agreement, including this Exhibit A
+ is not sufficient to license the Source Code under Secondary Licenses.
+
+ If it is not possible or desirable to put the notice in a particular
+ file, then You may include the notice in a location (such as a LICENSE
+ file in a relevant directory) where a recipient would be likely to
+ look for such a notice.
+
+ You may add additional accurate notices of copyright ownership.
+
+================================================================
+
+github.com/elastic/go-elasticsearch/v7
+https://github.com/elastic/go-elasticsearch/v7
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2018 Elasticsearch BV
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/envoyproxy/go-control-plane
+https://github.com/envoyproxy/go-control-plane
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/envoyproxy/go-control-plane/envoy
+https://github.com/envoyproxy/go-control-plane/envoy
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/envoyproxy/go-control-plane/ratelimit
+https://github.com/envoyproxy/go-control-plane/ratelimit
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/envoyproxy/protoc-gen-validate
+https://github.com/envoyproxy/protoc-gen-validate
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/erikgeiser/coninput
+https://github.com/erikgeiser/coninput
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2021 Erik G.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/fatih/color
+https://github.com/fatih/color
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2013 Fatih Arslan
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+================================================================
+
+github.com/fatih/structs
+https://github.com/fatih/structs
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2014 Fatih Arslan
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+================================================================
+
+github.com/felixge/fgprof
+https://github.com/felixge/fgprof
+----------------------------------------------------------------
+The MIT License (MIT)
+Copyright © 2020 Felix Geisendörfer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+================================================================
+
+github.com/felixge/httpsnoop
+https://github.com/felixge/httpsnoop
+----------------------------------------------------------------
+Copyright (c) 2016 Felix Geisendörfer (felix@debuggable.com)
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+
+================================================================
+
+github.com/fortytw2/leaktest
+https://github.com/fortytw2/leaktest
+----------------------------------------------------------------
+Copyright (c) 2012 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/fraugster/parquet-go
+https://github.com/fraugster/parquet-go
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/go-asn1-ber/asn1-ber
+https://github.com/go-asn1-ber/asn1-ber
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com)
+Portions copyright (c) 2015-2016 go-asn1-ber Authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/go-ini/ini
+https://github.com/go-ini/ini
+----------------------------------------------------------------
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, "control" means (i) the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+"Object" form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+"submitted" means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+2. Grant of Copyright License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+3. Grant of Patent License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution.
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+If the Work includes a "NOTICE" text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+5. Submission of Contributions.
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+6. Trademarks.
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty.
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+8. Limitation of Liability.
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability.
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets "[]" replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same "printed page" as the copyright notice for easier identification within
+third-party archives.
+
+ Copyright 2014 Unknwon
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/go-jose/go-jose/v4
+https://github.com/go-jose/go-jose/v4
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/go-ldap/ldap/v3
+https://github.com/go-ldap/ldap/v3
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com)
+Portions copyright (c) 2015-2024 go-ldap Authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/go-logr/logr
+https://github.com/go-logr/logr
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/go-logr/stdr
+https://github.com/go-logr/stdr
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/go-ole/go-ole
+https://github.com/go-ole/go-ole
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright © 2013-2017 Yasuhiro Matsumoto,
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the “Software”), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/go-openapi/analysis
+https://github.com/go-openapi/analysis
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/go-openapi/errors
+https://github.com/go-openapi/errors
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/go-openapi/jsonpointer
+https://github.com/go-openapi/jsonpointer
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/go-openapi/jsonreference
+https://github.com/go-openapi/jsonreference
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/go-openapi/loads
+https://github.com/go-openapi/loads
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/go-openapi/runtime
+https://github.com/go-openapi/runtime
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/go-openapi/spec
+https://github.com/go-openapi/spec
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/go-openapi/strfmt
+https://github.com/go-openapi/strfmt
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/go-openapi/swag
+https://github.com/go-openapi/swag
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/go-openapi/validate
+https://github.com/go-openapi/validate
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/go-sql-driver/mysql
+https://github.com/go-sql-driver/mysql
+----------------------------------------------------------------
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
+
+================================================================
+
+github.com/gobwas/httphead
+https://github.com/gobwas/httphead
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2017 Sergey Kamardin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/gobwas/pool
+https://github.com/gobwas/pool
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2017-2019 Sergey Kamardin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/gobwas/ws
+https://github.com/gobwas/ws
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2017-2021 Sergey Kamardin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/goccy/go-json
+https://github.com/goccy/go-json
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2020 Masaaki Goshima
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/gogo/protobuf
+https://github.com/gogo/protobuf
+----------------------------------------------------------------
+Copyright (c) 2013, The GoGo Authors. All rights reserved.
+
+Protocol Buffers for Go with Gadgets
+
+Go support for Protocol Buffers - Google's data interchange format
+
+Copyright 2010 The Go Authors. All rights reserved.
+https://github.com/golang/protobuf
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+================================================================
+
+github.com/golang-jwt/jwt/v4
+https://github.com/golang-jwt/jwt/v4
+----------------------------------------------------------------
+Copyright (c) 2012 Dave Grijalva
+Copyright (c) 2021 golang-jwt maintainers
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+================================================================
+
+github.com/golang-jwt/jwt/v5
+https://github.com/golang-jwt/jwt/v5
+----------------------------------------------------------------
+Copyright (c) 2012 Dave Grijalva
+Copyright (c) 2021 golang-jwt maintainers
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+================================================================
+
+github.com/golang/groupcache
+https://github.com/golang/groupcache
+----------------------------------------------------------------
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, "control" means (i) the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+"Object" form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+"submitted" means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+2. Grant of Copyright License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+3. Grant of Patent License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution.
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+If the Work includes a "NOTICE" text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+5. Submission of Contributions.
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+6. Trademarks.
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty.
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+8. Limitation of Liability.
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability.
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets "[]" replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same "printed page" as the copyright notice for easier identification within
+third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/golang/protobuf
+https://github.com/golang/protobuf
+----------------------------------------------------------------
+Copyright 2010 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+================================================================
+
+github.com/golang/snappy
+https://github.com/golang/snappy
+----------------------------------------------------------------
+Copyright (c) 2011 The Snappy-Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/gomodule/redigo
+https://github.com/gomodule/redigo
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+================================================================
+
+github.com/google/go-cmp
+https://github.com/google/go-cmp
+----------------------------------------------------------------
+Copyright (c) 2017 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/google/martian/v3
+https://github.com/google/martian/v3
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/google/pprof
+https://github.com/google/pprof
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/google/s2a-go
+https://github.com/google/s2a-go
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/google/shlex
+https://github.com/google/shlex
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/google/uuid
+https://github.com/google/uuid
+----------------------------------------------------------------
+Copyright (c) 2009,2014 Google Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/googleapis/enterprise-certificate-proxy
+https://github.com/googleapis/enterprise-certificate-proxy
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/googleapis/gax-go/v2
+https://github.com/googleapis/gax-go/v2
+----------------------------------------------------------------
+Copyright 2016, Google Inc.
+All rights reserved.
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/gorilla/mux
+https://github.com/gorilla/mux
+----------------------------------------------------------------
+Copyright (c) 2023 The Gorilla Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/gorilla/websocket
+https://github.com/gorilla/websocket
+----------------------------------------------------------------
+Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/hashicorp/errwrap
+https://github.com/hashicorp/errwrap
+----------------------------------------------------------------
+Mozilla Public License, version 2.0
+
+1. Definitions
+
+1.1. “Contributor”
+
+ means each individual or legal entity that creates, contributes to the
+ creation of, or owns Covered Software.
+
+1.2. “Contributor Version”
+
+ means the combination of the Contributions of others (if any) used by a
+ Contributor and that particular Contributor’s Contribution.
+
+1.3. “Contribution”
+
+ means Covered Software of a particular Contributor.
+
+1.4. “Covered Software”
+
+ means Source Code Form to which the initial Contributor has attached the
+ notice in Exhibit A, the Executable Form of such Source Code Form, and
+ Modifications of such Source Code Form, in each case including portions
+ thereof.
+
+1.5. “Incompatible With Secondary Licenses”
+ means
+
+ a. that the initial Contributor has attached the notice described in
+ Exhibit B to the Covered Software; or
+
+ b. that the Covered Software was made available under the terms of version
+ 1.1 or earlier of the License, but not also under the terms of a
+ Secondary License.
+
+1.6. “Executable Form”
+
+ means any form of the work other than Source Code Form.
+
+1.7. “Larger Work”
+
+ means a work that combines Covered Software with other material, in a separate
+ file or files, that is not Covered Software.
+
+1.8. “License”
+
+ means this document.
+
+1.9. “Licensable”
+
+ means having the right to grant, to the maximum extent possible, whether at the
+ time of the initial grant or subsequently, any and all of the rights conveyed by
+ this License.
+
+1.10. “Modifications”
+
+ means any of the following:
+
+ a. any file in Source Code Form that results from an addition to, deletion
+ from, or modification of the contents of Covered Software; or
+
+ b. any new file in Source Code Form that contains any Covered Software.
+
+1.11. “Patent Claims” of a Contributor
+
+ means any patent claim(s), including without limitation, method, process,
+ and apparatus claims, in any patent Licensable by such Contributor that
+ would be infringed, but for the grant of the License, by the making,
+ using, selling, offering for sale, having made, import, or transfer of
+ either its Contributions or its Contributor Version.
+
+1.12. “Secondary License”
+
+ means either the GNU General Public License, Version 2.0, the GNU Lesser
+ General Public License, Version 2.1, the GNU Affero General Public
+ License, Version 3.0, or any later versions of those licenses.
+
+1.13. “Source Code Form”
+
+ means the form of the work preferred for making modifications.
+
+1.14. “You” (or “Your”)
+
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, “You” includes any entity that controls, is
+ controlled by, or is under common control with You. For purposes of this
+ definition, “control” means (a) the power, direct or indirect, to cause
+ the direction or management of such entity, whether by contract or
+ otherwise, or (b) ownership of more than fifty percent (50%) of the
+ outstanding shares or beneficial ownership of such entity.
+
+
+2. License Grants and Conditions
+
+2.1. Grants
+
+ Each Contributor hereby grants You a world-wide, royalty-free,
+ non-exclusive license:
+
+ a. under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or as
+ part of a Larger Work; and
+
+ b. under Patent Claims of such Contributor to make, use, sell, offer for
+ sale, have made, import, and otherwise transfer either its Contributions
+ or its Contributor Version.
+
+2.2. Effective Date
+
+ The licenses granted in Section 2.1 with respect to any Contribution become
+ effective for each Contribution on the date the Contributor first distributes
+ such Contribution.
+
+2.3. Limitations on Grant Scope
+
+ The licenses granted in this Section 2 are the only rights granted under this
+ License. No additional rights or licenses will be implied from the distribution
+ or licensing of Covered Software under this License. Notwithstanding Section
+ 2.1(b) above, no patent license is granted by a Contributor:
+
+ a. for any code that a Contributor has removed from Covered Software; or
+
+ b. for infringements caused by: (i) Your and any other third party’s
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+ c. under Patent Claims infringed by Covered Software in the absence of its
+ Contributions.
+
+ This License does not grant any rights in the trademarks, service marks, or
+ logos of any Contributor (except as may be necessary to comply with the
+ notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+ No Contributor makes additional grants as a result of Your choice to
+ distribute the Covered Software under a subsequent version of this License
+ (see Section 10.2) or under the terms of a Secondary License (if permitted
+ under the terms of Section 3.3).
+
+2.5. Representation
+
+ Each Contributor represents that the Contributor believes its Contributions
+ are its original creation(s) or it has sufficient rights to grant the
+ rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+ This License is not intended to limit any rights You have under applicable
+ copyright doctrines of fair use, fair dealing, or other equivalents.
+
+2.7. Conditions
+
+ Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
+ Section 2.1.
+
+
+3. Responsibilities
+
+3.1. Distribution of Source Form
+
+ All distribution of Covered Software in Source Code Form, including any
+ Modifications that You create or to which You contribute, must be under the
+ terms of this License. You must inform recipients that the Source Code Form
+ of the Covered Software is governed by the terms of this License, and how
+ they can obtain a copy of this License. You may not attempt to alter or
+ restrict the recipients’ rights in the Source Code Form.
+
+3.2. Distribution of Executable Form
+
+ If You distribute Covered Software in Executable Form then:
+
+ a. such Covered Software must also be made available in Source Code Form,
+ as described in Section 3.1, and You must inform recipients of the
+ Executable Form how they can obtain a copy of such Source Code Form by
+ reasonable means in a timely manner, at a charge no more than the cost
+ of distribution to the recipient; and
+
+ b. You may distribute such Executable Form under the terms of this License,
+ or sublicense it under different terms, provided that the license for
+ the Executable Form does not attempt to limit or alter the recipients’
+ rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+ You may create and distribute a Larger Work under terms of Your choice,
+ provided that You also comply with the requirements of this License for the
+ Covered Software. If the Larger Work is a combination of Covered Software
+ with a work governed by one or more Secondary Licenses, and the Covered
+ Software is not Incompatible With Secondary Licenses, this License permits
+ You to additionally distribute such Covered Software under the terms of
+ such Secondary License(s), so that the recipient of the Larger Work may, at
+ their option, further distribute the Covered Software under the terms of
+ either this License or such Secondary License(s).
+
+3.4. Notices
+
+ You may not remove or alter the substance of any license notices (including
+ copyright notices, patent notices, disclaimers of warranty, or limitations
+ of liability) contained within the Source Code Form of the Covered
+ Software, except that You may alter any license notices to the extent
+ required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+ You may choose to offer, and to charge a fee for, warranty, support,
+ indemnity or liability obligations to one or more recipients of Covered
+ Software. However, You may do so only on Your own behalf, and not on behalf
+ of any Contributor. You must make it absolutely clear that any such
+ warranty, support, indemnity, or liability obligation is offered by You
+ alone, and You hereby agree to indemnify every Contributor for any
+ liability incurred by such Contributor as a result of warranty, support,
+ indemnity or liability terms You offer. You may include additional
+ disclaimers of warranty and limitations of liability specific to any
+ jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+
+ If it is impossible for You to comply with any of the terms of this License
+ with respect to some or all of the Covered Software due to statute, judicial
+ order, or regulation then You must: (a) comply with the terms of this License
+ to the maximum extent possible; and (b) describe the limitations and the code
+ they affect. Such description must be placed in a text file included with all
+ distributions of the Covered Software under this License. Except to the
+ extent prohibited by statute or regulation, such description must be
+ sufficiently detailed for a recipient of ordinary skill to be able to
+ understand it.
+
+5. Termination
+
+5.1. The rights granted under this License will terminate automatically if You
+ fail to comply with any of its terms. However, if You become compliant,
+ then the rights granted under this License from a particular Contributor
+ are reinstated (a) provisionally, unless and until such Contributor
+ explicitly and finally terminates Your grants, and (b) on an ongoing basis,
+ if such Contributor fails to notify You of the non-compliance by some
+ reasonable means prior to 60 days after You have come back into compliance.
+ Moreover, Your grants from a particular Contributor are reinstated on an
+ ongoing basis if such Contributor notifies You of the non-compliance by
+ some reasonable means, this is the first time You have received notice of
+ non-compliance with this License from such Contributor, and You become
+ compliant prior to 30 days after Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+ infringement claim (excluding declaratory judgment actions, counter-claims,
+ and cross-claims) alleging that a Contributor Version directly or
+ indirectly infringes any patent, then the rights granted to You by any and
+ all Contributors for the Covered Software under Section 2.1 of this License
+ shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
+ license agreements (excluding distributors and resellers) which have been
+ validly granted by You or Your distributors under this License prior to
+ termination shall survive termination.
+
+6. Disclaimer of Warranty
+
+ Covered Software is provided under this License on an “as is” basis, without
+ warranty of any kind, either expressed, implied, or statutory, including,
+ without limitation, warranties that the Covered Software is free of defects,
+ merchantable, fit for a particular purpose or non-infringing. The entire
+ risk as to the quality and performance of the Covered Software is with You.
+ Should any Covered Software prove defective in any respect, You (not any
+ Contributor) assume the cost of any necessary servicing, repair, or
+ correction. This disclaimer of warranty constitutes an essential part of this
+ License. No use of any Covered Software is authorized under this License
+ except under this disclaimer.
+
+7. Limitation of Liability
+
+ Under no circumstances and under no legal theory, whether tort (including
+ negligence), contract, or otherwise, shall any Contributor, or anyone who
+ distributes Covered Software as permitted above, be liable to You for any
+ direct, indirect, special, incidental, or consequential damages of any
+ character including, without limitation, damages for lost profits, loss of
+ goodwill, work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses, even if such party shall have been
+ informed of the possibility of such damages. This limitation of liability
+ shall not apply to liability for death or personal injury resulting from such
+ party’s negligence to the extent applicable law prohibits such limitation.
+ Some jurisdictions do not allow the exclusion or limitation of incidental or
+ consequential damages, so this exclusion and limitation may not apply to You.
+
+8. Litigation
+
+ Any litigation relating to this License may be brought only in the courts of
+ a jurisdiction where the defendant maintains its principal place of business
+ and such litigation shall be governed by laws of that jurisdiction, without
+ reference to its conflict-of-law provisions. Nothing in this Section shall
+ prevent a party’s ability to bring cross-claims or counter-claims.
+
+9. Miscellaneous
+
+ This License represents the complete agreement concerning the subject matter
+ hereof. If any provision of this License is held to be unenforceable, such
+ provision shall be reformed only to the extent necessary to make it
+ enforceable. Any law or regulation which provides that the language of a
+ contract shall be construed against the drafter shall not be used to construe
+ this License against a Contributor.
+
+
+10. Versions of the License
+
+10.1. New Versions
+
+ Mozilla Foundation is the license steward. Except as provided in Section
+ 10.3, no one other than the license steward has the right to modify or
+ publish new versions of this License. Each version will be given a
+ distinguishing version number.
+
+10.2. Effect of New Versions
+
+ You may distribute the Covered Software under the terms of the version of
+ the License under which You originally received the Covered Software, or
+ under the terms of any subsequent version published by the license
+ steward.
+
+10.3. Modified Versions
+
+ If you create software not governed by this License, and you want to
+ create a new license for such software, you may create and use a modified
+ version of this License if you rename the license and remove any
+ references to the name of the license steward (except to note that such
+ modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
+ If You choose to distribute Source Code Form that is Incompatible With
+ Secondary Licenses under the terms of this version of the License, the
+ notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+
+ This Source Code Form is subject to the
+ terms of the Mozilla Public License, v.
+ 2.0. If a copy of the MPL was not
+ distributed with this file, You can
+ obtain one at
+ http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular file, then
+You may include the notice in a location (such as a LICENSE file in a relevant
+directory) where a recipient would be likely to look for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - “Incompatible With Secondary Licenses” Notice
+
+ This Source Code Form is “Incompatible
+ With Secondary Licenses”, as defined by
+ the Mozilla Public License, v. 2.0.
+
+
+================================================================
+
+github.com/hashicorp/go-hclog
+https://github.com/hashicorp/go-hclog
+----------------------------------------------------------------
+Copyright (c) 2017 HashiCorp, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/hashicorp/go-immutable-radix
+https://github.com/hashicorp/go-immutable-radix
+----------------------------------------------------------------
+Mozilla Public License, version 2.0
+
+1. Definitions
+
+1.1. "Contributor"
+
+ means each individual or legal entity that creates, contributes to the
+ creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+
+ means the combination of the Contributions of others (if any) used by a
+ Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+
+ means Source Code Form to which the initial Contributor has attached the
+ notice in Exhibit A, the Executable Form of such Source Code Form, and
+ Modifications of such Source Code Form, in each case including portions
+ thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ a. that the initial Contributor has attached the notice described in
+ Exhibit B to the Covered Software; or
+
+ b. that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the terms of
+ a Secondary License.
+
+1.6. "Executable Form"
+
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+
+ means a work that combines Covered Software with other material, in a
+ separate file or files, that is not Covered Software.
+
+1.8. "License"
+
+ means this document.
+
+1.9. "Licensable"
+
+ means having the right to grant, to the maximum extent possible, whether
+ at the time of the initial grant or subsequently, any and all of the
+ rights conveyed by this License.
+
+1.10. "Modifications"
+
+ means any of the following:
+
+ a. any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered Software; or
+
+ b. any new file in Source Code Form that contains any Covered Software.
+
+1.11. "Patent Claims" of a Contributor
+
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the License,
+ by the making, using, selling, offering for sale, having made, import,
+ or transfer of either its Contributions or its Contributor Version.
+
+1.12. "Secondary License"
+
+ means either the GNU General Public License, Version 2.0, the GNU Lesser
+ General Public License, Version 2.1, the GNU Affero General Public
+ License, Version 3.0, or any later versions of those licenses.
+
+1.13. "Source Code Form"
+
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that controls, is
+ controlled by, or is under common control with You. For purposes of this
+ definition, "control" means (a) the power, direct or indirect, to cause
+ the direction or management of such entity, whether by contract or
+ otherwise, or (b) ownership of more than fifty percent (50%) of the
+ outstanding shares or beneficial ownership of such entity.
+
+
+2. License Grants and Conditions
+
+2.1. Grants
+
+ Each Contributor hereby grants You a world-wide, royalty-free,
+ non-exclusive license:
+
+ a. under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+ b. under Patent Claims of such Contributor to make, use, sell, offer for
+ sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+ The licenses granted in Section 2.1 with respect to any Contribution
+ become effective for each Contribution on the date the Contributor first
+ distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+ The licenses granted in this Section 2 are the only rights granted under
+ this License. No additional rights or licenses will be implied from the
+ distribution or licensing of Covered Software under this License.
+ Notwithstanding Section 2.1(b) above, no patent license is granted by a
+ Contributor:
+
+ a. for any code that a Contributor has removed from Covered Software; or
+
+ b. for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+ c. under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+ This License does not grant any rights in the trademarks, service marks,
+ or logos of any Contributor (except as may be necessary to comply with
+ the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+ No Contributor makes additional grants as a result of Your choice to
+ distribute the Covered Software under a subsequent version of this
+ License (see Section 10.2) or under the terms of a Secondary License (if
+ permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+ Each Contributor represents that the Contributor believes its
+ Contributions are its original creation(s) or it has sufficient rights to
+ grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+ This License is not intended to limit any rights You have under
+ applicable copyright doctrines of fair use, fair dealing, or other
+ equivalents.
+
+2.7. Conditions
+
+ Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
+ Section 2.1.
+
+
+3. Responsibilities
+
+3.1. Distribution of Source Form
+
+ All distribution of Covered Software in Source Code Form, including any
+ Modifications that You create or to which You contribute, must be under
+ the terms of this License. You must inform recipients that the Source
+ Code Form of the Covered Software is governed by the terms of this
+ License, and how they can obtain a copy of this License. You may not
+ attempt to alter or restrict the recipients' rights in the Source Code
+ Form.
+
+3.2. Distribution of Executable Form
+
+ If You distribute Covered Software in Executable Form then:
+
+ a. such Covered Software must also be made available in Source Code Form,
+ as described in Section 3.1, and You must inform recipients of the
+ Executable Form how they can obtain a copy of such Source Code Form by
+ reasonable means in a timely manner, at a charge no more than the cost
+ of distribution to the recipient; and
+
+ b. You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter the
+ recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+ You may create and distribute a Larger Work under terms of Your choice,
+ provided that You also comply with the requirements of this License for
+ the Covered Software. If the Larger Work is a combination of Covered
+ Software with a work governed by one or more Secondary Licenses, and the
+ Covered Software is not Incompatible With Secondary Licenses, this
+ License permits You to additionally distribute such Covered Software
+ under the terms of such Secondary License(s), so that the recipient of
+ the Larger Work may, at their option, further distribute the Covered
+ Software under the terms of either this License or such Secondary
+ License(s).
+
+3.4. Notices
+
+ You may not remove or alter the substance of any license notices
+ (including copyright notices, patent notices, disclaimers of warranty, or
+ limitations of liability) contained within the Source Code Form of the
+ Covered Software, except that You may alter any license notices to the
+ extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+ You may choose to offer, and to charge a fee for, warranty, support,
+ indemnity or liability obligations to one or more recipients of Covered
+ Software. However, You may do so only on Your own behalf, and not on
+ behalf of any Contributor. You must make it absolutely clear that any
+ such warranty, support, indemnity, or liability obligation is offered by
+ You alone, and You hereby agree to indemnify every Contributor for any
+ liability incurred by such Contributor as a result of warranty, support,
+ indemnity or liability terms You offer. You may include additional
+ disclaimers of warranty and limitations of liability specific to any
+ jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+
+ If it is impossible for You to comply with any of the terms of this License
+ with respect to some or all of the Covered Software due to statute,
+ judicial order, or regulation then You must: (a) comply with the terms of
+ this License to the maximum extent possible; and (b) describe the
+ limitations and the code they affect. Such description must be placed in a
+ text file included with all distributions of the Covered Software under
+ this License. Except to the extent prohibited by statute or regulation,
+ such description must be sufficiently detailed for a recipient of ordinary
+ skill to be able to understand it.
+
+5. Termination
+
+5.1. The rights granted under this License will terminate automatically if You
+ fail to comply with any of its terms. However, if You become compliant,
+ then the rights granted under this License from a particular Contributor
+ are reinstated (a) provisionally, unless and until such Contributor
+ explicitly and finally terminates Your grants, and (b) on an ongoing
+ basis, if such Contributor fails to notify You of the non-compliance by
+ some reasonable means prior to 60 days after You have come back into
+ compliance. Moreover, Your grants from a particular Contributor are
+ reinstated on an ongoing basis if such Contributor notifies You of the
+ non-compliance by some reasonable means, this is the first time You have
+ received notice of non-compliance with this License from such
+ Contributor, and You become compliant prior to 30 days after Your receipt
+ of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+ infringement claim (excluding declaratory judgment actions,
+ counter-claims, and cross-claims) alleging that a Contributor Version
+ directly or indirectly infringes any patent, then the rights granted to
+ You by any and all Contributors for the Covered Software under Section
+ 2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
+ license agreements (excluding distributors and resellers) which have been
+ validly granted by You or Your distributors under this License prior to
+ termination shall survive termination.
+
+6. Disclaimer of Warranty
+
+ Covered Software is provided under this License on an "as is" basis,
+ without warranty of any kind, either expressed, implied, or statutory,
+ including, without limitation, warranties that the Covered Software is free
+ of defects, merchantable, fit for a particular purpose or non-infringing.
+ The entire risk as to the quality and performance of the Covered Software
+ is with You. Should any Covered Software prove defective in any respect,
+ You (not any Contributor) assume the cost of any necessary servicing,
+ repair, or correction. This disclaimer of warranty constitutes an essential
+ part of this License. No use of any Covered Software is authorized under
+ this License except under this disclaimer.
+
+7. Limitation of Liability
+
+ Under no circumstances and under no legal theory, whether tort (including
+ negligence), contract, or otherwise, shall any Contributor, or anyone who
+ distributes Covered Software as permitted above, be liable to You for any
+ direct, indirect, special, incidental, or consequential damages of any
+ character including, without limitation, damages for lost profits, loss of
+ goodwill, work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses, even if such party shall have been
+ informed of the possibility of such damages. This limitation of liability
+ shall not apply to liability for death or personal injury resulting from
+ such party's negligence to the extent applicable law prohibits such
+ limitation. Some jurisdictions do not allow the exclusion or limitation of
+ incidental or consequential damages, so this exclusion and limitation may
+ not apply to You.
+
+8. Litigation
+
+ Any litigation relating to this License may be brought only in the courts
+ of a jurisdiction where the defendant maintains its principal place of
+ business and such litigation shall be governed by laws of that
+ jurisdiction, without reference to its conflict-of-law provisions. Nothing
+ in this Section shall prevent a party's ability to bring cross-claims or
+ counter-claims.
+
+9. Miscellaneous
+
+ This License represents the complete agreement concerning the subject
+ matter hereof. If any provision of this License is held to be
+ unenforceable, such provision shall be reformed only to the extent
+ necessary to make it enforceable. Any law or regulation which provides that
+ the language of a contract shall be construed against the drafter shall not
+ be used to construe this License against a Contributor.
+
+
+10. Versions of the License
+
+10.1. New Versions
+
+ Mozilla Foundation is the license steward. Except as provided in Section
+ 10.3, no one other than the license steward has the right to modify or
+ publish new versions of this License. Each version will be given a
+ distinguishing version number.
+
+10.2. Effect of New Versions
+
+ You may distribute the Covered Software under the terms of the version
+ of the License under which You originally received the Covered Software,
+ or under the terms of any subsequent version published by the license
+ steward.
+
+10.3. Modified Versions
+
+ If you create software not governed by this License, and you want to
+ create a new license for such software, you may create and use a
+ modified version of this License if you rename the license and remove
+ any references to the name of the license steward (except to note that
+ such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+ Licenses If You choose to distribute Source Code Form that is
+ Incompatible With Secondary Licenses under the terms of this version of
+ the License, the notice described in Exhibit B of this License must be
+ attached.
+
+Exhibit A - Source Code Form License Notice
+
+ This Source Code Form is subject to the
+ terms of the Mozilla Public License, v.
+ 2.0. If a copy of the MPL was not
+ distributed with this file, You can
+ obtain one at
+ http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular file,
+then You may include the notice in a location (such as a LICENSE file in a
+relevant directory) where a recipient would be likely to look for such a
+notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+
+ This Source Code Form is "Incompatible
+ With Secondary Licenses", as defined by
+ the Mozilla Public License, v. 2.0.
+
+
+================================================================
+
+github.com/hashicorp/go-msgpack
+https://github.com/hashicorp/go-msgpack
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2012-2015 Ugorji Nwoke.
+All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/hashicorp/go-multierror
+https://github.com/hashicorp/go-multierror
+----------------------------------------------------------------
+Mozilla Public License, version 2.0
+
+1. Definitions
+
+1.1. “Contributor”
+
+ means each individual or legal entity that creates, contributes to the
+ creation of, or owns Covered Software.
+
+1.2. “Contributor Version”
+
+ means the combination of the Contributions of others (if any) used by a
+ Contributor and that particular Contributor’s Contribution.
+
+1.3. “Contribution”
+
+ means Covered Software of a particular Contributor.
+
+1.4. “Covered Software”
+
+ means Source Code Form to which the initial Contributor has attached the
+ notice in Exhibit A, the Executable Form of such Source Code Form, and
+ Modifications of such Source Code Form, in each case including portions
+ thereof.
+
+1.5. “Incompatible With Secondary Licenses”
+ means
+
+ a. that the initial Contributor has attached the notice described in
+ Exhibit B to the Covered Software; or
+
+ b. that the Covered Software was made available under the terms of version
+ 1.1 or earlier of the License, but not also under the terms of a
+ Secondary License.
+
+1.6. “Executable Form”
+
+ means any form of the work other than Source Code Form.
+
+1.7. “Larger Work”
+
+ means a work that combines Covered Software with other material, in a separate
+ file or files, that is not Covered Software.
+
+1.8. “License”
+
+ means this document.
+
+1.9. “Licensable”
+
+ means having the right to grant, to the maximum extent possible, whether at the
+ time of the initial grant or subsequently, any and all of the rights conveyed by
+ this License.
+
+1.10. “Modifications”
+
+ means any of the following:
+
+ a. any file in Source Code Form that results from an addition to, deletion
+ from, or modification of the contents of Covered Software; or
+
+ b. any new file in Source Code Form that contains any Covered Software.
+
+1.11. “Patent Claims” of a Contributor
+
+ means any patent claim(s), including without limitation, method, process,
+ and apparatus claims, in any patent Licensable by such Contributor that
+ would be infringed, but for the grant of the License, by the making,
+ using, selling, offering for sale, having made, import, or transfer of
+ either its Contributions or its Contributor Version.
+
+1.12. “Secondary License”
+
+ means either the GNU General Public License, Version 2.0, the GNU Lesser
+ General Public License, Version 2.1, the GNU Affero General Public
+ License, Version 3.0, or any later versions of those licenses.
+
+1.13. “Source Code Form”
+
+ means the form of the work preferred for making modifications.
+
+1.14. “You” (or “Your”)
+
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, “You” includes any entity that controls, is
+ controlled by, or is under common control with You. For purposes of this
+ definition, “control” means (a) the power, direct or indirect, to cause
+ the direction or management of such entity, whether by contract or
+ otherwise, or (b) ownership of more than fifty percent (50%) of the
+ outstanding shares or beneficial ownership of such entity.
+
+
+2. License Grants and Conditions
+
+2.1. Grants
+
+ Each Contributor hereby grants You a world-wide, royalty-free,
+ non-exclusive license:
+
+ a. under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or as
+ part of a Larger Work; and
+
+ b. under Patent Claims of such Contributor to make, use, sell, offer for
+ sale, have made, import, and otherwise transfer either its Contributions
+ or its Contributor Version.
+
+2.2. Effective Date
+
+ The licenses granted in Section 2.1 with respect to any Contribution become
+ effective for each Contribution on the date the Contributor first distributes
+ such Contribution.
+
+2.3. Limitations on Grant Scope
+
+ The licenses granted in this Section 2 are the only rights granted under this
+ License. No additional rights or licenses will be implied from the distribution
+ or licensing of Covered Software under this License. Notwithstanding Section
+ 2.1(b) above, no patent license is granted by a Contributor:
+
+ a. for any code that a Contributor has removed from Covered Software; or
+
+ b. for infringements caused by: (i) Your and any other third party’s
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+ c. under Patent Claims infringed by Covered Software in the absence of its
+ Contributions.
+
+ This License does not grant any rights in the trademarks, service marks, or
+ logos of any Contributor (except as may be necessary to comply with the
+ notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+ No Contributor makes additional grants as a result of Your choice to
+ distribute the Covered Software under a subsequent version of this License
+ (see Section 10.2) or under the terms of a Secondary License (if permitted
+ under the terms of Section 3.3).
+
+2.5. Representation
+
+ Each Contributor represents that the Contributor believes its Contributions
+ are its original creation(s) or it has sufficient rights to grant the
+ rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+ This License is not intended to limit any rights You have under applicable
+ copyright doctrines of fair use, fair dealing, or other equivalents.
+
+2.7. Conditions
+
+ Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
+ Section 2.1.
+
+
+3. Responsibilities
+
+3.1. Distribution of Source Form
+
+ All distribution of Covered Software in Source Code Form, including any
+ Modifications that You create or to which You contribute, must be under the
+ terms of this License. You must inform recipients that the Source Code Form
+ of the Covered Software is governed by the terms of this License, and how
+ they can obtain a copy of this License. You may not attempt to alter or
+ restrict the recipients’ rights in the Source Code Form.
+
+3.2. Distribution of Executable Form
+
+ If You distribute Covered Software in Executable Form then:
+
+ a. such Covered Software must also be made available in Source Code Form,
+ as described in Section 3.1, and You must inform recipients of the
+ Executable Form how they can obtain a copy of such Source Code Form by
+ reasonable means in a timely manner, at a charge no more than the cost
+ of distribution to the recipient; and
+
+ b. You may distribute such Executable Form under the terms of this License,
+ or sublicense it under different terms, provided that the license for
+ the Executable Form does not attempt to limit or alter the recipients’
+ rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+ You may create and distribute a Larger Work under terms of Your choice,
+ provided that You also comply with the requirements of this License for the
+ Covered Software. If the Larger Work is a combination of Covered Software
+ with a work governed by one or more Secondary Licenses, and the Covered
+ Software is not Incompatible With Secondary Licenses, this License permits
+ You to additionally distribute such Covered Software under the terms of
+ such Secondary License(s), so that the recipient of the Larger Work may, at
+ their option, further distribute the Covered Software under the terms of
+ either this License or such Secondary License(s).
+
+3.4. Notices
+
+ You may not remove or alter the substance of any license notices (including
+ copyright notices, patent notices, disclaimers of warranty, or limitations
+ of liability) contained within the Source Code Form of the Covered
+ Software, except that You may alter any license notices to the extent
+ required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+ You may choose to offer, and to charge a fee for, warranty, support,
+ indemnity or liability obligations to one or more recipients of Covered
+ Software. However, You may do so only on Your own behalf, and not on behalf
+ of any Contributor. You must make it absolutely clear that any such
+ warranty, support, indemnity, or liability obligation is offered by You
+ alone, and You hereby agree to indemnify every Contributor for any
+ liability incurred by such Contributor as a result of warranty, support,
+ indemnity or liability terms You offer. You may include additional
+ disclaimers of warranty and limitations of liability specific to any
+ jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+
+ If it is impossible for You to comply with any of the terms of this License
+ with respect to some or all of the Covered Software due to statute, judicial
+ order, or regulation then You must: (a) comply with the terms of this License
+ to the maximum extent possible; and (b) describe the limitations and the code
+ they affect. Such description must be placed in a text file included with all
+ distributions of the Covered Software under this License. Except to the
+ extent prohibited by statute or regulation, such description must be
+ sufficiently detailed for a recipient of ordinary skill to be able to
+ understand it.
+
+5. Termination
+
+5.1. The rights granted under this License will terminate automatically if You
+ fail to comply with any of its terms. However, if You become compliant,
+ then the rights granted under this License from a particular Contributor
+ are reinstated (a) provisionally, unless and until such Contributor
+ explicitly and finally terminates Your grants, and (b) on an ongoing basis,
+ if such Contributor fails to notify You of the non-compliance by some
+ reasonable means prior to 60 days after You have come back into compliance.
+ Moreover, Your grants from a particular Contributor are reinstated on an
+ ongoing basis if such Contributor notifies You of the non-compliance by
+ some reasonable means, this is the first time You have received notice of
+ non-compliance with this License from such Contributor, and You become
+ compliant prior to 30 days after Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+ infringement claim (excluding declaratory judgment actions, counter-claims,
+ and cross-claims) alleging that a Contributor Version directly or
+ indirectly infringes any patent, then the rights granted to You by any and
+ all Contributors for the Covered Software under Section 2.1 of this License
+ shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
+ license agreements (excluding distributors and resellers) which have been
+ validly granted by You or Your distributors under this License prior to
+ termination shall survive termination.
+
+6. Disclaimer of Warranty
+
+ Covered Software is provided under this License on an “as is” basis, without
+ warranty of any kind, either expressed, implied, or statutory, including,
+ without limitation, warranties that the Covered Software is free of defects,
+ merchantable, fit for a particular purpose or non-infringing. The entire
+ risk as to the quality and performance of the Covered Software is with You.
+ Should any Covered Software prove defective in any respect, You (not any
+ Contributor) assume the cost of any necessary servicing, repair, or
+ correction. This disclaimer of warranty constitutes an essential part of this
+ License. No use of any Covered Software is authorized under this License
+ except under this disclaimer.
+
+7. Limitation of Liability
+
+ Under no circumstances and under no legal theory, whether tort (including
+ negligence), contract, or otherwise, shall any Contributor, or anyone who
+ distributes Covered Software as permitted above, be liable to You for any
+ direct, indirect, special, incidental, or consequential damages of any
+ character including, without limitation, damages for lost profits, loss of
+ goodwill, work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses, even if such party shall have been
+ informed of the possibility of such damages. This limitation of liability
+ shall not apply to liability for death or personal injury resulting from such
+ party’s negligence to the extent applicable law prohibits such limitation.
+ Some jurisdictions do not allow the exclusion or limitation of incidental or
+ consequential damages, so this exclusion and limitation may not apply to You.
+
+8. Litigation
+
+ Any litigation relating to this License may be brought only in the courts of
+ a jurisdiction where the defendant maintains its principal place of business
+ and such litigation shall be governed by laws of that jurisdiction, without
+ reference to its conflict-of-law provisions. Nothing in this Section shall
+ prevent a party’s ability to bring cross-claims or counter-claims.
+
+9. Miscellaneous
+
+ This License represents the complete agreement concerning the subject matter
+ hereof. If any provision of this License is held to be unenforceable, such
+ provision shall be reformed only to the extent necessary to make it
+ enforceable. Any law or regulation which provides that the language of a
+ contract shall be construed against the drafter shall not be used to construe
+ this License against a Contributor.
+
+
+10. Versions of the License
+
+10.1. New Versions
+
+ Mozilla Foundation is the license steward. Except as provided in Section
+ 10.3, no one other than the license steward has the right to modify or
+ publish new versions of this License. Each version will be given a
+ distinguishing version number.
+
+10.2. Effect of New Versions
+
+ You may distribute the Covered Software under the terms of the version of
+ the License under which You originally received the Covered Software, or
+ under the terms of any subsequent version published by the license
+ steward.
+
+10.3. Modified Versions
+
+ If you create software not governed by this License, and you want to
+ create a new license for such software, you may create and use a modified
+ version of this License if you rename the license and remove any
+ references to the name of the license steward (except to note that such
+ modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
+ If You choose to distribute Source Code Form that is Incompatible With
+ Secondary Licenses under the terms of this version of the License, the
+ notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+
+ This Source Code Form is subject to the
+ terms of the Mozilla Public License, v.
+ 2.0. If a copy of the MPL was not
+ distributed with this file, You can
+ obtain one at
+ http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular file, then
+You may include the notice in a location (such as a LICENSE file in a relevant
+directory) where a recipient would be likely to look for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - “Incompatible With Secondary Licenses” Notice
+
+ This Source Code Form is “Incompatible
+ With Secondary Licenses”, as defined by
+ the Mozilla Public License, v. 2.0.
+
+================================================================
+
+github.com/hashicorp/go-uuid
+https://github.com/hashicorp/go-uuid
+----------------------------------------------------------------
+Copyright © 2015-2022 HashiCorp, Inc.
+
+Mozilla Public License, version 2.0
+
+1. Definitions
+
+1.1. "Contributor"
+
+ means each individual or legal entity that creates, contributes to the
+ creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+
+ means the combination of the Contributions of others (if any) used by a
+ Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+
+ means Source Code Form to which the initial Contributor has attached the
+ notice in Exhibit A, the Executable Form of such Source Code Form, and
+ Modifications of such Source Code Form, in each case including portions
+ thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ a. that the initial Contributor has attached the notice described in
+ Exhibit B to the Covered Software; or
+
+ b. that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the terms of
+ a Secondary License.
+
+1.6. "Executable Form"
+
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+
+ means a work that combines Covered Software with other material, in a
+ separate file or files, that is not Covered Software.
+
+1.8. "License"
+
+ means this document.
+
+1.9. "Licensable"
+
+ means having the right to grant, to the maximum extent possible, whether
+ at the time of the initial grant or subsequently, any and all of the
+ rights conveyed by this License.
+
+1.10. "Modifications"
+
+ means any of the following:
+
+ a. any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered Software; or
+
+ b. any new file in Source Code Form that contains any Covered Software.
+
+1.11. "Patent Claims" of a Contributor
+
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the License,
+ by the making, using, selling, offering for sale, having made, import,
+ or transfer of either its Contributions or its Contributor Version.
+
+1.12. "Secondary License"
+
+ means either the GNU General Public License, Version 2.0, the GNU Lesser
+ General Public License, Version 2.1, the GNU Affero General Public
+ License, Version 3.0, or any later versions of those licenses.
+
+1.13. "Source Code Form"
+
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that controls, is
+ controlled by, or is under common control with You. For purposes of this
+ definition, "control" means (a) the power, direct or indirect, to cause
+ the direction or management of such entity, whether by contract or
+ otherwise, or (b) ownership of more than fifty percent (50%) of the
+ outstanding shares or beneficial ownership of such entity.
+
+
+2. License Grants and Conditions
+
+2.1. Grants
+
+ Each Contributor hereby grants You a world-wide, royalty-free,
+ non-exclusive license:
+
+ a. under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+ b. under Patent Claims of such Contributor to make, use, sell, offer for
+ sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+ The licenses granted in Section 2.1 with respect to any Contribution
+ become effective for each Contribution on the date the Contributor first
+ distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+ The licenses granted in this Section 2 are the only rights granted under
+ this License. No additional rights or licenses will be implied from the
+ distribution or licensing of Covered Software under this License.
+ Notwithstanding Section 2.1(b) above, no patent license is granted by a
+ Contributor:
+
+ a. for any code that a Contributor has removed from Covered Software; or
+
+ b. for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+ c. under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+ This License does not grant any rights in the trademarks, service marks,
+ or logos of any Contributor (except as may be necessary to comply with
+ the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+ No Contributor makes additional grants as a result of Your choice to
+ distribute the Covered Software under a subsequent version of this
+ License (see Section 10.2) or under the terms of a Secondary License (if
+ permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+ Each Contributor represents that the Contributor believes its
+ Contributions are its original creation(s) or it has sufficient rights to
+ grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+ This License is not intended to limit any rights You have under
+ applicable copyright doctrines of fair use, fair dealing, or other
+ equivalents.
+
+2.7. Conditions
+
+ Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
+ Section 2.1.
+
+
+3. Responsibilities
+
+3.1. Distribution of Source Form
+
+ All distribution of Covered Software in Source Code Form, including any
+ Modifications that You create or to which You contribute, must be under
+ the terms of this License. You must inform recipients that the Source
+ Code Form of the Covered Software is governed by the terms of this
+ License, and how they can obtain a copy of this License. You may not
+ attempt to alter or restrict the recipients' rights in the Source Code
+ Form.
+
+3.2. Distribution of Executable Form
+
+ If You distribute Covered Software in Executable Form then:
+
+ a. such Covered Software must also be made available in Source Code Form,
+ as described in Section 3.1, and You must inform recipients of the
+ Executable Form how they can obtain a copy of such Source Code Form by
+ reasonable means in a timely manner, at a charge no more than the cost
+ of distribution to the recipient; and
+
+ b. You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter the
+ recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+ You may create and distribute a Larger Work under terms of Your choice,
+ provided that You also comply with the requirements of this License for
+ the Covered Software. If the Larger Work is a combination of Covered
+ Software with a work governed by one or more Secondary Licenses, and the
+ Covered Software is not Incompatible With Secondary Licenses, this
+ License permits You to additionally distribute such Covered Software
+ under the terms of such Secondary License(s), so that the recipient of
+ the Larger Work may, at their option, further distribute the Covered
+ Software under the terms of either this License or such Secondary
+ License(s).
+
+3.4. Notices
+
+ You may not remove or alter the substance of any license notices
+ (including copyright notices, patent notices, disclaimers of warranty, or
+ limitations of liability) contained within the Source Code Form of the
+ Covered Software, except that You may alter any license notices to the
+ extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+ You may choose to offer, and to charge a fee for, warranty, support,
+ indemnity or liability obligations to one or more recipients of Covered
+ Software. However, You may do so only on Your own behalf, and not on
+ behalf of any Contributor. You must make it absolutely clear that any
+ such warranty, support, indemnity, or liability obligation is offered by
+ You alone, and You hereby agree to indemnify every Contributor for any
+ liability incurred by such Contributor as a result of warranty, support,
+ indemnity or liability terms You offer. You may include additional
+ disclaimers of warranty and limitations of liability specific to any
+ jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+
+ If it is impossible for You to comply with any of the terms of this License
+ with respect to some or all of the Covered Software due to statute,
+ judicial order, or regulation then You must: (a) comply with the terms of
+ this License to the maximum extent possible; and (b) describe the
+ limitations and the code they affect. Such description must be placed in a
+ text file included with all distributions of the Covered Software under
+ this License. Except to the extent prohibited by statute or regulation,
+ such description must be sufficiently detailed for a recipient of ordinary
+ skill to be able to understand it.
+
+5. Termination
+
+5.1. The rights granted under this License will terminate automatically if You
+ fail to comply with any of its terms. However, if You become compliant,
+ then the rights granted under this License from a particular Contributor
+ are reinstated (a) provisionally, unless and until such Contributor
+ explicitly and finally terminates Your grants, and (b) on an ongoing
+ basis, if such Contributor fails to notify You of the non-compliance by
+ some reasonable means prior to 60 days after You have come back into
+ compliance. Moreover, Your grants from a particular Contributor are
+ reinstated on an ongoing basis if such Contributor notifies You of the
+ non-compliance by some reasonable means, this is the first time You have
+ received notice of non-compliance with this License from such
+ Contributor, and You become compliant prior to 30 days after Your receipt
+ of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+ infringement claim (excluding declaratory judgment actions,
+ counter-claims, and cross-claims) alleging that a Contributor Version
+ directly or indirectly infringes any patent, then the rights granted to
+ You by any and all Contributors for the Covered Software under Section
+ 2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
+ license agreements (excluding distributors and resellers) which have been
+ validly granted by You or Your distributors under this License prior to
+ termination shall survive termination.
+
+6. Disclaimer of Warranty
+
+ Covered Software is provided under this License on an "as is" basis,
+ without warranty of any kind, either expressed, implied, or statutory,
+ including, without limitation, warranties that the Covered Software is free
+ of defects, merchantable, fit for a particular purpose or non-infringing.
+ The entire risk as to the quality and performance of the Covered Software
+ is with You. Should any Covered Software prove defective in any respect,
+ You (not any Contributor) assume the cost of any necessary servicing,
+ repair, or correction. This disclaimer of warranty constitutes an essential
+ part of this License. No use of any Covered Software is authorized under
+ this License except under this disclaimer.
+
+7. Limitation of Liability
+
+ Under no circumstances and under no legal theory, whether tort (including
+ negligence), contract, or otherwise, shall any Contributor, or anyone who
+ distributes Covered Software as permitted above, be liable to You for any
+ direct, indirect, special, incidental, or consequential damages of any
+ character including, without limitation, damages for lost profits, loss of
+ goodwill, work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses, even if such party shall have been
+ informed of the possibility of such damages. This limitation of liability
+ shall not apply to liability for death or personal injury resulting from
+ such party's negligence to the extent applicable law prohibits such
+ limitation. Some jurisdictions do not allow the exclusion or limitation of
+ incidental or consequential damages, so this exclusion and limitation may
+ not apply to You.
+
+8. Litigation
+
+ Any litigation relating to this License may be brought only in the courts
+ of a jurisdiction where the defendant maintains its principal place of
+ business and such litigation shall be governed by laws of that
+ jurisdiction, without reference to its conflict-of-law provisions. Nothing
+ in this Section shall prevent a party's ability to bring cross-claims or
+ counter-claims.
+
+9. Miscellaneous
+
+ This License represents the complete agreement concerning the subject
+ matter hereof. If any provision of this License is held to be
+ unenforceable, such provision shall be reformed only to the extent
+ necessary to make it enforceable. Any law or regulation which provides that
+ the language of a contract shall be construed against the drafter shall not
+ be used to construe this License against a Contributor.
+
+
+10. Versions of the License
+
+10.1. New Versions
+
+ Mozilla Foundation is the license steward. Except as provided in Section
+ 10.3, no one other than the license steward has the right to modify or
+ publish new versions of this License. Each version will be given a
+ distinguishing version number.
+
+10.2. Effect of New Versions
+
+ You may distribute the Covered Software under the terms of the version
+ of the License under which You originally received the Covered Software,
+ or under the terms of any subsequent version published by the license
+ steward.
+
+10.3. Modified Versions
+
+ If you create software not governed by this License, and you want to
+ create a new license for such software, you may create and use a
+ modified version of this License if you rename the license and remove
+ any references to the name of the license steward (except to note that
+ such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+ Licenses If You choose to distribute Source Code Form that is
+ Incompatible With Secondary Licenses under the terms of this version of
+ the License, the notice described in Exhibit B of this License must be
+ attached.
+
+Exhibit A - Source Code Form License Notice
+
+ This Source Code Form is subject to the
+ terms of the Mozilla Public License, v.
+ 2.0. If a copy of the MPL was not
+ distributed with this file, You can
+ obtain one at
+ http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular file,
+then You may include the notice in a location (such as a LICENSE file in a
+relevant directory) where a recipient would be likely to look for such a
+notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+
+ This Source Code Form is "Incompatible
+ With Secondary Licenses", as defined by
+ the Mozilla Public License, v. 2.0.
+
+
+================================================================
+
+github.com/hashicorp/golang-lru
+https://github.com/hashicorp/golang-lru
+----------------------------------------------------------------
+Copyright (c) 2014 HashiCorp, Inc.
+
+Mozilla Public License, version 2.0
+
+1. Definitions
+
+1.1. "Contributor"
+
+ means each individual or legal entity that creates, contributes to the
+ creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+
+ means the combination of the Contributions of others (if any) used by a
+ Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+
+ means Source Code Form to which the initial Contributor has attached the
+ notice in Exhibit A, the Executable Form of such Source Code Form, and
+ Modifications of such Source Code Form, in each case including portions
+ thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ a. that the initial Contributor has attached the notice described in
+ Exhibit B to the Covered Software; or
+
+ b. that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the terms of
+ a Secondary License.
+
+1.6. "Executable Form"
+
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+
+ means a work that combines Covered Software with other material, in a
+ separate file or files, that is not Covered Software.
+
+1.8. "License"
+
+ means this document.
+
+1.9. "Licensable"
+
+ means having the right to grant, to the maximum extent possible, whether
+ at the time of the initial grant or subsequently, any and all of the
+ rights conveyed by this License.
+
+1.10. "Modifications"
+
+ means any of the following:
+
+ a. any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered Software; or
+
+ b. any new file in Source Code Form that contains any Covered Software.
+
+1.11. "Patent Claims" of a Contributor
+
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the License,
+ by the making, using, selling, offering for sale, having made, import,
+ or transfer of either its Contributions or its Contributor Version.
+
+1.12. "Secondary License"
+
+ means either the GNU General Public License, Version 2.0, the GNU Lesser
+ General Public License, Version 2.1, the GNU Affero General Public
+ License, Version 3.0, or any later versions of those licenses.
+
+1.13. "Source Code Form"
+
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that controls, is
+ controlled by, or is under common control with You. For purposes of this
+ definition, "control" means (a) the power, direct or indirect, to cause
+ the direction or management of such entity, whether by contract or
+ otherwise, or (b) ownership of more than fifty percent (50%) of the
+ outstanding shares or beneficial ownership of such entity.
+
+
+2. License Grants and Conditions
+
+2.1. Grants
+
+ Each Contributor hereby grants You a world-wide, royalty-free,
+ non-exclusive license:
+
+ a. under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+ b. under Patent Claims of such Contributor to make, use, sell, offer for
+ sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+ The licenses granted in Section 2.1 with respect to any Contribution
+ become effective for each Contribution on the date the Contributor first
+ distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+ The licenses granted in this Section 2 are the only rights granted under
+ this License. No additional rights or licenses will be implied from the
+ distribution or licensing of Covered Software under this License.
+ Notwithstanding Section 2.1(b) above, no patent license is granted by a
+ Contributor:
+
+ a. for any code that a Contributor has removed from Covered Software; or
+
+ b. for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+ c. under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+ This License does not grant any rights in the trademarks, service marks,
+ or logos of any Contributor (except as may be necessary to comply with
+ the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+ No Contributor makes additional grants as a result of Your choice to
+ distribute the Covered Software under a subsequent version of this
+ License (see Section 10.2) or under the terms of a Secondary License (if
+ permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+ Each Contributor represents that the Contributor believes its
+ Contributions are its original creation(s) or it has sufficient rights to
+ grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+ This License is not intended to limit any rights You have under
+ applicable copyright doctrines of fair use, fair dealing, or other
+ equivalents.
+
+2.7. Conditions
+
+ Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
+ Section 2.1.
+
+
+3. Responsibilities
+
+3.1. Distribution of Source Form
+
+ All distribution of Covered Software in Source Code Form, including any
+ Modifications that You create or to which You contribute, must be under
+ the terms of this License. You must inform recipients that the Source
+ Code Form of the Covered Software is governed by the terms of this
+ License, and how they can obtain a copy of this License. You may not
+ attempt to alter or restrict the recipients' rights in the Source Code
+ Form.
+
+3.2. Distribution of Executable Form
+
+ If You distribute Covered Software in Executable Form then:
+
+ a. such Covered Software must also be made available in Source Code Form,
+ as described in Section 3.1, and You must inform recipients of the
+ Executable Form how they can obtain a copy of such Source Code Form by
+ reasonable means in a timely manner, at a charge no more than the cost
+ of distribution to the recipient; and
+
+ b. You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter the
+ recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+ You may create and distribute a Larger Work under terms of Your choice,
+ provided that You also comply with the requirements of this License for
+ the Covered Software. If the Larger Work is a combination of Covered
+ Software with a work governed by one or more Secondary Licenses, and the
+ Covered Software is not Incompatible With Secondary Licenses, this
+ License permits You to additionally distribute such Covered Software
+ under the terms of such Secondary License(s), so that the recipient of
+ the Larger Work may, at their option, further distribute the Covered
+ Software under the terms of either this License or such Secondary
+ License(s).
+
+3.4. Notices
+
+ You may not remove or alter the substance of any license notices
+ (including copyright notices, patent notices, disclaimers of warranty, or
+ limitations of liability) contained within the Source Code Form of the
+ Covered Software, except that You may alter any license notices to the
+ extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+ You may choose to offer, and to charge a fee for, warranty, support,
+ indemnity or liability obligations to one or more recipients of Covered
+ Software. However, You may do so only on Your own behalf, and not on
+ behalf of any Contributor. You must make it absolutely clear that any
+ such warranty, support, indemnity, or liability obligation is offered by
+ You alone, and You hereby agree to indemnify every Contributor for any
+ liability incurred by such Contributor as a result of warranty, support,
+ indemnity or liability terms You offer. You may include additional
+ disclaimers of warranty and limitations of liability specific to any
+ jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+
+ If it is impossible for You to comply with any of the terms of this License
+ with respect to some or all of the Covered Software due to statute,
+ judicial order, or regulation then You must: (a) comply with the terms of
+ this License to the maximum extent possible; and (b) describe the
+ limitations and the code they affect. Such description must be placed in a
+ text file included with all distributions of the Covered Software under
+ this License. Except to the extent prohibited by statute or regulation,
+ such description must be sufficiently detailed for a recipient of ordinary
+ skill to be able to understand it.
+
+5. Termination
+
+5.1. The rights granted under this License will terminate automatically if You
+ fail to comply with any of its terms. However, if You become compliant,
+ then the rights granted under this License from a particular Contributor
+ are reinstated (a) provisionally, unless and until such Contributor
+ explicitly and finally terminates Your grants, and (b) on an ongoing
+ basis, if such Contributor fails to notify You of the non-compliance by
+ some reasonable means prior to 60 days after You have come back into
+ compliance. Moreover, Your grants from a particular Contributor are
+ reinstated on an ongoing basis if such Contributor notifies You of the
+ non-compliance by some reasonable means, this is the first time You have
+ received notice of non-compliance with this License from such
+ Contributor, and You become compliant prior to 30 days after Your receipt
+ of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+ infringement claim (excluding declaratory judgment actions,
+ counter-claims, and cross-claims) alleging that a Contributor Version
+ directly or indirectly infringes any patent, then the rights granted to
+ You by any and all Contributors for the Covered Software under Section
+ 2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
+ license agreements (excluding distributors and resellers) which have been
+ validly granted by You or Your distributors under this License prior to
+ termination shall survive termination.
+
+6. Disclaimer of Warranty
+
+ Covered Software is provided under this License on an "as is" basis,
+ without warranty of any kind, either expressed, implied, or statutory,
+ including, without limitation, warranties that the Covered Software is free
+ of defects, merchantable, fit for a particular purpose or non-infringing.
+ The entire risk as to the quality and performance of the Covered Software
+ is with You. Should any Covered Software prove defective in any respect,
+ You (not any Contributor) assume the cost of any necessary servicing,
+ repair, or correction. This disclaimer of warranty constitutes an essential
+ part of this License. No use of any Covered Software is authorized under
+ this License except under this disclaimer.
+
+7. Limitation of Liability
+
+ Under no circumstances and under no legal theory, whether tort (including
+ negligence), contract, or otherwise, shall any Contributor, or anyone who
+ distributes Covered Software as permitted above, be liable to You for any
+ direct, indirect, special, incidental, or consequential damages of any
+ character including, without limitation, damages for lost profits, loss of
+ goodwill, work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses, even if such party shall have been
+ informed of the possibility of such damages. This limitation of liability
+ shall not apply to liability for death or personal injury resulting from
+ such party's negligence to the extent applicable law prohibits such
+ limitation. Some jurisdictions do not allow the exclusion or limitation of
+ incidental or consequential damages, so this exclusion and limitation may
+ not apply to You.
+
+8. Litigation
+
+ Any litigation relating to this License may be brought only in the courts
+ of a jurisdiction where the defendant maintains its principal place of
+ business and such litigation shall be governed by laws of that
+ jurisdiction, without reference to its conflict-of-law provisions. Nothing
+ in this Section shall prevent a party's ability to bring cross-claims or
+ counter-claims.
+
+9. Miscellaneous
+
+ This License represents the complete agreement concerning the subject
+ matter hereof. If any provision of this License is held to be
+ unenforceable, such provision shall be reformed only to the extent
+ necessary to make it enforceable. Any law or regulation which provides that
+ the language of a contract shall be construed against the drafter shall not
+ be used to construe this License against a Contributor.
+
+
+10. Versions of the License
+
+10.1. New Versions
+
+ Mozilla Foundation is the license steward. Except as provided in Section
+ 10.3, no one other than the license steward has the right to modify or
+ publish new versions of this License. Each version will be given a
+ distinguishing version number.
+
+10.2. Effect of New Versions
+
+ You may distribute the Covered Software under the terms of the version
+ of the License under which You originally received the Covered Software,
+ or under the terms of any subsequent version published by the license
+ steward.
+
+10.3. Modified Versions
+
+ If you create software not governed by this License, and you want to
+ create a new license for such software, you may create and use a
+ modified version of this License if you rename the license and remove
+ any references to the name of the license steward (except to note that
+ such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+ Licenses If You choose to distribute Source Code Form that is
+ Incompatible With Secondary Licenses under the terms of this version of
+ the License, the notice described in Exhibit B of this License must be
+ attached.
+
+Exhibit A - Source Code Form License Notice
+
+ This Source Code Form is subject to the
+ terms of the Mozilla Public License, v.
+ 2.0. If a copy of the MPL was not
+ distributed with this file, You can
+ obtain one at
+ http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular file,
+then You may include the notice in a location (such as a LICENSE file in a
+relevant directory) where a recipient would be likely to look for such a
+notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+
+ This Source Code Form is "Incompatible
+ With Secondary Licenses", as defined by
+ the Mozilla Public License, v. 2.0.
+
+================================================================
+
+github.com/hashicorp/raft
+https://github.com/hashicorp/raft
+----------------------------------------------------------------
+Mozilla Public License, version 2.0
+
+1. Definitions
+
+1.1. “Contributor”
+
+ means each individual or legal entity that creates, contributes to the
+ creation of, or owns Covered Software.
+
+1.2. “Contributor Version”
+
+ means the combination of the Contributions of others (if any) used by a
+ Contributor and that particular Contributor’s Contribution.
+
+1.3. “Contribution”
+
+ means Covered Software of a particular Contributor.
+
+1.4. “Covered Software”
+
+ means Source Code Form to which the initial Contributor has attached the
+ notice in Exhibit A, the Executable Form of such Source Code Form, and
+ Modifications of such Source Code Form, in each case including portions
+ thereof.
+
+1.5. “Incompatible With Secondary Licenses”
+ means
+
+ a. that the initial Contributor has attached the notice described in
+ Exhibit B to the Covered Software; or
+
+ b. that the Covered Software was made available under the terms of version
+ 1.1 or earlier of the License, but not also under the terms of a
+ Secondary License.
+
+1.6. “Executable Form”
+
+ means any form of the work other than Source Code Form.
+
+1.7. “Larger Work”
+
+ means a work that combines Covered Software with other material, in a separate
+ file or files, that is not Covered Software.
+
+1.8. “License”
+
+ means this document.
+
+1.9. “Licensable”
+
+ means having the right to grant, to the maximum extent possible, whether at the
+ time of the initial grant or subsequently, any and all of the rights conveyed by
+ this License.
+
+1.10. “Modifications”
+
+ means any of the following:
+
+ a. any file in Source Code Form that results from an addition to, deletion
+ from, or modification of the contents of Covered Software; or
+
+ b. any new file in Source Code Form that contains any Covered Software.
+
+1.11. “Patent Claims” of a Contributor
+
+ means any patent claim(s), including without limitation, method, process,
+ and apparatus claims, in any patent Licensable by such Contributor that
+ would be infringed, but for the grant of the License, by the making,
+ using, selling, offering for sale, having made, import, or transfer of
+ either its Contributions or its Contributor Version.
+
+1.12. “Secondary License”
+
+ means either the GNU General Public License, Version 2.0, the GNU Lesser
+ General Public License, Version 2.1, the GNU Affero General Public
+ License, Version 3.0, or any later versions of those licenses.
+
+1.13. “Source Code Form”
+
+ means the form of the work preferred for making modifications.
+
+1.14. “You” (or “Your”)
+
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, “You” includes any entity that controls, is
+ controlled by, or is under common control with You. For purposes of this
+ definition, “control” means (a) the power, direct or indirect, to cause
+ the direction or management of such entity, whether by contract or
+ otherwise, or (b) ownership of more than fifty percent (50%) of the
+ outstanding shares or beneficial ownership of such entity.
+
+
+2. License Grants and Conditions
+
+2.1. Grants
+
+ Each Contributor hereby grants You a world-wide, royalty-free,
+ non-exclusive license:
+
+ a. under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or as
+ part of a Larger Work; and
+
+ b. under Patent Claims of such Contributor to make, use, sell, offer for
+ sale, have made, import, and otherwise transfer either its Contributions
+ or its Contributor Version.
+
+2.2. Effective Date
+
+ The licenses granted in Section 2.1 with respect to any Contribution become
+ effective for each Contribution on the date the Contributor first distributes
+ such Contribution.
+
+2.3. Limitations on Grant Scope
+
+ The licenses granted in this Section 2 are the only rights granted under this
+ License. No additional rights or licenses will be implied from the distribution
+ or licensing of Covered Software under this License. Notwithstanding Section
+ 2.1(b) above, no patent license is granted by a Contributor:
+
+ a. for any code that a Contributor has removed from Covered Software; or
+
+ b. for infringements caused by: (i) Your and any other third party’s
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+ c. under Patent Claims infringed by Covered Software in the absence of its
+ Contributions.
+
+ This License does not grant any rights in the trademarks, service marks, or
+ logos of any Contributor (except as may be necessary to comply with the
+ notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+ No Contributor makes additional grants as a result of Your choice to
+ distribute the Covered Software under a subsequent version of this License
+ (see Section 10.2) or under the terms of a Secondary License (if permitted
+ under the terms of Section 3.3).
+
+2.5. Representation
+
+ Each Contributor represents that the Contributor believes its Contributions
+ are its original creation(s) or it has sufficient rights to grant the
+ rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+ This License is not intended to limit any rights You have under applicable
+ copyright doctrines of fair use, fair dealing, or other equivalents.
+
+2.7. Conditions
+
+ Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
+ Section 2.1.
+
+
+3. Responsibilities
+
+3.1. Distribution of Source Form
+
+ All distribution of Covered Software in Source Code Form, including any
+ Modifications that You create or to which You contribute, must be under the
+ terms of this License. You must inform recipients that the Source Code Form
+ of the Covered Software is governed by the terms of this License, and how
+ they can obtain a copy of this License. You may not attempt to alter or
+ restrict the recipients’ rights in the Source Code Form.
+
+3.2. Distribution of Executable Form
+
+ If You distribute Covered Software in Executable Form then:
+
+ a. such Covered Software must also be made available in Source Code Form,
+ as described in Section 3.1, and You must inform recipients of the
+ Executable Form how they can obtain a copy of such Source Code Form by
+ reasonable means in a timely manner, at a charge no more than the cost
+ of distribution to the recipient; and
+
+ b. You may distribute such Executable Form under the terms of this License,
+ or sublicense it under different terms, provided that the license for
+ the Executable Form does not attempt to limit or alter the recipients’
+ rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+ You may create and distribute a Larger Work under terms of Your choice,
+ provided that You also comply with the requirements of this License for the
+ Covered Software. If the Larger Work is a combination of Covered Software
+ with a work governed by one or more Secondary Licenses, and the Covered
+ Software is not Incompatible With Secondary Licenses, this License permits
+ You to additionally distribute such Covered Software under the terms of
+ such Secondary License(s), so that the recipient of the Larger Work may, at
+ their option, further distribute the Covered Software under the terms of
+ either this License or such Secondary License(s).
+
+3.4. Notices
+
+ You may not remove or alter the substance of any license notices (including
+ copyright notices, patent notices, disclaimers of warranty, or limitations
+ of liability) contained within the Source Code Form of the Covered
+ Software, except that You may alter any license notices to the extent
+ required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+ You may choose to offer, and to charge a fee for, warranty, support,
+ indemnity or liability obligations to one or more recipients of Covered
+ Software. However, You may do so only on Your own behalf, and not on behalf
+ of any Contributor. You must make it absolutely clear that any such
+ warranty, support, indemnity, or liability obligation is offered by You
+ alone, and You hereby agree to indemnify every Contributor for any
+ liability incurred by such Contributor as a result of warranty, support,
+ indemnity or liability terms You offer. You may include additional
+ disclaimers of warranty and limitations of liability specific to any
+ jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+
+ If it is impossible for You to comply with any of the terms of this License
+ with respect to some or all of the Covered Software due to statute, judicial
+ order, or regulation then You must: (a) comply with the terms of this License
+ to the maximum extent possible; and (b) describe the limitations and the code
+ they affect. Such description must be placed in a text file included with all
+ distributions of the Covered Software under this License. Except to the
+ extent prohibited by statute or regulation, such description must be
+ sufficiently detailed for a recipient of ordinary skill to be able to
+ understand it.
+
+5. Termination
+
+5.1. The rights granted under this License will terminate automatically if You
+ fail to comply with any of its terms. However, if You become compliant,
+ then the rights granted under this License from a particular Contributor
+ are reinstated (a) provisionally, unless and until such Contributor
+ explicitly and finally terminates Your grants, and (b) on an ongoing basis,
+ if such Contributor fails to notify You of the non-compliance by some
+ reasonable means prior to 60 days after You have come back into compliance.
+ Moreover, Your grants from a particular Contributor are reinstated on an
+ ongoing basis if such Contributor notifies You of the non-compliance by
+ some reasonable means, this is the first time You have received notice of
+ non-compliance with this License from such Contributor, and You become
+ compliant prior to 30 days after Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+ infringement claim (excluding declaratory judgment actions, counter-claims,
+ and cross-claims) alleging that a Contributor Version directly or
+ indirectly infringes any patent, then the rights granted to You by any and
+ all Contributors for the Covered Software under Section 2.1 of this License
+ shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
+ license agreements (excluding distributors and resellers) which have been
+ validly granted by You or Your distributors under this License prior to
+ termination shall survive termination.
+
+6. Disclaimer of Warranty
+
+ Covered Software is provided under this License on an “as is” basis, without
+ warranty of any kind, either expressed, implied, or statutory, including,
+ without limitation, warranties that the Covered Software is free of defects,
+ merchantable, fit for a particular purpose or non-infringing. The entire
+ risk as to the quality and performance of the Covered Software is with You.
+ Should any Covered Software prove defective in any respect, You (not any
+ Contributor) assume the cost of any necessary servicing, repair, or
+ correction. This disclaimer of warranty constitutes an essential part of this
+ License. No use of any Covered Software is authorized under this License
+ except under this disclaimer.
+
+7. Limitation of Liability
+
+ Under no circumstances and under no legal theory, whether tort (including
+ negligence), contract, or otherwise, shall any Contributor, or anyone who
+ distributes Covered Software as permitted above, be liable to You for any
+ direct, indirect, special, incidental, or consequential damages of any
+ character including, without limitation, damages for lost profits, loss of
+ goodwill, work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses, even if such party shall have been
+ informed of the possibility of such damages. This limitation of liability
+ shall not apply to liability for death or personal injury resulting from such
+ party’s negligence to the extent applicable law prohibits such limitation.
+ Some jurisdictions do not allow the exclusion or limitation of incidental or
+ consequential damages, so this exclusion and limitation may not apply to You.
+
+8. Litigation
+
+ Any litigation relating to this License may be brought only in the courts of
+ a jurisdiction where the defendant maintains its principal place of business
+ and such litigation shall be governed by laws of that jurisdiction, without
+ reference to its conflict-of-law provisions. Nothing in this Section shall
+ prevent a party’s ability to bring cross-claims or counter-claims.
+
+9. Miscellaneous
+
+ This License represents the complete agreement concerning the subject matter
+ hereof. If any provision of this License is held to be unenforceable, such
+ provision shall be reformed only to the extent necessary to make it
+ enforceable. Any law or regulation which provides that the language of a
+ contract shall be construed against the drafter shall not be used to construe
+ this License against a Contributor.
+
+
+10. Versions of the License
+
+10.1. New Versions
+
+ Mozilla Foundation is the license steward. Except as provided in Section
+ 10.3, no one other than the license steward has the right to modify or
+ publish new versions of this License. Each version will be given a
+ distinguishing version number.
+
+10.2. Effect of New Versions
+
+ You may distribute the Covered Software under the terms of the version of
+ the License under which You originally received the Covered Software, or
+ under the terms of any subsequent version published by the license
+ steward.
+
+10.3. Modified Versions
+
+ If you create software not governed by this License, and you want to
+ create a new license for such software, you may create and use a modified
+ version of this License if you rename the license and remove any
+ references to the name of the license steward (except to note that such
+ modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
+ If You choose to distribute Source Code Form that is Incompatible With
+ Secondary Licenses under the terms of this version of the License, the
+ notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+
+ This Source Code Form is subject to the
+ terms of the Mozilla Public License, v.
+ 2.0. If a copy of the MPL was not
+ distributed with this file, You can
+ obtain one at
+ http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular file, then
+You may include the notice in a location (such as a LICENSE file in a relevant
+directory) where a recipient would be likely to look for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - “Incompatible With Secondary Licenses” Notice
+
+ This Source Code Form is “Incompatible
+ With Secondary Licenses”, as defined by
+ the Mozilla Public License, v. 2.0.
+
+
+================================================================
+
+github.com/inconshreveable/mousetrap
+https://github.com/inconshreveable/mousetrap
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2022 Alan Shreve (@inconshreveable)
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/jcmturner/aescts/v2
+https://github.com/jcmturner/aescts/v2
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/jcmturner/dnsutils/v2
+https://github.com/jcmturner/dnsutils/v2
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/jcmturner/gofork
+https://github.com/jcmturner/gofork
+----------------------------------------------------------------
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/jcmturner/goidentity/v6
+https://github.com/jcmturner/goidentity/v6
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/jcmturner/gokrb5/v8
+https://github.com/jcmturner/gokrb5/v8
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/jcmturner/rpc/v2
+https://github.com/jcmturner/rpc/v2
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/jedib0t/go-pretty/v6
+https://github.com/jedib0t/go-pretty/v6
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2018 jedib0t
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/jessevdk/go-flags
+https://github.com/jessevdk/go-flags
+----------------------------------------------------------------
+Copyright (c) 2012 Jesse van den Kieboom. All rights reserved.
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following disclaimer
+ in the documentation and/or other materials provided with the
+ distribution.
+ * Neither the name of Google Inc. nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/josharian/intern
+https://github.com/josharian/intern
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2019 Josh Bleecher Snyder
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/json-iterator/go
+https://github.com/json-iterator/go
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2016 json-iterator
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/juju/ratelimit
+https://github.com/juju/ratelimit
+----------------------------------------------------------------
+All files in this repository are licensed as follows. If you contribute
+to this repository, it is assumed that you license your contribution
+under the same license unless you state otherwise.
+
+All files Copyright (C) 2015 Canonical Ltd. unless otherwise specified in the file.
+
+This software is licensed under the LGPLv3, included below.
+
+As a special exception to the GNU Lesser General Public License version 3
+("LGPL3"), the copyright holders of this Library give you permission to
+convey to a third party a Combined Work that links statically or dynamically
+to this Library without providing any Minimal Corresponding Source or
+Minimal Application Code as set out in 4d or providing the installation
+information set out in section 4e, provided that you comply with the other
+provisions of LGPL3 and provided that you meet, for the Application the
+terms and conditions of the license(s) which apply to the Application.
+
+Except as stated in this special exception, the provisions of LGPL3 will
+continue to comply in full to this Library. If you modify this Library, you
+may apply this exception to your version of this Library, but you are not
+obliged to do so. If you do not wish to do so, delete this exception
+statement from your version. This exception does not (and cannot) modify any
+license terms which apply to the Application, with which you must still
+comply.
+
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
+
+================================================================
+
+github.com/keybase/go-keychain
+https://github.com/keybase/go-keychain
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2015 Keybase
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+================================================================
+
+github.com/klauspost/compress
+https://github.com/klauspost/compress
+----------------------------------------------------------------
+Copyright (c) 2012 The Go Authors. All rights reserved.
+Copyright (c) 2019 Klaus Post. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+------------------
+
+Files: gzhttp/*
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2016-2017 The New York Times Company
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+------------------
+
+Files: s2/cmd/internal/readahead/*
+
+The MIT License (MIT)
+
+Copyright (c) 2015 Klaus Post
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+---------------------
+Files: snappy/*
+Files: internal/snapref/*
+
+Copyright (c) 2011 The Snappy-Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+-----------------
+
+Files: s2/cmd/internal/filepathx/*
+
+Copyright 2016 The filepathx Authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+================================================================
+
+github.com/klauspost/cpuid/v2
+https://github.com/klauspost/cpuid/v2
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2015 Klaus Post
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+================================================================
+
+github.com/klauspost/filepathx
+https://github.com/klauspost/filepathx
+----------------------------------------------------------------
+Copyright 2016 The filepathx Authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+================================================================
+
+github.com/klauspost/pgzip
+https://github.com/klauspost/pgzip
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2014 Klaus Post
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+================================================================
+
+github.com/klauspost/readahead
+https://github.com/klauspost/readahead
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2015 Klaus Post
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+================================================================
+
+github.com/klauspost/reedsolomon
+https://github.com/klauspost/reedsolomon
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2015 Klaus Post
+Copyright (c) 2015 Backblaze
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+================================================================
+
+github.com/kr/fs
+https://github.com/kr/fs
+----------------------------------------------------------------
+Copyright (c) 2012 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/kr/pretty
+https://github.com/kr/pretty
+----------------------------------------------------------------
+Copyright 2012 Keith Rarick
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+================================================================
+
+github.com/kr/text
+https://github.com/kr/text
+----------------------------------------------------------------
+Copyright 2012 Keith Rarick
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+================================================================
+
+github.com/kylelemons/godebug
+https://github.com/kylelemons/godebug
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/lestrrat-go/blackmagic
+https://github.com/lestrrat-go/blackmagic
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2021 lestrrat-go
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/lestrrat-go/httpcc
+https://github.com/lestrrat-go/httpcc
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2020 lestrrat-go
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/lestrrat-go/httprc
+https://github.com/lestrrat-go/httprc
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2022 lestrrat
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/lestrrat-go/iter
+https://github.com/lestrrat-go/iter
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2020 lestrrat-go
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/lestrrat-go/jwx/v2
+https://github.com/lestrrat-go/jwx/v2
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2015 lestrrat
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+================================================================
+
+github.com/lestrrat-go/option
+https://github.com/lestrrat-go/option
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2021 lestrrat-go
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/lib/pq
+https://github.com/lib/pq
+----------------------------------------------------------------
+Copyright (c) 2011-2013, 'pq' Contributors
+Portions Copyright (C) 2011 Blake Mizerany
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+================================================================
+
+github.com/lithammer/shortuuid/v4
+https://github.com/lithammer/shortuuid/v4
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2018 Peter Lithammer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/lucasb-eyer/go-colorful
+https://github.com/lucasb-eyer/go-colorful
+----------------------------------------------------------------
+Copyright (c) 2013 Lucas Beyer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+================================================================
+
+github.com/lufia/plan9stats
+https://github.com/lufia/plan9stats
+----------------------------------------------------------------
+BSD 3-Clause License
+
+Copyright (c) 2019, KADOTA, Kyohei
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/mailru/easyjson
+https://github.com/mailru/easyjson
+----------------------------------------------------------------
+Copyright (c) 2016 Mail.Ru Group
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+================================================================
+
+github.com/mattn/go-colorable
+https://github.com/mattn/go-colorable
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2016 Yasuhiro Matsumoto
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/mattn/go-ieproxy
+https://github.com/mattn/go-ieproxy
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2014 mattn
+Copyright (c) 2017 oliverpool
+Copyright (c) 2019 Adele Reed
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/mattn/go-isatty
+https://github.com/mattn/go-isatty
+----------------------------------------------------------------
+Copyright (c) Yasuhiro MATSUMOTO
+
+MIT License (Expat)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+================================================================
+
+github.com/mattn/go-runewidth
+https://github.com/mattn/go-runewidth
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2016 Yasuhiro Matsumoto
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/matttproud/golang_protobuf_extensions
+https://github.com/matttproud/golang_protobuf_extensions
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/miekg/dns
+https://github.com/miekg/dns
+----------------------------------------------------------------
+BSD 3-Clause License
+
+Copyright (c) 2009, The Go Authors. Extensions copyright (c) 2011, Miek Gieben.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/minio/cli
+https://github.com/minio/cli
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2016 Jeremy Saenz & Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/minio/console
+https://github.com/minio/console
+----------------------------------------------------------------
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
+
+================================================================
+
+github.com/minio/crc64nvme
+https://github.com/minio/crc64nvme
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/minio/dnscache
+https://github.com/minio/dnscache
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2023 MinIO, Inc.
+Copyright (c) 2018 Olivier Poitrey
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/minio/dperf
+https://github.com/minio/dperf
+----------------------------------------------------------------
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
+
+================================================================
+
+github.com/minio/highwayhash
+https://github.com/minio/highwayhash
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/minio/kms-go/kes
+https://github.com/minio/kms-go/kes
+----------------------------------------------------------------
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
+
+================================================================
+
+github.com/minio/kms-go/kms
+https://github.com/minio/kms-go/kms
+----------------------------------------------------------------
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
+
+================================================================
+
+github.com/minio/madmin-go/v3
+https://github.com/minio/madmin-go/v3
+----------------------------------------------------------------
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
+
+================================================================
+
+github.com/minio/mc
+https://github.com/minio/mc
+----------------------------------------------------------------
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
+
+================================================================
+
+github.com/minio/md5-simd
+https://github.com/minio/md5-simd
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/minio/minio-go/v7
+https://github.com/minio/minio-go/v7
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/minio/mux
+https://github.com/minio/mux
+----------------------------------------------------------------
+Copyright (c) 2023 The Gorilla Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/minio/pkg/v3
+https://github.com/minio/pkg/v3
+----------------------------------------------------------------
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
+
+================================================================
+
+github.com/minio/selfupdate
+https://github.com/minio/selfupdate
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/minio/simdjson-go
+https://github.com/minio/simdjson-go
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/minio/sio
+https://github.com/minio/sio
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/minio/websocket
+https://github.com/minio/websocket
+----------------------------------------------------------------
+Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/minio/xxml
+https://github.com/minio/xxml
+----------------------------------------------------------------
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/minio/zipindex
+https://github.com/minio/zipindex
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/mitchellh/go-homedir
+https://github.com/mitchellh/go-homedir
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2013 Mitchell Hashimoto
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+================================================================
+
+github.com/mitchellh/mapstructure
+https://github.com/mitchellh/mapstructure
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2013 Mitchell Hashimoto
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+================================================================
+
+github.com/modern-go/concurrent
+https://github.com/modern-go/concurrent
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/modern-go/reflect2
+https://github.com/modern-go/reflect2
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/muesli/ansi
+https://github.com/muesli/ansi
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2021 Christian Muehlhaeuser
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/muesli/cancelreader
+https://github.com/muesli/cancelreader
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2022 Erik Geiser and Christian Muehlhaeuser
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/muesli/reflow
+https://github.com/muesli/reflow
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2019 Christian Muehlhaeuser
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/muesli/termenv
+https://github.com/muesli/termenv
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2019 Christian Muehlhaeuser
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/munnerz/goautoneg
+https://github.com/munnerz/goautoneg
+----------------------------------------------------------------
+Copyright (c) 2011, Open Knowledge Foundation Ltd.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ Neither the name of the Open Knowledge Foundation Ltd. nor the
+ names of its contributors may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/nats-io/jwt/v2
+https://github.com/nats-io/jwt/v2
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/nats-io/nats-server/v2
+https://github.com/nats-io/nats-server/v2
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/nats-io/nats-streaming-server
+https://github.com/nats-io/nats-streaming-server
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+================================================================
+
+github.com/nats-io/nats.go
+https://github.com/nats-io/nats.go
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/nats-io/nkeys
+https://github.com/nats-io/nkeys
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/nats-io/nuid
+https://github.com/nats-io/nuid
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/nats-io/stan.go
+https://github.com/nats-io/stan.go
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+================================================================
+
+github.com/ncw/directio
+https://github.com/ncw/directio
+----------------------------------------------------------------
+Copyright (C) 2012 by Nick Craig-Wood http://www.craig-wood.com/nick/
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+================================================================
+
+github.com/nsqio/go-nsq
+https://github.com/nsqio/go-nsq
+----------------------------------------------------------------
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+================================================================
+
+github.com/oklog/ulid
+https://github.com/oklog/ulid
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/olekukonko/tablewriter
+https://github.com/olekukonko/tablewriter
+----------------------------------------------------------------
+Copyright (C) 2014 by Oleku Konko
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+================================================================
+
+github.com/philhofer/fwd
+https://github.com/philhofer/fwd
+----------------------------------------------------------------
+Copyright (c) 2014-2015, Philip Hofer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+================================================================
+
+github.com/pierrec/lz4/v4
+https://github.com/pierrec/lz4/v4
+----------------------------------------------------------------
+Copyright (c) 2015, Pierre Curto
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of xxHash nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+================================================================
+
+github.com/pkg/browser
+https://github.com/pkg/browser
+----------------------------------------------------------------
+Copyright (c) 2014, Dave Cheney
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/pkg/errors
+https://github.com/pkg/errors
+----------------------------------------------------------------
+Copyright (c) 2015, Dave Cheney
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/pkg/sftp
+https://github.com/pkg/sftp
+----------------------------------------------------------------
+Copyright (c) 2013, Dave Cheney
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/pkg/xattr
+https://github.com/pkg/xattr
+----------------------------------------------------------------
+Copyright (c) 2012 Dave Cheney. All rights reserved.
+Copyright (c) 2014 Kuba Podgórski. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/planetscale/vtprotobuf
+https://github.com/planetscale/vtprotobuf
+----------------------------------------------------------------
+Copyright (c) 2021, PlanetScale Inc. All rights reserved.
+Copyright (c) 2013, The GoGo Authors. All rights reserved.
+Copyright (c) 2018 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/pmezard/go-difflib
+https://github.com/pmezard/go-difflib
+----------------------------------------------------------------
+Copyright (c) 2013, Patrick Mezard
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+ The names of its contributors may not be used to endorse or promote
+products derived from this software without specific prior written
+permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/posener/complete
+https://github.com/posener/complete
+----------------------------------------------------------------
+The MIT License
+
+Copyright (c) 2017 Eyal Posener
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+================================================================
+
+github.com/power-devops/perfstat
+https://github.com/power-devops/perfstat
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2020 Power DevOps
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+
+================================================================
+
+github.com/prometheus/client_golang
+https://github.com/prometheus/client_golang
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/prometheus/client_model
+https://github.com/prometheus/client_model
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/prometheus/common
+https://github.com/prometheus/common
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/prometheus/procfs
+https://github.com/prometheus/procfs
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/prometheus/prom2json
+https://github.com/prometheus/prom2json
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/prometheus/prometheus
+https://github.com/prometheus/prometheus
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/puzpuzpuz/xsync/v3
+https://github.com/puzpuzpuz/xsync/v3
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/rabbitmq/amqp091-go
+https://github.com/rabbitmq/amqp091-go
+----------------------------------------------------------------
+AMQP 0-9-1 Go Client
+Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved.
+
+Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/rcrowley/go-metrics
+https://github.com/rcrowley/go-metrics
+----------------------------------------------------------------
+Copyright 2012 Richard Crowley. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY RICHARD CROWLEY ``AS IS'' AND ANY EXPRESS
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL RICHARD CROWLEY OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+THE POSSIBILITY OF SUCH DAMAGE.
+
+The views and conclusions contained in the software and documentation
+are those of the authors and should not be interpreted as representing
+official policies, either expressed or implied, of Richard Crowley.
+
+================================================================
+
+github.com/redis/go-redis/v9
+https://github.com/redis/go-redis/v9
+----------------------------------------------------------------
+Copyright (c) 2013 The github.com/redis/go-redis Authors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/rivo/uniseg
+https://github.com/rivo/uniseg
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2019 Oliver Kuederle
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/rjeczalik/notify
+https://github.com/rjeczalik/notify
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2014-2015 The Notify Authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/rogpeppe/go-internal
+https://github.com/rogpeppe/go-internal
+----------------------------------------------------------------
+Copyright (c) 2018 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/rs/cors
+https://github.com/rs/cors
+----------------------------------------------------------------
+Copyright (c) 2014 Olivier Poitrey
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+================================================================
+
+github.com/rs/xid
+https://github.com/rs/xid
+----------------------------------------------------------------
+Copyright (c) 2015 Olivier Poitrey
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+================================================================
+
+github.com/safchain/ethtool
+https://github.com/safchain/ethtool
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright (c) 2015 The Ethtool Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+================================================================
+
+github.com/secure-io/sio-go
+https://github.com/secure-io/sio-go
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2019 SecureIO
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/segmentio/asm
+https://github.com/segmentio/asm
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2021 Segment
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/shirou/gopsutil/v3
+https://github.com/shirou/gopsutil/v3
+----------------------------------------------------------------
+gopsutil is distributed under BSD license reproduced below.
+
+Copyright (c) 2014, WAKAYAMA Shirou
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name of the gopsutil authors nor the names of its contributors
+ may be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+-------
+internal/common/binary.go in the gopsutil is copied and modified from golang/encoding/binary.go.
+
+
+
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+================================================================
+
+github.com/shoenig/go-m1cpu
+https://github.com/shoenig/go-m1cpu
+----------------------------------------------------------------
+Mozilla Public License, version 2.0
+
+1. Definitions
+
+1.1. "Contributor"
+
+ means each individual or legal entity that creates, contributes to the
+ creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+
+ means the combination of the Contributions of others (if any) used by a
+ Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+
+ means Source Code Form to which the initial Contributor has attached the
+ notice in Exhibit A, the Executable Form of such Source Code Form, and
+ Modifications of such Source Code Form, in each case including portions
+ thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ a. that the initial Contributor has attached the notice described in
+ Exhibit B to the Covered Software; or
+
+ b. that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the terms of
+ a Secondary License.
+
+1.6. "Executable Form"
+
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+
+ means a work that combines Covered Software with other material, in a
+ separate file or files, that is not Covered Software.
+
+1.8. "License"
+
+ means this document.
+
+1.9. "Licensable"
+
+ means having the right to grant, to the maximum extent possible, whether
+ at the time of the initial grant or subsequently, any and all of the
+ rights conveyed by this License.
+
+1.10. "Modifications"
+
+ means any of the following:
+
+ a. any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered Software; or
+
+ b. any new file in Source Code Form that contains any Covered Software.
+
+1.11. "Patent Claims" of a Contributor
+
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the License,
+ by the making, using, selling, offering for sale, having made, import,
+ or transfer of either its Contributions or its Contributor Version.
+
+1.12. "Secondary License"
+
+ means either the GNU General Public License, Version 2.0, the GNU Lesser
+ General Public License, Version 2.1, the GNU Affero General Public
+ License, Version 3.0, or any later versions of those licenses.
+
+1.13. "Source Code Form"
+
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that controls, is
+ controlled by, or is under common control with You. For purposes of this
+ definition, "control" means (a) the power, direct or indirect, to cause
+ the direction or management of such entity, whether by contract or
+ otherwise, or (b) ownership of more than fifty percent (50%) of the
+ outstanding shares or beneficial ownership of such entity.
+
+
+2. License Grants and Conditions
+
+2.1. Grants
+
+ Each Contributor hereby grants You a world-wide, royalty-free,
+ non-exclusive license:
+
+ a. under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+ b. under Patent Claims of such Contributor to make, use, sell, offer for
+ sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+ The licenses granted in Section 2.1 with respect to any Contribution
+ become effective for each Contribution on the date the Contributor first
+ distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+ The licenses granted in this Section 2 are the only rights granted under
+ this License. No additional rights or licenses will be implied from the
+ distribution or licensing of Covered Software under this License.
+ Notwithstanding Section 2.1(b) above, no patent license is granted by a
+ Contributor:
+
+ a. for any code that a Contributor has removed from Covered Software; or
+
+ b. for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+ c. under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+ This License does not grant any rights in the trademarks, service marks,
+ or logos of any Contributor (except as may be necessary to comply with
+ the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+ No Contributor makes additional grants as a result of Your choice to
+ distribute the Covered Software under a subsequent version of this
+ License (see Section 10.2) or under the terms of a Secondary License (if
+ permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+ Each Contributor represents that the Contributor believes its
+ Contributions are its original creation(s) or it has sufficient rights to
+ grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+ This License is not intended to limit any rights You have under
+ applicable copyright doctrines of fair use, fair dealing, or other
+ equivalents.
+
+2.7. Conditions
+
+ Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
+ Section 2.1.
+
+
+3. Responsibilities
+
+3.1. Distribution of Source Form
+
+ All distribution of Covered Software in Source Code Form, including any
+ Modifications that You create or to which You contribute, must be under
+ the terms of this License. You must inform recipients that the Source
+ Code Form of the Covered Software is governed by the terms of this
+ License, and how they can obtain a copy of this License. You may not
+ attempt to alter or restrict the recipients' rights in the Source Code
+ Form.
+
+3.2. Distribution of Executable Form
+
+ If You distribute Covered Software in Executable Form then:
+
+ a. such Covered Software must also be made available in Source Code Form,
+ as described in Section 3.1, and You must inform recipients of the
+ Executable Form how they can obtain a copy of such Source Code Form by
+ reasonable means in a timely manner, at a charge no more than the cost
+ of distribution to the recipient; and
+
+ b. You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter the
+ recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+ You may create and distribute a Larger Work under terms of Your choice,
+ provided that You also comply with the requirements of this License for
+ the Covered Software. If the Larger Work is a combination of Covered
+ Software with a work governed by one or more Secondary Licenses, and the
+ Covered Software is not Incompatible With Secondary Licenses, this
+ License permits You to additionally distribute such Covered Software
+ under the terms of such Secondary License(s), so that the recipient of
+ the Larger Work may, at their option, further distribute the Covered
+ Software under the terms of either this License or such Secondary
+ License(s).
+
+3.4. Notices
+
+ You may not remove or alter the substance of any license notices
+ (including copyright notices, patent notices, disclaimers of warranty, or
+ limitations of liability) contained within the Source Code Form of the
+ Covered Software, except that You may alter any license notices to the
+ extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+ You may choose to offer, and to charge a fee for, warranty, support,
+ indemnity or liability obligations to one or more recipients of Covered
+ Software. However, You may do so only on Your own behalf, and not on
+ behalf of any Contributor. You must make it absolutely clear that any
+ such warranty, support, indemnity, or liability obligation is offered by
+ You alone, and You hereby agree to indemnify every Contributor for any
+ liability incurred by such Contributor as a result of warranty, support,
+ indemnity or liability terms You offer. You may include additional
+ disclaimers of warranty and limitations of liability specific to any
+ jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+
+ If it is impossible for You to comply with any of the terms of this License
+ with respect to some or all of the Covered Software due to statute,
+ judicial order, or regulation then You must: (a) comply with the terms of
+ this License to the maximum extent possible; and (b) describe the
+ limitations and the code they affect. Such description must be placed in a
+ text file included with all distributions of the Covered Software under
+ this License. Except to the extent prohibited by statute or regulation,
+ such description must be sufficiently detailed for a recipient of ordinary
+ skill to be able to understand it.
+
+5. Termination
+
+5.1. The rights granted under this License will terminate automatically if You
+ fail to comply with any of its terms. However, if You become compliant,
+ then the rights granted under this License from a particular Contributor
+ are reinstated (a) provisionally, unless and until such Contributor
+ explicitly and finally terminates Your grants, and (b) on an ongoing
+ basis, if such Contributor fails to notify You of the non-compliance by
+ some reasonable means prior to 60 days after You have come back into
+ compliance. Moreover, Your grants from a particular Contributor are
+ reinstated on an ongoing basis if such Contributor notifies You of the
+ non-compliance by some reasonable means, this is the first time You have
+ received notice of non-compliance with this License from such
+ Contributor, and You become compliant prior to 30 days after Your receipt
+ of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+ infringement claim (excluding declaratory judgment actions,
+ counter-claims, and cross-claims) alleging that a Contributor Version
+ directly or indirectly infringes any patent, then the rights granted to
+ You by any and all Contributors for the Covered Software under Section
+ 2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
+ license agreements (excluding distributors and resellers) which have been
+ validly granted by You or Your distributors under this License prior to
+ termination shall survive termination.
+
+6. Disclaimer of Warranty
+
+ Covered Software is provided under this License on an "as is" basis,
+ without warranty of any kind, either expressed, implied, or statutory,
+ including, without limitation, warranties that the Covered Software is free
+ of defects, merchantable, fit for a particular purpose or non-infringing.
+ The entire risk as to the quality and performance of the Covered Software
+ is with You. Should any Covered Software prove defective in any respect,
+ You (not any Contributor) assume the cost of any necessary servicing,
+ repair, or correction. This disclaimer of warranty constitutes an essential
+ part of this License. No use of any Covered Software is authorized under
+ this License except under this disclaimer.
+
+7. Limitation of Liability
+
+ Under no circumstances and under no legal theory, whether tort (including
+ negligence), contract, or otherwise, shall any Contributor, or anyone who
+ distributes Covered Software as permitted above, be liable to You for any
+ direct, indirect, special, incidental, or consequential damages of any
+ character including, without limitation, damages for lost profits, loss of
+ goodwill, work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses, even if such party shall have been
+ informed of the possibility of such damages. This limitation of liability
+ shall not apply to liability for death or personal injury resulting from
+ such party's negligence to the extent applicable law prohibits such
+ limitation. Some jurisdictions do not allow the exclusion or limitation of
+ incidental or consequential damages, so this exclusion and limitation may
+ not apply to You.
+
+8. Litigation
+
+ Any litigation relating to this License may be brought only in the courts
+ of a jurisdiction where the defendant maintains its principal place of
+ business and such litigation shall be governed by laws of that
+ jurisdiction, without reference to its conflict-of-law provisions. Nothing
+ in this Section shall prevent a party's ability to bring cross-claims or
+ counter-claims.
+
+9. Miscellaneous
+
+ This License represents the complete agreement concerning the subject
+ matter hereof. If any provision of this License is held to be
+ unenforceable, such provision shall be reformed only to the extent
+ necessary to make it enforceable. Any law or regulation which provides that
+ the language of a contract shall be construed against the drafter shall not
+ be used to construe this License against a Contributor.
+
+
+10. Versions of the License
+
+10.1. New Versions
+
+ Mozilla Foundation is the license steward. Except as provided in Section
+ 10.3, no one other than the license steward has the right to modify or
+ publish new versions of this License. Each version will be given a
+ distinguishing version number.
+
+10.2. Effect of New Versions
+
+ You may distribute the Covered Software under the terms of the version
+ of the License under which You originally received the Covered Software,
+ or under the terms of any subsequent version published by the license
+ steward.
+
+10.3. Modified Versions
+
+ If you create software not governed by this License, and you want to
+ create a new license for such software, you may create and use a
+ modified version of this License if you rename the license and remove
+ any references to the name of the license steward (except to note that
+ such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+ Licenses If You choose to distribute Source Code Form that is
+ Incompatible With Secondary Licenses under the terms of this version of
+ the License, the notice described in Exhibit B of this License must be
+ attached.
+
+Exhibit A - Source Code Form License Notice
+
+ This Source Code Form is subject to the
+ terms of the Mozilla Public License, v.
+ 2.0. If a copy of the MPL was not
+ distributed with this file, You can
+ obtain one at
+ http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular file,
+then You may include the notice in a location (such as a LICENSE file in a
+relevant directory) where a recipient would be likely to look for such a
+notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+
+ This Source Code Form is "Incompatible
+ With Secondary Licenses", as defined by
+ the Mozilla Public License, v. 2.0.
+
+
+================================================================
+
+github.com/shoenig/test
+https://github.com/shoenig/test
+----------------------------------------------------------------
+Mozilla Public License, version 2.0
+
+1. Definitions
+
+1.1. "Contributor"
+
+ means each individual or legal entity that creates, contributes to the
+ creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+
+ means the combination of the Contributions of others (if any) used by a
+ Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+
+ means Source Code Form to which the initial Contributor has attached the
+ notice in Exhibit A, the Executable Form of such Source Code Form, and
+ Modifications of such Source Code Form, in each case including portions
+ thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ a. that the initial Contributor has attached the notice described in
+ Exhibit B to the Covered Software; or
+
+ b. that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the terms of
+ a Secondary License.
+
+1.6. "Executable Form"
+
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+
+ means a work that combines Covered Software with other material, in a
+ separate file or files, that is not Covered Software.
+
+1.8. "License"
+
+ means this document.
+
+1.9. "Licensable"
+
+ means having the right to grant, to the maximum extent possible, whether
+ at the time of the initial grant or subsequently, any and all of the
+ rights conveyed by this License.
+
+1.10. "Modifications"
+
+ means any of the following:
+
+ a. any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered Software; or
+
+ b. any new file in Source Code Form that contains any Covered Software.
+
+1.11. "Patent Claims" of a Contributor
+
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the License,
+ by the making, using, selling, offering for sale, having made, import,
+ or transfer of either its Contributions or its Contributor Version.
+
+1.12. "Secondary License"
+
+ means either the GNU General Public License, Version 2.0, the GNU Lesser
+ General Public License, Version 2.1, the GNU Affero General Public
+ License, Version 3.0, or any later versions of those licenses.
+
+1.13. "Source Code Form"
+
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that controls, is
+ controlled by, or is under common control with You. For purposes of this
+ definition, "control" means (a) the power, direct or indirect, to cause
+ the direction or management of such entity, whether by contract or
+ otherwise, or (b) ownership of more than fifty percent (50%) of the
+ outstanding shares or beneficial ownership of such entity.
+
+
+2. License Grants and Conditions
+
+2.1. Grants
+
+ Each Contributor hereby grants You a world-wide, royalty-free,
+ non-exclusive license:
+
+ a. under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+ b. under Patent Claims of such Contributor to make, use, sell, offer for
+ sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+ The licenses granted in Section 2.1 with respect to any Contribution
+ become effective for each Contribution on the date the Contributor first
+ distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+ The licenses granted in this Section 2 are the only rights granted under
+ this License. No additional rights or licenses will be implied from the
+ distribution or licensing of Covered Software under this License.
+ Notwithstanding Section 2.1(b) above, no patent license is granted by a
+ Contributor:
+
+ a. for any code that a Contributor has removed from Covered Software; or
+
+ b. for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+ c. under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+ This License does not grant any rights in the trademarks, service marks,
+ or logos of any Contributor (except as may be necessary to comply with
+ the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+ No Contributor makes additional grants as a result of Your choice to
+ distribute the Covered Software under a subsequent version of this
+ License (see Section 10.2) or under the terms of a Secondary License (if
+ permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+ Each Contributor represents that the Contributor believes its
+ Contributions are its original creation(s) or it has sufficient rights to
+ grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+ This License is not intended to limit any rights You have under
+ applicable copyright doctrines of fair use, fair dealing, or other
+ equivalents.
+
+2.7. Conditions
+
+ Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
+ Section 2.1.
+
+
+3. Responsibilities
+
+3.1. Distribution of Source Form
+
+ All distribution of Covered Software in Source Code Form, including any
+ Modifications that You create or to which You contribute, must be under
+ the terms of this License. You must inform recipients that the Source
+ Code Form of the Covered Software is governed by the terms of this
+ License, and how they can obtain a copy of this License. You may not
+ attempt to alter or restrict the recipients' rights in the Source Code
+ Form.
+
+3.2. Distribution of Executable Form
+
+ If You distribute Covered Software in Executable Form then:
+
+ a. such Covered Software must also be made available in Source Code Form,
+ as described in Section 3.1, and You must inform recipients of the
+ Executable Form how they can obtain a copy of such Source Code Form by
+ reasonable means in a timely manner, at a charge no more than the cost
+ of distribution to the recipient; and
+
+ b. You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter the
+ recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+ You may create and distribute a Larger Work under terms of Your choice,
+ provided that You also comply with the requirements of this License for
+ the Covered Software. If the Larger Work is a combination of Covered
+ Software with a work governed by one or more Secondary Licenses, and the
+ Covered Software is not Incompatible With Secondary Licenses, this
+ License permits You to additionally distribute such Covered Software
+ under the terms of such Secondary License(s), so that the recipient of
+ the Larger Work may, at their option, further distribute the Covered
+ Software under the terms of either this License or such Secondary
+ License(s).
+
+3.4. Notices
+
+ You may not remove or alter the substance of any license notices
+ (including copyright notices, patent notices, disclaimers of warranty, or
+ limitations of liability) contained within the Source Code Form of the
+ Covered Software, except that You may alter any license notices to the
+ extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+ You may choose to offer, and to charge a fee for, warranty, support,
+ indemnity or liability obligations to one or more recipients of Covered
+ Software. However, You may do so only on Your own behalf, and not on
+ behalf of any Contributor. You must make it absolutely clear that any
+ such warranty, support, indemnity, or liability obligation is offered by
+ You alone, and You hereby agree to indemnify every Contributor for any
+ liability incurred by such Contributor as a result of warranty, support,
+ indemnity or liability terms You offer. You may include additional
+ disclaimers of warranty and limitations of liability specific to any
+ jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+
+ If it is impossible for You to comply with any of the terms of this License
+ with respect to some or all of the Covered Software due to statute,
+ judicial order, or regulation then You must: (a) comply with the terms of
+ this License to the maximum extent possible; and (b) describe the
+ limitations and the code they affect. Such description must be placed in a
+ text file included with all distributions of the Covered Software under
+ this License. Except to the extent prohibited by statute or regulation,
+ such description must be sufficiently detailed for a recipient of ordinary
+ skill to be able to understand it.
+
+5. Termination
+
+5.1. The rights granted under this License will terminate automatically if You
+ fail to comply with any of its terms. However, if You become compliant,
+ then the rights granted under this License from a particular Contributor
+ are reinstated (a) provisionally, unless and until such Contributor
+ explicitly and finally terminates Your grants, and (b) on an ongoing
+ basis, if such Contributor fails to notify You of the non-compliance by
+ some reasonable means prior to 60 days after You have come back into
+ compliance. Moreover, Your grants from a particular Contributor are
+ reinstated on an ongoing basis if such Contributor notifies You of the
+ non-compliance by some reasonable means, this is the first time You have
+ received notice of non-compliance with this License from such
+ Contributor, and You become compliant prior to 30 days after Your receipt
+ of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+ infringement claim (excluding declaratory judgment actions,
+ counter-claims, and cross-claims) alleging that a Contributor Version
+ directly or indirectly infringes any patent, then the rights granted to
+ You by any and all Contributors for the Covered Software under Section
+ 2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
+ license agreements (excluding distributors and resellers) which have been
+ validly granted by You or Your distributors under this License prior to
+ termination shall survive termination.
+
+6. Disclaimer of Warranty
+
+ Covered Software is provided under this License on an "as is" basis,
+ without warranty of any kind, either expressed, implied, or statutory,
+ including, without limitation, warranties that the Covered Software is free
+ of defects, merchantable, fit for a particular purpose or non-infringing.
+ The entire risk as to the quality and performance of the Covered Software
+ is with You. Should any Covered Software prove defective in any respect,
+ You (not any Contributor) assume the cost of any necessary servicing,
+ repair, or correction. This disclaimer of warranty constitutes an essential
+ part of this License. No use of any Covered Software is authorized under
+ this License except under this disclaimer.
+
+7. Limitation of Liability
+
+ Under no circumstances and under no legal theory, whether tort (including
+ negligence), contract, or otherwise, shall any Contributor, or anyone who
+ distributes Covered Software as permitted above, be liable to You for any
+ direct, indirect, special, incidental, or consequential damages of any
+ character including, without limitation, damages for lost profits, loss of
+ goodwill, work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses, even if such party shall have been
+ informed of the possibility of such damages. This limitation of liability
+ shall not apply to liability for death or personal injury resulting from
+ such party's negligence to the extent applicable law prohibits such
+ limitation. Some jurisdictions do not allow the exclusion or limitation of
+ incidental or consequential damages, so this exclusion and limitation may
+ not apply to You.
+
+8. Litigation
+
+ Any litigation relating to this License may be brought only in the courts
+ of a jurisdiction where the defendant maintains its principal place of
+ business and such litigation shall be governed by laws of that
+ jurisdiction, without reference to its conflict-of-law provisions. Nothing
+ in this Section shall prevent a party's ability to bring cross-claims or
+ counter-claims.
+
+9. Miscellaneous
+
+ This License represents the complete agreement concerning the subject
+ matter hereof. If any provision of this License is held to be
+ unenforceable, such provision shall be reformed only to the extent
+ necessary to make it enforceable. Any law or regulation which provides that
+ the language of a contract shall be construed against the drafter shall not
+ be used to construe this License against a Contributor.
+
+
+10. Versions of the License
+
+10.1. New Versions
+
+ Mozilla Foundation is the license steward. Except as provided in Section
+ 10.3, no one other than the license steward has the right to modify or
+ publish new versions of this License. Each version will be given a
+ distinguishing version number.
+
+10.2. Effect of New Versions
+
+ You may distribute the Covered Software under the terms of the version
+ of the License under which You originally received the Covered Software,
+ or under the terms of any subsequent version published by the license
+ steward.
+
+10.3. Modified Versions
+
+ If you create software not governed by this License, and you want to
+ create a new license for such software, you may create and use a
+ modified version of this License if you rename the license and remove
+ any references to the name of the license steward (except to note that
+ such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+ Licenses If You choose to distribute Source Code Form that is
+ Incompatible With Secondary Licenses under the terms of this version of
+ the License, the notice described in Exhibit B of this License must be
+ attached.
+
+Exhibit A - Source Code Form License Notice
+
+ This Source Code Form is subject to the
+ terms of the Mozilla Public License, v.
+ 2.0. If a copy of the MPL was not
+ distributed with this file, You can
+ obtain one at
+ http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular file,
+then You may include the notice in a location (such as a LICENSE file in a
+relevant directory) where a recipient would be likely to look for such a
+notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+
+ This Source Code Form is "Incompatible
+ With Secondary Licenses", as defined by
+ the Mozilla Public License, v. 2.0.
+
+
+================================================================
+
+github.com/stretchr/testify
+https://github.com/stretchr/testify
+----------------------------------------------------------------
+MIT License
+
+Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+================================================================
+
+github.com/tidwall/gjson
+https://github.com/tidwall/gjson
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2016 Josh Baker
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+================================================================
+
+github.com/tidwall/match
+https://github.com/tidwall/match
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2016 Josh Baker
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+================================================================
+
+github.com/tidwall/pretty
+https://github.com/tidwall/pretty
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2017 Josh Baker
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+================================================================
+
+github.com/tinylib/msgp
+https://github.com/tinylib/msgp
+----------------------------------------------------------------
+Copyright (c) 2014 Philip Hofer
+Portions Copyright (c) 2009 The Go Authors (license at http://golang.org) where indicated
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+================================================================
+
+github.com/tklauser/go-sysconf
+https://github.com/tklauser/go-sysconf
+----------------------------------------------------------------
+BSD 3-Clause License
+
+Copyright (c) 2018-2022, Tobias Klauser
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+github.com/tklauser/numcpus
+https://github.com/tklauser/numcpus
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+github.com/unrolled/secure
+https://github.com/unrolled/secure
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2014 Cory Jacobsen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+================================================================
+
+github.com/valyala/bytebufferpool
+https://github.com/valyala/bytebufferpool
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2016 Aliaksandr Valialkin, VertaMedia
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+================================================================
+
+github.com/vbauerster/mpb/v8
+https://github.com/vbauerster/mpb/v8
+----------------------------------------------------------------
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to
+
+================================================================
+
+github.com/xdg/scram
+https://github.com/xdg/scram
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+================================================================
+
+github.com/xdg/stringprep
+https://github.com/xdg/stringprep
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+================================================================
+
+github.com/yusufpapurcu/wmi
+https://github.com/yusufpapurcu/wmi
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2013 Stack Exchange
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+================================================================
+
+github.com/zeebo/assert
+https://github.com/zeebo/assert
+----------------------------------------------------------------
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+ HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+ i. the right to reproduce, adapt, distribute, perform, display,
+ communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+ likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+ subject to the limitations in paragraph 4(a), below;
+ v. rights protecting the extraction, dissemination, use and reuse of data
+ in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+ European Parliament and of the Council of 11 March 1996 on the legal
+ protection of databases, and under any national implementation
+ thereof, including any amended or successor version of such
+ directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+ world based on applicable law or treaty, and any national
+ implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+ surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+ warranties of any kind concerning the Work, express, implied,
+ statutory or otherwise, including without limitation warranties of
+ title, merchantability, fitness for a particular purpose, non
+ infringement, or the absence of latent or other defects, accuracy, or
+ the present or absence of errors, whether or not discoverable, all to
+ the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+ that may apply to the Work or any use thereof, including without
+ limitation any person's Copyright and Related Rights in the Work.
+ Further, Affirmer disclaims responsibility for obtaining any necessary
+ consents, permissions or other rights required for any use of the
+ Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+ party to this document and has no duty or obligation with respect to
+ this CC0 or use of the Work.
+
+================================================================
+
+github.com/zeebo/xxh3
+https://github.com/zeebo/xxh3
+----------------------------------------------------------------
+xxHash Library
+Copyright (c) 2012-2014, Yann Collet
+Copyright (c) 2019, Jeff Wendling
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice, this
+ list of conditions and the following disclaimer in the documentation and/or
+ other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+go.etcd.io/bbolt
+https://go.etcd.io/bbolt
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2013 Ben Johnson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+================================================================
+
+go.etcd.io/etcd/api/v3
+https://go.etcd.io/etcd/api/v3
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+go.etcd.io/etcd/client/pkg/v3
+https://go.etcd.io/etcd/client/pkg/v3
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+go.etcd.io/etcd/client/v3
+https://go.etcd.io/etcd/client/v3
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+go.mongodb.org/mongo-driver
+https://go.mongodb.org/mongo-driver
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+go.opencensus.io
+https://go.opencensus.io
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+================================================================
+
+go.opentelemetry.io/auto/sdk
+https://go.opentelemetry.io/auto/sdk
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+go.opentelemetry.io/contrib/detectors/gcp
+https://go.opentelemetry.io/contrib/detectors/gcp
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
+https://go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
+https://go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+go.opentelemetry.io/otel
+https://go.opentelemetry.io/otel
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+go.opentelemetry.io/otel/metric
+https://go.opentelemetry.io/otel/metric
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+go.opentelemetry.io/otel/sdk
+https://go.opentelemetry.io/otel/sdk
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+go.opentelemetry.io/otel/sdk/metric
+https://go.opentelemetry.io/otel/sdk/metric
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+go.opentelemetry.io/otel/trace
+https://go.opentelemetry.io/otel/trace
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+go.uber.org/atomic
+https://go.uber.org/atomic
+----------------------------------------------------------------
+Copyright (c) 2016 Uber Technologies, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+================================================================
+
+go.uber.org/goleak
+https://go.uber.org/goleak
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2018 Uber Technologies, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+================================================================
+
+go.uber.org/multierr
+https://go.uber.org/multierr
+----------------------------------------------------------------
+Copyright (c) 2017-2021 Uber Technologies, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+================================================================
+
+go.uber.org/zap
+https://go.uber.org/zap
+----------------------------------------------------------------
+Copyright (c) 2016-2017 Uber Technologies, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+================================================================
+
+goftp.io/server/v2
+https://goftp.io/server/v2
+----------------------------------------------------------------
+Copyright (c) 2018 Goftp Authors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+================================================================
+
+golang.org/x/crypto
+https://golang.org/x/crypto
+----------------------------------------------------------------
+Copyright 2009 The Go Authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google LLC nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+golang.org/x/mod
+https://golang.org/x/mod
+----------------------------------------------------------------
+Copyright 2009 The Go Authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google LLC nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+golang.org/x/net
+https://golang.org/x/net
+----------------------------------------------------------------
+Copyright 2009 The Go Authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google LLC nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+golang.org/x/oauth2
+https://golang.org/x/oauth2
+----------------------------------------------------------------
+Copyright 2009 The Go Authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google LLC nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+golang.org/x/sync
+https://golang.org/x/sync
+----------------------------------------------------------------
+Copyright 2009 The Go Authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google LLC nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+golang.org/x/sys
+https://golang.org/x/sys
+----------------------------------------------------------------
+Copyright 2009 The Go Authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google LLC nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+golang.org/x/term
+https://golang.org/x/term
+----------------------------------------------------------------
+Copyright 2009 The Go Authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google LLC nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+golang.org/x/text
+https://golang.org/x/text
+----------------------------------------------------------------
+Copyright 2009 The Go Authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google LLC nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+golang.org/x/time
+https://golang.org/x/time
+----------------------------------------------------------------
+Copyright 2009 The Go Authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google LLC nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+golang.org/x/tools
+https://golang.org/x/tools
+----------------------------------------------------------------
+Copyright 2009 The Go Authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google LLC nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+google.golang.org/api
+https://google.golang.org/api
+----------------------------------------------------------------
+Copyright (c) 2011 Google Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+google.golang.org/genproto
+https://google.golang.org/genproto
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+google.golang.org/genproto/googleapis/api
+https://google.golang.org/genproto/googleapis/api
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+google.golang.org/genproto/googleapis/rpc
+https://google.golang.org/genproto/googleapis/rpc
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+google.golang.org/grpc
+https://google.golang.org/grpc
+----------------------------------------------------------------
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+google.golang.org/protobuf
+https://google.golang.org/protobuf
+----------------------------------------------------------------
+Copyright (c) 2018 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+gopkg.in/check.v1
+https://gopkg.in/check.v1
+----------------------------------------------------------------
+Gocheck - A rich testing framework for Go
+
+Copyright (c) 2010-2013 Gustavo Niemeyer
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+================================================================
+
+gopkg.in/yaml.v2
+https://gopkg.in/yaml.v2
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+================================================================
+
+gopkg.in/yaml.v3
+https://gopkg.in/yaml.v3
+----------------------------------------------------------------
+
+This project is covered by two different licenses: MIT and Apache.
+
+#### MIT License ####
+
+The following files were ported to Go from C files of libyaml, and thus
+are still covered by their original MIT license, with the additional
+copyright staring in 2011 when the project was ported over:
+
+ apic.go emitterc.go parserc.go readerc.go scannerc.go
+ writerc.go yamlh.go yamlprivateh.go
+
+Copyright (c) 2006-2010 Kirill Simonov
+Copyright (c) 2006-2011 Kirill Simonov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+### Apache License ###
+
+All the remaining project files are covered by the Apache license:
+
+Copyright (c) 2011-2019 Canonical Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+================================================================
+
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..693d814d
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,12 @@
+FROM minio/minio:latest
+
+RUN chmod -R 777 /usr/bin
+
+COPY ./minio /usr/bin/minio
+COPY dockerscripts/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
+
+ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
+
+VOLUME ["/data"]
+
+CMD ["minio"]
diff --git a/Dockerfile.cicd b/Dockerfile.cicd
new file mode 100644
index 00000000..118ab032
--- /dev/null
+++ b/Dockerfile.cicd
@@ -0,0 +1,3 @@
+FROM minio/minio:edge
+
+CMD ["minio", "server", "/data"]
diff --git a/Dockerfile.hotfix b/Dockerfile.hotfix
new file mode 100644
index 00000000..4dc428cf
--- /dev/null
+++ b/Dockerfile.hotfix
@@ -0,0 +1,71 @@
+FROM golang:1.24-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/hotfixes/linux-${TARGETARCH}/archive/minio.${RELEASE} -o /go/bin/minio && \
+ curl -s -q https://dl.min.io/server/minio/hotfixes/linux-${TARGETARCH}/archive/minio.${RELEASE}.minisig -o /go/bin/minio.minisig && \
+ curl -s -q https://dl.min.io/server/minio/hotfixes/linux-${TARGETARCH}/archive/minio.${RELEASE}.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 -o /go/bin/mc && \
+ curl -s -q https://dl.min.io/client/mc/release/linux-${TARGETARCH}/mc.minisig -o /go/bin/mc.minisig && \
+ curl -s -q https://dl.min.io/client/mc/release/linux-${TARGETARCH}/mc.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 && \
+ minisign -Vqm /go/bin/mc -x /go/bin/mc.minisig -P RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav
+
+FROM registry.access.redhat.com/ubi9/ubi-micro:latest
+
+ARG RELEASE
+
+LABEL name="MinIO" \
+ vendor="MinIO Inc " \
+ maintainer="MinIO Inc " \
+ 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 \
+ MC_CONFIG_DIR=/tmp/.mc
+
+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"]
diff --git a/Dockerfile.release b/Dockerfile.release
new file mode 100644
index 00000000..b2d05283
--- /dev/null
+++ b/Dockerfile.release
@@ -0,0 +1,73 @@
+FROM golang:1.24-alpine AS build
+
+ARG TARGETARCH
+ARG RELEASE
+
+ENV GOPATH=/go
+ENV CGO_ENABLED=0
+
+WORKDIR /build
+
+# Install curl and minisign
+RUN apk add -U --no-cache ca-certificates && \
+ apk add -U --no-cache curl && \
+ apk add -U --no-cache bash && \
+ 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} -o /go/bin/minio && \
+ curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.minisig -o /go/bin/minio.minisig && \
+ curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.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 -o /go/bin/mc && \
+ curl -s -q https://dl.min.io/client/mc/release/linux-${TARGETARCH}/mc.minisig -o /go/bin/mc.minisig && \
+ curl -s -q https://dl.min.io/client/mc/release/linux-${TARGETARCH}/mc.sha256sum -o /go/bin/mc.sha256sum && \
+ chmod +x /go/bin/mc
+
+# Verify binary signature using public key "RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGavRUN"
+RUN minisign -Vqm /go/bin/minio -x /go/bin/minio.minisig -P RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav && \
+ minisign -Vqm /go/bin/mc -x /go/bin/mc.minisig -P RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav
+
+COPY dockerscripts/download-static-curl.sh /build/download-static-curl
+RUN chmod +x /build/download-static-curl && \
+ /build/download-static-curl
+
+FROM registry.access.redhat.com/ubi9/ubi-micro:latest
+
+ARG RELEASE
+
+LABEL name="MinIO" \
+ vendor="MinIO Inc " \
+ maintainer="MinIO Inc " \
+ 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 \
+ MC_CONFIG_DIR=/tmp/.mc
+
+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/curl* /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"]
diff --git a/Dockerfile.release.old_cpu b/Dockerfile.release.old_cpu
new file mode 100644
index 00000000..5fc0f3e3
--- /dev/null
+++ b/Dockerfile.release.old_cpu
@@ -0,0 +1,71 @@
+FROM golang:1.24-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} -o /go/bin/minio && \
+ curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.minisig -o /go/bin/minio.minisig && \
+ curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.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 -o /go/bin/mc && \
+ curl -s -q https://dl.min.io/client/mc/release/linux-${TARGETARCH}/mc.minisig -o /go/bin/mc.minisig && \
+ curl -s -q https://dl.min.io/client/mc/release/linux-${TARGETARCH}/mc.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 && \
+ minisign -Vqm /go/bin/mc -x /go/bin/mc.minisig -P RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav
+
+FROM registry.access.redhat.com/ubi8/ubi-micro:latest
+
+ARG RELEASE
+
+LABEL name="MinIO" \
+ vendor="MinIO Inc " \
+ maintainer="MinIO Inc " \
+ 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 \
+ MC_CONFIG_DIR=/tmp/.mc
+
+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"]
diff --git a/Dockerfile.scratch b/Dockerfile.scratch
new file mode 100644
index 00000000..8074aa15
--- /dev/null
+++ b/Dockerfile.scratch
@@ -0,0 +1,5 @@
+FROM scratch
+
+COPY minio /minio
+
+CMD ["/minio"]
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 00000000..18fa743e
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,102 @@
+pipeline {
+ agent any
+
+ post {
+ always {
+ deleteDir() /* clean up our workspace */
+ }
+ success {
+ updateGitlabCommitStatus state: 'success'
+ }
+ failure {
+ updateGitlabCommitStatus state: 'failed'
+ step([$class: 'Mailer', notifyEveryUnstableBuild: true, recipients: 'support@confdroid.com', sendToIndividuals: true])
+ }
+ }
+
+ options {
+ gitLabConnection('gitlab.confdroid.com')
+ }
+
+ stages {
+
+ stage('SonarScan') {
+ steps {
+ withCredentials([string(credentialsId: 'sonar-token', variable: 'SONAR_TOKEN')]) {
+ sh '''
+ /opt/sonar-scanner/bin/sonar-scanner \
+ -Dsonar.projectKey=confdroid_minio \
+ -Dsonar.sources=. \
+ -Dsonar.host.url=https://sonarqube.confdroid.com \
+ -Dsonar.token=$SONAR_TOKEN
+ '''
+ }
+ }
+ }
+
+stage('build and push image to gitlab') {
+ steps {
+ withCredentials([usernamePassword(credentialsId: 'jenkins-gitlab-registry-token-2026', passwordVariable: 'pw', usernameVariable: 'un')]) {
+ sh '''
+ set +xe
+ docker login gitlab.confdroid.com:5050 -u $un -p $pw
+ docker build --network=host -t gitlab.confdroid.com:5050/containers/confdroid_minio:1.0.0 .
+ docker build --network=host -t gitlab.confdroid.com:5050/containers/confdroid_minio:latest .
+ docker push gitlab.confdroid.com:5050/containers/confdroid_minio:1.0.0
+ docker push gitlab.confdroid.com:5050/containers/confdroid_minio:latest
+ '''
+ }
+ }
+ }
+
+ stage('build and push image to gitea') {
+ steps {
+ withCredentials([usernamePassword(credentialsId: 'Gitea-package-token', passwordVariable: 'pw', usernameVariable: 'un')]) {
+ sh '''
+ set +xe
+ docker login sourcecode.confdroid.com -u $un -p $pw
+ docker build --network=host -t sourcecode.confdroid.com/confdroid/confdroid_minio:1.0.0 .
+ docker push sourcecode.confdroid.com/confdroid/confdroid_minio:1.0.0
+ docker build --network=host -t sourcecode.confdroid.com/confdroid/confdroid_minio:latest .
+ docker push sourcecode.confdroid.com/confdroid/confdroid_minio:latest
+ '''
+ }
+ }
+ }
+
+ stage('update repo') {
+ steps {
+ sshagent(['edd05eb6-26b5-4c7b-a5cc-ea2ab899f4fa']) {
+ sh '''
+ git config user.name "Jenkins Server"
+ git config user.email jenkins@confdroid.com
+ git add -A && git commit -am "Recommit for updates in build $BUILD_NUMBER" || echo "No changes to commit"
+ git push origin HEAD:master
+ '''
+ }
+ }
+ }
+ stage('Mirror to Gitea') {
+ steps {
+ withCredentials([usernamePassword(
+ credentialsId: 'Jenkins-gitea',
+ usernameVariable: 'GITEA_USER',
+ passwordVariable: 'GITEA_TOKEN')]) {
+ script {
+ // Checkout from GitLab (already done implicitly)
+ sh '''
+ git checkout master
+ git pull origin master
+ git rm -f Jenkinsfile || echo "No Jenkinsfile to remove from git"
+ git rm -r --cached .vscode || echo "No .vscode to remove from git"
+ git commit --amend --no-edit --allow-empty
+ git remote add master https://sourcecode.confdroid.com/confdroid/confdroid_minio.git
+ git -c credential.helper="!f() { echo username=${GITEA_USER}; echo password=${GITEA_TOKEN}; }; f" \
+ push master --mirror
+ '''
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..be3f7b28
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..c7ea4b27
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,245 @@
+PWD := $(shell pwd)
+GOPATH := $(shell go env GOPATH)
+LDFLAGS := $(shell go run buildscripts/gen-ldflags.go)
+
+GOOS ?= $(shell go env GOOS)
+GOARCH ?= $(shell go env GOARCH)
+
+VERSION ?= $(shell git describe --tags)
+REPO ?= quay.io/minio
+TAG ?= $(REPO)/minio:$(VERSION)
+
+GOLANGCI_DIR = .bin/golangci/$(GOLANGCI_VERSION)
+GOLANGCI = $(GOLANGCI_DIR)/golangci-lint
+
+all: build
+
+checks: ## check dependencies
+ @echo "Checking dependencies"
+ @(env bash $(PWD)/buildscripts/checkdeps.sh)
+
+help: ## print this help
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' Makefile | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-40s\033[0m %s\n", $$1, $$2}'
+
+getdeps: ## fetch necessary dependencies
+ @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)
+
+crosscompile: ## cross compile minio
+ @(env bash $(PWD)/buildscripts/cross-compile.sh)
+
+verifiers: lint check-gen
+
+check-gen: ## check for updated autogenerated files
+ @go generate ./... >/dev/null
+ @go mod tidy -compat=1.21
+ @(! git diff --name-only | grep '_gen.go$$') || (echo "Non-committed changes in auto-generated code is detected, please commit them to proceed." && false)
+ @(! git diff --name-only | grep 'go.sum') || (echo "Non-committed changes in auto-generated go.sum is detected, please commit them to proceed." && false)
+
+lint: getdeps ## runs golangci-lint suite of linters
+ @echo "Running $@ check"
+ @$(GOLANGCI) run --build-tags kqueue --timeout=10m --config ./.golangci.yml
+ @command typos && typos ./ || echo "typos binary is not found.. skipping.."
+
+lint-fix: getdeps ## runs golangci-lint suite of linters with automatic fixes
+ @echo "Running $@ check"
+ @$(GOLANGCI) run --build-tags kqueue --timeout=10m --config ./.golangci.yml --fix
+
+check: test
+test: verifiers build ## builds minio, runs linters, tests
+ @echo "Running unit tests"
+ @MINIO_API_REQUESTS_MAX=10000 CGO_ENABLED=0 go test -v -tags kqueue,dev ./...
+
+test-root-disable: install-race
+ @echo "Running minio root lockdown tests"
+ @env bash $(PWD)/buildscripts/disable-root.sh
+
+test-ilm: install-race
+ @echo "Running ILM tests"
+ @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
+ @echo "Running bucket policies tests"
+ @env bash $(PWD)/docs/iam/policies/pbac-tests.sh
+
+test-decom: install-race
+ @echo "Running minio decom tests"
+ @env bash $(PWD)/docs/distributed/decom.sh
+ @env bash $(PWD)/docs/distributed/decom-encrypted.sh
+ @env bash $(PWD)/docs/distributed/decom-encrypted-sse-s3.sh
+ @env bash $(PWD)/docs/distributed/decom-compressed-sse-s3.sh
+ @env bash $(PWD)/docs/distributed/decom-encrypted-kes.sh
+
+test-versioning: install-race
+ @echo "Running minio versioning tests"
+ @env bash $(PWD)/docs/bucket/versioning/versioning-tests.sh
+
+test-configfile: install-race
+ @env bash $(PWD)/docs/distributed/distributed-from-config-file.sh
+
+test-upgrade: install-race
+ @echo "Running minio upgrade tests"
+ @(env bash $(PWD)/buildscripts/minio-upgrade.sh)
+
+test-race: verifiers build ## builds minio, runs linters, tests (race)
+ @echo "Running unit tests under -race"
+ @(env bash $(PWD)/buildscripts/race.sh)
+
+test-iam: install-race ## verify IAM (external IDP, etcd backends)
+ @echo "Running tests for IAM (external IDP, etcd backends)"
+ @MINIO_API_REQUESTS_MAX=10000 CGO_ENABLED=0 go test -timeout 15m -tags kqueue,dev -v -run TestIAM* ./cmd
+ @echo "Running tests for IAM (external IDP, etcd backends) with -race"
+ @MINIO_API_REQUESTS_MAX=10000 GORACE=history_size=7 CGO_ENABLED=1 go test -timeout 15m -race -tags kqueue,dev -v -run TestIAM* ./cmd
+
+test-iam-ldap-upgrade-import: install-race ## verify IAM (external LDAP IDP)
+ @echo "Running upgrade tests for IAM (LDAP backend)"
+ @env bash $(PWD)/buildscripts/minio-iam-ldap-upgrade-import-test.sh
+
+test-iam-import-with-missing-entities: install-race ## test import of external iam config withg missing entities
+ @echo "Test IAM import configurations with missing entities"
+ @env bash $(PWD)/docs/distributed/iam-import-with-missing-entities.sh
+
+test-iam-import-with-openid: install-race
+ @echo "Test IAM import configurations with openid"
+ @env bash $(PWD)/docs/distributed/iam-import-with-openid.sh
+
+test-sio-error:
+ @(env bash $(PWD)/docs/bucket/replication/sio-error.sh)
+
+test-replication-2site:
+ @(env bash $(PWD)/docs/bucket/replication/setup_2site_existing_replication.sh)
+
+test-replication-3site:
+ @(env bash $(PWD)/docs/bucket/replication/setup_3site_replication.sh)
+
+test-delete-replication:
+ @(env bash $(PWD)/docs/bucket/replication/delete-replication.sh)
+
+test-delete-marker-proxying:
+ @(env bash $(PWD)/docs/bucket/replication/test_del_marker_proxying.sh)
+
+test-replication: install-race test-replication-2site test-replication-3site test-delete-replication test-sio-error test-delete-marker-proxying ## verify multi site replication
+ @echo "Running tests for replicating three sites"
+
+test-site-replication-ldap: install-race ## verify automatic site replication
+ @echo "Running tests for automatic site replication of IAM (with LDAP)"
+ @(env bash $(PWD)/docs/site-replication/run-multi-site-ldap.sh)
+
+test-site-replication-oidc: install-race ## verify automatic site replication
+ @echo "Running tests for automatic site replication of IAM (with OIDC)"
+ @(env bash $(PWD)/docs/site-replication/run-multi-site-oidc.sh)
+
+test-site-replication-minio: install-race ## verify automatic site replication
+ @echo "Running tests for automatic site replication of IAM (with MinIO IDP)"
+ @(env bash $(PWD)/docs/site-replication/run-multi-site-minio-idp.sh)
+ @echo "Running tests for automatic site replication of SSE-C objects"
+ @(env bash $(PWD)/docs/site-replication/run-ssec-object-replication.sh)
+ @echo "Running tests for automatic site replication of SSE-C objects with SSE-KMS enabled for bucket"
+ @(env bash $(PWD)/docs/site-replication/run-sse-kms-object-replication.sh)
+ @echo "Running tests for automatic site replication of SSE-C objects with compression enabled for site"
+ @(env bash $(PWD)/docs/site-replication/run-ssec-object-replication-with-compression.sh)
+
+test-multipart: install-race ## test multipart
+ @echo "Test multipart behavior when part files are missing"
+ @(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
+ @echo "Verifying build with race"
+ @(env bash $(PWD)/buildscripts/verify-build.sh)
+
+verify-healing: install-race ## verify healing and replacing disks with minio binary
+ @echo "Verify healing build with race"
+ @(env bash $(PWD)/buildscripts/verify-healing.sh)
+ @(env bash $(PWD)/buildscripts/verify-healing-empty-erasure-set.sh)
+ @(env bash $(PWD)/buildscripts/heal-inconsistent-versions.sh)
+
+verify-healing-with-root-disks: install-race ## verify healing root disks
+ @echo "Verify healing with root drives"
+ @(env bash $(PWD)/buildscripts/verify-healing-with-root-disks.sh)
+
+verify-healing-with-rewrite: install-race ## verify healing to rewrite old xl.meta -> new xl.meta
+ @echo "Verify healing with rewrite"
+ @(env bash $(PWD)/buildscripts/rewrite-old-new.sh)
+
+verify-healing-inconsistent-versions: install-race ## verify resolving inconsistent versions
+ @echo "Verify resolving inconsistent versions build with race"
+ @(env bash $(PWD)/buildscripts/resolve-right-versions.sh)
+
+build-debugging:
+ @(env bash $(PWD)/docs/debugging/build.sh)
+
+build: checks build-debugging ## builds minio to $(PWD)
+ @echo "Building minio binary to './minio'"
+ @CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) go build -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
+
+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 | \
+ sed 's#RELEASE\.\([0-9]\+\)-\([0-9]\+\)-\([0-9]\+\)T\([0-9]\+\)-\([0-9]\+\)-\([0-9]\+\)Z#\1-\2-\3T\4:\5:\6Z#')))
+ $(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
+ @wget -q -c https://github.com/minio/pkger/releases/download/v2.3.11/pkger_2.3.11_linux_amd64.deb
+ @wget -q -c https://raw.githubusercontent.com/minio/minio-service/v1.1.1/linux-systemd/distributed/minio.service
+ @sudo apt install ./pkger_2.3.11_linux_amd64.deb --yes
+ @mkdir -p minio-release/$(GOOS)-$(GOARCH)/archive
+ @cp -af ./minio minio-release/$(GOOS)-$(GOARCH)/minio
+ @cp -af ./minio minio-release/$(GOOS)-$(GOARCH)/minio.$(VERSION)
+ @minisign -qQSm minio-release/$(GOOS)-$(GOARCH)/minio.$(VERSION) -s "${CRED_DIR}/minisign.key" < "${CRED_DIR}/minisign-passphrase"
+ @sha256sum < minio-release/$(GOOS)-$(GOARCH)/minio.$(VERSION) | sed 's, -,minio.$(VERSION),g' > minio-release/$(GOOS)-$(GOARCH)/minio.$(VERSION).sha256sum
+ @cp -af minio-release/$(GOOS)-$(GOARCH)/minio.$(VERSION)* minio-release/$(GOOS)-$(GOARCH)/archive/
+ @pkger -r $(VERSION) --ignore
+
+hotfix-push: hotfix
+ @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-$(GOOS)/archive
+ @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-$(GOOS)/archive
+ @echo "Published new hotfix binaries at https://dl.min.io/server/minio/hotfixes/linux-$(GOOS)/archive/minio.$(VERSION)"
+
+docker-hotfix-push: docker-hotfix
+ @docker push -q $(TAG) && echo "Published new container $(TAG)"
+
+docker-hotfix: hotfix-push checks ## builds minio docker container with hotfix tags
+ @echo "Building minio docker image '$(TAG)'"
+ @docker build -q --no-cache -t $(TAG) --build-arg RELEASE=$(VERSION) . -f Dockerfile.hotfix
+
+docker: build ## builds minio docker container
+ @echo "Building minio docker image '$(TAG)'"
+ @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)
+ @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
+ @echo "Installing minio binary with -race to '$(GOPATH)/bin/minio'"
+ @mkdir -p $(GOPATH)/bin && cp -af $(PWD)/minio $(GOPATH)/bin/minio
+
+install: build ## builds minio and installs it to $GOPATH/bin.
+ @echo "Installing minio binary to '$(GOPATH)/bin/minio'"
+ @mkdir -p $(GOPATH)/bin && cp -af $(PWD)/minio $(GOPATH)/bin/minio
+ @echo "Installation successful. To learn more, try \"minio --help\"."
+
+clean: ## cleanup all generated assets
+ @echo "Cleaning up all the generated files"
+ @find . -name '*.test' | xargs rm -fv
+ @find . -name '*~' | xargs rm -fv
+ @find . -name '.#*#' | xargs rm -fv
+ @find . -name '#*#' | xargs rm -fv
+ @rm -rvf minio
+ @rm -rvf build
+ @rm -rvf release
+ @rm -rvf .verify*
+ @rm -rvf minio-release
+ @rm -rvf minio.RELEASE*.hotfix.*
+ @rm -rvf pkger_*.deb
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 00000000..cee66a9c
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,9 @@
+MinIO Project, (C) 2015-2023 MinIO, Inc.
+
+This product includes software developed at MinIO, Inc.
+(https://min.io/).
+
+The MinIO project contains unmodified/modified subcomponents too with
+separate copyright notices and license terms. Your use of the source
+code for these subcomponents is subject to the terms and conditions
+of GNU Affero General Public License 3.0.
diff --git a/PULL_REQUESTS_ETIQUETTE.md b/PULL_REQUESTS_ETIQUETTE.md
new file mode 100644
index 00000000..a4a9008f
--- /dev/null
+++ b/PULL_REQUESTS_ETIQUETTE.md
@@ -0,0 +1,93 @@
+# MinIO Pull Request Guidelines
+
+These guidelines ensure high-quality commits in MinIO’s GitHub repositories, maintaining
+a clear, valuable commit history for our open-source projects. They apply to all contributors,
+fostering efficient reviews and robust code.
+
+## Why Pull Requests?
+
+Pull Requests (PRs) drive quality in MinIO’s codebase by:
+- Enabling peer review without pair programming.
+- Documenting changes for future reference.
+- Ensuring commits tell a clear story of development.
+
+**A poor commit lasts forever, even if code is refactored.**
+
+## Crafting a Quality PR
+
+A strong MinIO PR:
+- Delivers a complete, valuable change (feature, bug fix, or improvement).
+- Has a concise title (e.g., `[S3] Fix bucket policy parsing #1234`) and a summary with context, referencing issues (e.g., `#1234`).
+- Contains well-written, logical commits explaining *why* changes were made (e.g., “Add S3 bucket tagging support so that users can organize resources efficiently”).
+- Is small, focused, and easy to review—ideally one commit, unless multiple commits better narrate complex work.
+- Adheres to MinIO’s coding standards (e.g., Go style, error handling, testing).
+
+PRs must flow smoothly through review to reach production. Large PRs should be split into smaller, manageable ones.
+
+## Submitting PRs
+
+1. **Title and Summary**:
+ - Use a scannable title: `[Subsystem] Action Description #Issue` (e.g., `[IAM] Add role-based access control #567`).
+ - Include context in the summary: what changed, why, and any issue references.
+ - Use `[WIP]` for in-progress PRs to avoid premature merging or choose GitHub draft PRs.
+
+2. **Commits**:
+ - Write clear messages: what changed and why (e.g., “Refactor S3 API handler to reduce latency so that requests process 20% faster”).
+ - Rebase to tidy commits before submitting (e.g., `git rebase -i main` to squash typos or reword messages), unless multiple contributors worked on the branch.
+ - Keep PRs focused—one feature or fix. Split large changes into multiple PRs.
+
+3. **Testing**:
+ - Include unit tests for new functionality or bug fixes.
+ - Ensure existing tests pass (`make test`).
+ - Document testing steps in the PR summary if manual testing was performed.
+
+4. **Before Submitting**:
+ - Run `make verify` to check formatting, linting, and tests.
+ - Reference related issues (e.g., “Closes #1234”).
+ - Notify team members via GitHub `@mentions` if urgent or complex.
+
+## Reviewing PRs
+
+Reviewers ensure MinIO’s commit history remains a clear, reliable record. Responsibilities include:
+
+1. **Commit Quality**:
+ - Verify each commit explains *why* the change was made (e.g., “So that…”).
+ - Request rebasing if commits are unclear, redundant, or lack context (e.g., “Please squash typo fixes into the parent commit”).
+
+2. **Code Quality**:
+ - Check adherence to MinIO’s Go standards (e.g., error handling, documentation).
+ - Ensure tests cover new code and pass CI.
+ - Flag bugs or critical issues for immediate fixes; suggest non-blocking improvements as follow-up issues.
+
+3. **Flow**:
+ - Review promptly to avoid blocking progress.
+ - Balance quality and speed—minor issues can be addressed later via issues, not PR blocks.
+ - If unable to complete the review, tag another reviewer (e.g., `@username please take over`).
+
+4. **Shared Responsibility**:
+ - All MinIO contributors are reviewers. The first commenter on a PR owns the review unless they delegate.
+ - Multiple reviewers are encouraged for complex PRs.
+
+5. **No Self-Edits**:
+ - Don’t modify the PR directly (e.g., fixing bugs). Request changes from the submitter or create a follow-up PR.
+ - If you edit, you’re a collaborator, not a reviewer, and cannot merge.
+
+6. **Testing**:
+ - Assume the submitter tested the code. If testing is unclear, ask for details (e.g., “How was this tested?”).
+ - Reject untested PRs unless testing is infeasible, then assist with test setup.
+
+## Tips for Success
+
+- **Small PRs**: Easier to review, faster to merge. Split large changes logically.
+- **Clear Commits**: Use `git rebase -i` to refine history before submitting.
+- **Engage Early**: Discuss complex changes in issues or Slack (https://slack.min.io) before coding.
+- **Be Responsive**: Address reviewer feedback promptly to keep PRs moving.
+- **Learn from Reviews**: Use feedback to improve future contributions.
+
+## Resources
+
+- [MinIO Coding Standards](https://github.com/minio/minio/blob/master/CONTRIBUTING.md)
+- [Effective Commit Messages](https://mislav.net/2014/02/hidden-documentation/)
+- [GitHub PR Tips](https://github.com/blog/1943-how-to-write-the-perfect-pull-request)
+
+By following these guidelines, we ensure MinIO’s codebase remains high-quality, maintainable, and a joy to contribute to. Happy coding!
diff --git a/README.md b/README.md
index 1ee7aa6d..46929f90 100644
--- a/README.md
+++ b/README.md
@@ -1,93 +1,160 @@
-# confdroid_minio
+# MinIO Quickstart Guide
+[](https://slack.min.io) [](https://hub.docker.com/r/minio/minio/) [](https://github.com/minio/minio/blob/master/LICENSE)
+[](https://min.io)
-## Getting started
+MinIO is a high-performance, S3-compatible object storage solution released under the GNU AGPL v3.0 license.
+Designed for speed and scalability, it powers AI/ML, analytics, and data-intensive workloads with industry-leading performance.
-To make it easy for you to get started with GitLab, here's a list of recommended next steps.
+- S3 API Compatible – Seamless integration with existing S3 tools
+- Built for AI & Analytics – Optimized for large-scale data pipelines
+- High Performance – Ideal for demanding storage workloads.
-Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
+This README provides instructions for building MinIO from source and deploying onto baremetal hardware.
+Use the [MinIO Documentation](https://github.com/minio/docs) project to build and host a local copy of the documentation.
-## Add your files
+## MinIO is Open Source Software
-* [Create](https://docs.gitlab.com/user/project/repository/web_editor/#create-a-file) or [upload](https://docs.gitlab.com/user/project/repository/web_editor/#upload-a-file) files
-* [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command:
+We designed MinIO as Open Source software for the Open Source software community. We encourage the community to remix, redesign, and reshare MinIO under the terms of the AGPLv3 license.
-```
-cd existing_repo
-git remote add origin https://gitlab.confdroid.com/containers/confdroid_minio.git
-git branch -M master
-git push -uf origin master
+All usage of MinIO in your application stack requires validation against AGPLv3 obligations, which include but are not limited to the release of modified code to the community from which you have benefited. Any commercial/proprietary usage of the AGPLv3 software, including repackaging or reselling services/features, is done at your own risk.
+
+The AGPLv3 provides no obligation by any party to support, maintain, or warranty the original or any modified work.
+All support is provided on a best-effort basis through Github and our [Slack](https//slack.min.io) channel, and any member of the community is welcome to contribute and assist others in their usage of the software.
+
+MinIO [AIStor](https://www.min.io/product/aistor) includes enterprise-grade support and licensing for workloads which require commercial or proprietary usage and production-level SLA/SLO-backed support. For more information, [reach out for a quote](https://min.io/pricing).
+
+## Source-Only Distribution
+
+**Important:** The MinIO community edition is now distributed as source code only. We will no longer provide pre-compiled binary releases for the community version.
+
+### Installing Latest MinIO Community Edition
+
+To use MinIO community edition, you have two options:
+
+1. **Install from source** using `go install github.com/minio/minio@latest` (recommended)
+2. **Build a Docker image** from the provided Dockerfile
+
+See the sections below for detailed instructions on each method.
+
+### Legacy Binary Releases
+
+Historical pre-compiled binary releases remain available for reference but are no longer maintained:
+- GitHub Releases: https://github.com/minio/minio/releases
+- Direct downloads: https://dl.min.io/server/minio/release/
+
+**These legacy binaries will not receive updates.** We strongly recommend using source builds for access to the latest features, bug fixes, and security updates.
+
+## Install from Source
+
+Use the following commands to compile and run a standalone MinIO server from source.
+If you do not have a working Golang environment, please follow [How to install Golang](https://golang.org/doc/install). Minimum version required is [go1.24](https://golang.org/dl/#stable)
+
+```sh
+go install github.com/minio/minio@latest
```
-## Integrate with your tools
+You can alternatively run `go build` and use the `GOOS` and `GOARCH` environment variables to control the OS and architecture target.
+For example:
-* [Set up project integrations](https://gitlab.confdroid.com/containers/confdroid_minio/-/settings/integrations)
+```
+env GOOS=linux GOARCh=arm64 go build
+```
-## Collaborate with your team
+Start MinIO by running `minio server PATH` where `PATH` is any empty folder on your local filesystem.
-* [Invite team members and collaborators](https://docs.gitlab.com/user/project/members/)
-* [Create a new merge request](https://docs.gitlab.com/user/project/merge_requests/creating_merge_requests/)
-* [Automatically close issues from merge requests](https://docs.gitlab.com/user/project/issues/managing_issues/#closing-issues-automatically)
-* [Enable merge request approvals](https://docs.gitlab.com/user/project/merge_requests/approvals/)
-* [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/)
+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 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.
-## Test and Deploy
+You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool:
-Use the built-in continuous integration in GitLab.
+```sh
+mc alias set local http://localhost:9000 minioadmin minioadmin
+mc admin info local
+```
-* [Get started with GitLab CI/CD](https://docs.gitlab.com/ci/quick_start/)
-* [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/user/application_security/sast/)
-* [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/topics/autodevops/requirements/)
-* [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/user/clusters/agent/)
-* [Set up protected environments](https://docs.gitlab.com/ci/environments/protected_environments/)
+See [Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool.
+For application developers, see to view MinIO SDKs for supported languages.
-***
+> [!NOTE]
+> Production environments using compiled-from-source MinIO binaries do so at their own risk.
+> The AGPLv3 license provides no warranties nor liabilites for any such usage.
-# Editing this README
+## Build Docker Image
-When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
+You can use the `docker build .` command to build a Docker image on your local host machine.
+You must first [build MinIO](#install-from-source) and ensure the `minio` binary exists in the project root.
-## Suggestions for a good README
+The following command builds the Docker image using the default `Dockerfile` in the root project directory with the repository and image tag `myminio:minio`
-Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
+```sh
+docker build -t myminio:minio .
+```
-## Name
-Choose a self-explaining name for your project.
+Use `docker image ls` to confirm the image exists in your local repository.
+You can run the server using standard Docker invocation:
-## Description
-Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
+```sh
+docker run -p 9000:9000 -p 9001:9001 myminio:minio server /tmp/minio --console-address :9001
+```
-## Badges
-On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
+Complete documentation for building Docker containers, managing custom images, or loading images into orchestration platforms is out of scope for this documentation.
+You can modify the `Dockerfile` and `dockerscripts/socker-entrypoint.sh` as-needed to reflect your specific image requirements.
-## Visuals
-Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
+See the [MinIO Container](https://docs.min.io/community/minio-object-store/operations/deployments/baremetal-deploy-minio-as-a-container.html#deploy-minio-container) documentation for more guidance on running MinIO within a Container image.
-## Installation
-Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
+## Install using Helm Charts
-## Usage
-Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
+There are two paths for installing MinIO onto Kubernetes infrastructure:
-## Support
-Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
+- Use the [MinIO Operator](https://github.com/minio/operator)
+- Use the community-maintained [Helm charts](https://github.com/minio/minio/tree/master/helm/minio)
-## Roadmap
-If you have ideas for releases in the future, it is a good idea to list them in the README.
+See the [MinIO Documentation](https://docs.min.io/community/minio-object-store/operations/deployments/kubernetes.html) for guidance on deploying using the Operator.
+The Community Helm chart has instructions in the folder-level README.
-## Contributing
-State if you are open to contributions and what your requirements are for accepting them.
+## Test MinIO Connectivity
-For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
+### Test using MinIO Console
-You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
+MinIO Server comes with an embedded web based object browser.
+Point your web browser to to ensure your server has started successfully.
-## Authors and acknowledgment
-Show your appreciation to those who have contributed to the project.
+> [!NOTE]
+> MinIO runs console on random port by default, if you wish to choose a specific port use `--console-address` to pick a specific interface and port.
+
+### Test using MinIO Client `mc`
+
+`mc` provides a modern alternative to UNIX commands like ls, cat, cp, mirror, diff etc. It supports filesystems and Amazon S3 compatible cloud storage services.
+
+The following commands set a local alias, validate the server information, create a bucket, copy data to that bucket, and list the contents of the bucket.
+
+```sh
+mc alias set local http://localhost:9000 minioadmin minioadmin
+mc admin info
+mc mb data
+mc cp ~/Downloads/mydata data/
+mc ls data/
+```
+
+Follow the MinIO Client [Quickstart Guide](https://docs.min.io/community/minio-object-store/reference/minio-mc.html#quickstart) for further instructions.
+
+## Explore Further
+
+- [The MinIO documentation website](https://docs.min.io/community/minio-object-store/index.html)
+- [MinIO Erasure Code Overview](https://docs.min.io/community/minio-object-store/operations/concepts/erasure-coding.html)
+- [Use `mc` with MinIO Server](https://docs.min.io/community/minio-object-store/reference/minio-mc.html)
+- [Use `minio-go` SDK with MinIO Server](https://docs.min.io/community/minio-object-store/developers/go/minio-go.html)
+
+## Contribute to MinIO Project
+
+Please follow MinIO [Contributor's Guide](https://github.com/minio/minio/blob/master/CONTRIBUTING.md) for guidance on making new contributions to the repository.
## License
-For open source projects, say how it is licensed.
-## Project status
-If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
+- MinIO source is licensed under the [GNU AGPLv3](https://github.com/minio/minio/blob/master/LICENSE).
+- MinIO [documentation](https://github.com/minio/minio/tree/master/docs) is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
+- [License Compliance](https://github.com/minio/minio/blob/master/COMPLIANCE.md)
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..496b2ef2
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,42 @@
+# Security Policy
+
+## Supported Versions
+
+We always provide security updates for the [latest release](https://github.com/minio/minio/releases/latest).
+Whenever there is a security update you just need to upgrade to the latest version.
+
+## Reporting a Vulnerability
+
+All security bugs in [minio/minio](https://github,com/minio/minio) (or other minio/* repositories)
+should be reported by email to security@min.io. Your email will be acknowledged within 48 hours,
+and you'll receive a more detailed response to your email within 72 hours indicating the next steps
+in handling your report.
+
+Please, provide a detailed explanation of the issue. In particular, outline the type of the security
+issue (DoS, authentication bypass, information disclose, ...) and the assumptions you're making (e.g. do
+you need access credentials for a successful exploit).
+
+If you have not received a reply to your email within 48 hours or you have not heard from the security team
+for the past five days please contact the security team directly:
+
+- Primary security coordinator: aead@min.io
+- Secondary coordinator: harsha@min.io
+- If you receive no response: dev@min.io
+
+### Disclosure Process
+
+MinIO uses the following disclosure process:
+
+1. Once the security report is received one member of the security team tries to verify and reproduce
+ the issue and determines the impact it has.
+2. A member of the security team will respond and either confirm or reject the security report.
+ If the report is rejected the response explains why.
+3. Code is audited to find any potential similar problems.
+4. Fixes are prepared for the latest release.
+5. On the date that the fixes are applied a security advisory will be published on .
+ Please inform us in your report email whether MinIO should mention your contribution w.r.t. fixing
+ the security issue. By default MinIO will **not** publish this information to protect your privacy.
+
+This process can take some time, especially when coordination is required with maintainers of other projects.
+Every effort will be made to handle the bug in as timely a manner as possible, however it's important that we
+follow the process described above to ensure that disclosures are handled consistently.
diff --git a/VULNERABILITY_REPORT.md b/VULNERABILITY_REPORT.md
new file mode 100644
index 00000000..8fcace07
--- /dev/null
+++ b/VULNERABILITY_REPORT.md
@@ -0,0 +1,38 @@
+# Vulnerability Management Policy
+
+This document formally describes the process of addressing and managing a
+reported vulnerability that has been found in the MinIO server code base,
+any directly connected ecosystem component or a direct / indirect dependency
+of the code base.
+
+## Scope
+
+The vulnerability management policy described in this document covers the
+process of investigating, assessing and resolving a vulnerability report
+opened by a MinIO employee or an external third party.
+
+Therefore, it lists pre-conditions and actions that should be performed to
+resolve and fix a reported vulnerability.
+
+## Vulnerability Management Process
+
+The vulnerability management process requires that the vulnerability report
+contains the following information:
+
+- The project / component that contains the reported vulnerability.
+- A description of the vulnerability. In particular, the type of the
+ reported vulnerability and how it might be exploited. Alternatively,
+ a well-established vulnerability identifier, e.g. CVE number, can be
+ used instead.
+
+Based on the description mentioned above, a MinIO engineer or security team
+member investigates:
+
+- Whether the reported vulnerability exists.
+- The conditions that are required such that the vulnerability can be exploited.
+- The steps required to fix the vulnerability.
+
+In general, if the vulnerability exists in one of the MinIO code bases
+itself - not in a code dependency - then MinIO will, if possible, fix
+the vulnerability or implement reasonable countermeasures such that the
+vulnerability cannot be exploited anymore.
diff --git a/_config.yml b/_config.yml
new file mode 100644
index 00000000..2f7efbea
--- /dev/null
+++ b/_config.yml
@@ -0,0 +1 @@
+theme: jekyll-theme-minimal
\ No newline at end of file
diff --git a/buildscripts/checkdeps.sh b/buildscripts/checkdeps.sh
new file mode 100755
index 00000000..ed4f666e
--- /dev/null
+++ b/buildscripts/checkdeps.sh
@@ -0,0 +1,143 @@
+#!/usr/bin/env bash
+#
+
+_init() {
+
+ shopt -s extglob
+
+ ## Minimum required versions for build dependencies
+ GIT_VERSION="1.0"
+ GO_VERSION="1.16"
+ OSX_VERSION="10.8"
+ KNAME=$(uname -s)
+ ARCH=$(uname -m)
+ case "${KNAME}" in
+ SunOS)
+ ARCH=$(isainfo -k)
+ ;;
+ esac
+}
+
+## FIXME:
+## In OSX, 'readlink -f' option does not exist, hence
+## we have our own readlink -f behavior here.
+## Once OSX has the option, below function is good enough.
+##
+## readlink() {
+## return /bin/readlink -f "$1"
+## }
+##
+readlink() {
+ TARGET_FILE=$1
+
+ cd $(dirname $TARGET_FILE)
+ TARGET_FILE=$(basename $TARGET_FILE)
+
+ # Iterate down a (possible) chain of symlinks
+ while [ -L "$TARGET_FILE" ]; do
+ TARGET_FILE=$(env readlink $TARGET_FILE)
+ cd $(dirname $TARGET_FILE)
+ TARGET_FILE=$(basename $TARGET_FILE)
+ done
+
+ # Compute the canonicalized name by finding the physical path
+ # for the directory we're in and appending the target file.
+ PHYS_DIR=$(pwd -P)
+ RESULT=$PHYS_DIR/$TARGET_FILE
+ echo $RESULT
+}
+
+## FIXME:
+## In OSX, 'sort -V' option does not exist, hence
+## we have our own version compare function.
+## Once OSX has the option, below function is good enough.
+##
+## check_minimum_version() {
+## versions=($(echo -e "$1\n$2" | sort -V))
+## return [ "$1" == "${versions[0]}" ]
+## }
+##
+check_minimum_version() {
+ IFS='.' read -r -a varray1 <<<"$1"
+ IFS='.' read -r -a varray2 <<<"$2"
+
+ for i in "${!varray1[@]}"; do
+ if [[ ${varray1[i]} -lt ${varray2[i]} ]]; then
+ return 0
+ elif [[ ${varray1[i]} -gt ${varray2[i]} ]]; then
+ return 1
+ fi
+ done
+
+ return 0
+}
+
+assert_is_supported_arch() {
+ case "${ARCH}" in
+ x86_64 | amd64 | aarch64 | ppc64le | arm* | s390x | loong64 | loongarch64 | riscv64)
+ return
+ ;;
+ *)
+ echo "Arch '${ARCH}' is not supported. Supported Arch: [x86_64, amd64, aarch64, ppc64le, arm*, s390x, loong64, loongarch64, riscv64]"
+ exit 1
+ ;;
+ esac
+}
+
+assert_is_supported_os() {
+ case "${KNAME}" in
+ Linux | FreeBSD | OpenBSD | NetBSD | DragonFly | SunOS)
+ return
+ ;;
+ Darwin)
+ osx_host_version=$(env sw_vers -productVersion)
+ if ! check_minimum_version "${OSX_VERSION}" "${osx_host_version}"; then
+ echo "OSX version '${osx_host_version}' is not supported. Minimum supported version: ${OSX_VERSION}"
+ exit 1
+ fi
+ return
+ ;;
+ *)
+ echo "OS '${KNAME}' is not supported. Supported OS: [Linux, FreeBSD, OpenBSD, NetBSD, Darwin, DragonFly]"
+ exit 1
+ ;;
+ esac
+}
+
+assert_check_golang_env() {
+ if ! which go >/dev/null 2>&1; then
+ echo "Cannot find go binary in your PATH configuration, please refer to Go installation document at https://golang.org/doc/install"
+ exit 1
+ fi
+
+ installed_go_version=$(go version | sed 's/^.* go\([0-9.]*\).*$/\1/')
+ if ! check_minimum_version "${GO_VERSION}" "${installed_go_version}"; then
+ echo "Go runtime version '${installed_go_version}' is unsupported. Minimum supported version: ${GO_VERSION} to compile."
+ exit 1
+ fi
+}
+
+assert_check_deps() {
+ # support unusual Git versions such as: 2.7.4 (Apple Git-66)
+ installed_git_version=$(git version | perl -ne '$_ =~ m/git version (.*?)( |$)/; print "$1\n";')
+ if ! check_minimum_version "${GIT_VERSION}" "${installed_git_version}"; then
+ echo "Git version '${installed_git_version}' is not supported. Minimum supported version: ${GIT_VERSION}"
+ exit 1
+ fi
+}
+
+main() {
+ ## Check for supported arch
+ assert_is_supported_arch
+
+ ## Check for supported os
+ assert_is_supported_os
+
+ ## Check for Go environment
+ assert_check_golang_env
+
+ ## Check for dependencies
+ assert_check_deps
+}
+
+_init && main "$@"
diff --git a/buildscripts/cicd-corpus/disk1/bucket/testobj/2b4f7e41-df82-4a5e-a3c1-8df87f83332f/part.1 b/buildscripts/cicd-corpus/disk1/bucket/testobj/2b4f7e41-df82-4a5e-a3c1-8df87f83332f/part.1
new file mode 100644
index 00000000..afbb2da1
Binary files /dev/null and b/buildscripts/cicd-corpus/disk1/bucket/testobj/2b4f7e41-df82-4a5e-a3c1-8df87f83332f/part.1 differ
diff --git a/buildscripts/cicd-corpus/disk2/bucket/testobj/2b4f7e41-df82-4a5e-a3c1-8df87f83332f/part.1 b/buildscripts/cicd-corpus/disk2/bucket/testobj/2b4f7e41-df82-4a5e-a3c1-8df87f83332f/part.1
new file mode 100644
index 00000000..1031cd2a
Binary files /dev/null and b/buildscripts/cicd-corpus/disk2/bucket/testobj/2b4f7e41-df82-4a5e-a3c1-8df87f83332f/part.1 differ
diff --git a/buildscripts/cicd-corpus/disk2/bucket/testobj/xl.meta b/buildscripts/cicd-corpus/disk2/bucket/testobj/xl.meta
new file mode 100644
index 00000000..4e786994
Binary files /dev/null and b/buildscripts/cicd-corpus/disk2/bucket/testobj/xl.meta differ
diff --git a/buildscripts/cicd-corpus/disk3/bucket/testobj/2b4f7e41-df82-4a5e-a3c1-8df87f83332f/part.1 b/buildscripts/cicd-corpus/disk3/bucket/testobj/2b4f7e41-df82-4a5e-a3c1-8df87f83332f/part.1
new file mode 100644
index 00000000..789a168c
Binary files /dev/null and b/buildscripts/cicd-corpus/disk3/bucket/testobj/2b4f7e41-df82-4a5e-a3c1-8df87f83332f/part.1 differ
diff --git a/buildscripts/cicd-corpus/disk3/bucket/testobj/xl.meta b/buildscripts/cicd-corpus/disk3/bucket/testobj/xl.meta
new file mode 100644
index 00000000..cba1a347
Binary files /dev/null and b/buildscripts/cicd-corpus/disk3/bucket/testobj/xl.meta differ
diff --git a/buildscripts/cicd-corpus/disk4/bucket/testobj/2b4f7e41-df82-4a5e-a3c1-8df87f83332f/part.1 b/buildscripts/cicd-corpus/disk4/bucket/testobj/2b4f7e41-df82-4a5e-a3c1-8df87f83332f/part.1
new file mode 100644
index 00000000..86700285
Binary files /dev/null and b/buildscripts/cicd-corpus/disk4/bucket/testobj/2b4f7e41-df82-4a5e-a3c1-8df87f83332f/part.1 differ
diff --git a/buildscripts/cicd-corpus/disk4/bucket/testobj/a599bd9e-69fe-49b7-b6bf-fe53021039d5/part.1 b/buildscripts/cicd-corpus/disk4/bucket/testobj/a599bd9e-69fe-49b7-b6bf-fe53021039d5/part.1
new file mode 100644
index 00000000..86700285
Binary files /dev/null and b/buildscripts/cicd-corpus/disk4/bucket/testobj/a599bd9e-69fe-49b7-b6bf-fe53021039d5/part.1 differ
diff --git a/buildscripts/cicd-corpus/disk4/bucket/testobj/xl.meta b/buildscripts/cicd-corpus/disk4/bucket/testobj/xl.meta
new file mode 100644
index 00000000..54bbedd6
Binary files /dev/null and b/buildscripts/cicd-corpus/disk4/bucket/testobj/xl.meta differ
diff --git a/buildscripts/cicd-corpus/disk5/bucket/testobj/2b4f7e41-df82-4a5e-a3c1-8df87f83332f/part.1 b/buildscripts/cicd-corpus/disk5/bucket/testobj/2b4f7e41-df82-4a5e-a3c1-8df87f83332f/part.1
new file mode 100644
index 00000000..d39542b5
Binary files /dev/null and b/buildscripts/cicd-corpus/disk5/bucket/testobj/2b4f7e41-df82-4a5e-a3c1-8df87f83332f/part.1 differ
diff --git a/buildscripts/cicd-corpus/disk5/bucket/testobj/a599bd9e-69fe-49b7-b6bf-fe53021039d5/part.1 b/buildscripts/cicd-corpus/disk5/bucket/testobj/a599bd9e-69fe-49b7-b6bf-fe53021039d5/part.1
new file mode 100644
index 00000000..d39542b5
Binary files /dev/null and b/buildscripts/cicd-corpus/disk5/bucket/testobj/a599bd9e-69fe-49b7-b6bf-fe53021039d5/part.1 differ
diff --git a/buildscripts/cicd-corpus/disk5/bucket/testobj/xl.meta b/buildscripts/cicd-corpus/disk5/bucket/testobj/xl.meta
new file mode 100644
index 00000000..68f69a63
Binary files /dev/null and b/buildscripts/cicd-corpus/disk5/bucket/testobj/xl.meta differ
diff --git a/buildscripts/cross-compile.sh b/buildscripts/cross-compile.sh
new file mode 100755
index 00000000..0590aebb
--- /dev/null
+++ b/buildscripts/cross-compile.sh
@@ -0,0 +1,37 @@
+#!/bin/bash
+
+set -e
+# Enable tracing if set.
+[ -n "$BASH_XTRACEFD" ] && set -x
+
+function _init() {
+ ## All binaries are static make sure to disable CGO.
+ export CGO_ENABLED=0
+
+ ## List of architectures and OS to test coss compilation.
+ SUPPORTED_OSARCH="linux/ppc64le linux/mips64 linux/amd64 linux/arm64 linux/s390x darwin/arm64 darwin/amd64 freebsd/amd64 windows/amd64 linux/arm linux/386 netbsd/amd64 linux/mips openbsd/amd64 linux/riscv64"
+}
+
+function _build() {
+ local osarch=$1
+ IFS=/ read -r -a arr <<<"$osarch"
+ os="${arr[0]}"
+ arch="${arr[1]}"
+ package=$(go list -f '{{.ImportPath}}')
+ printf -- "--> %15s:%s\n" "${osarch}" "${package}"
+
+ # go build -trimpath to build the binary.
+ export GOOS=$os
+ export GOARCH=$arch
+ export GO111MODULE=on
+ go build -trimpath -tags kqueue -o /dev/null
+}
+
+function main() {
+ echo "Testing builds for OS/Arch: ${SUPPORTED_OSARCH}"
+ for each_osarch in ${SUPPORTED_OSARCH}; do
+ _build "${each_osarch}"
+ done
+}
+
+_init && main "$@"
diff --git a/buildscripts/disable-root.sh b/buildscripts/disable-root.sh
new file mode 100755
index 00000000..c35c769f
--- /dev/null
+++ b/buildscripts/disable-root.sh
@@ -0,0 +1,124 @@
+#!/bin/bash
+
+set -x
+
+export MINIO_CI_CD=1
+killall -9 minio
+
+rm -rf ${HOME}/tmp/dist
+
+scheme="http"
+nr_servers=4
+
+addr="localhost"
+args=""
+for ((i = 0; i < $((nr_servers)); i++)); do
+ args="$args $scheme://$addr:$((9100 + i))/${HOME}/tmp/dist/path1/$i"
+done
+
+echo $args
+
+for ((i = 0; i < $((nr_servers)); i++)); do
+ (minio server --address ":$((9100 + i))" $args 2>&1 >/tmp/log$i.txt) &
+done
+
+sleep 10s
+
+if [ ! -f ./mc ]; then
+ wget --quiet -O ./mc https://dl.minio.io/client/mc/release/linux-amd64/./mc &&
+ chmod +x mc
+fi
+
+set +e
+
+export MC_HOST_minioadm=http://minioadmin:minioadmin@localhost:9100/
+./mc ready minioadm
+
+./mc ls minioadm/
+
+./mc admin config set minioadm/ api root_access=off
+
+sleep 3s # let things settle a little
+
+./mc ls minioadm/
+if [ $? -eq 0 ]; then
+ echo "listing succeeded, 'minioadmin' was not disabled"
+ exit 1
+fi
+
+set -e
+
+killall -9 minio
+
+export MINIO_API_ROOT_ACCESS=on
+for ((i = 0; i < $((nr_servers)); i++)); do
+ (minio server --address ":$((9100 + i))" $args 2>&1 >/tmp/log$i.txt) &
+done
+
+set +e
+
+./mc ready minioadm/
+
+./mc ls minioadm/
+if [ $? -ne 0 ]; then
+ echo "listing failed, 'minioadmin' should be enabled"
+ exit 1
+fi
+
+killall -9 minio
+
+rm -rf /tmp/multisitea/
+rm -rf /tmp/multisiteb/
+
+echo "Setup site-replication and then disable root credentials"
+
+minio server --address 127.0.0.1:9001 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \
+ "http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_1.log 2>&1 &
+minio server --address 127.0.0.1:9002 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \
+ "http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_2.log 2>&1 &
+
+minio server --address 127.0.0.1:9003 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \
+ "http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_1.log 2>&1 &
+minio server --address 127.0.0.1:9004 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \
+ "http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_2.log 2>&1 &
+
+export MC_HOST_sitea=http://minioadmin:minioadmin@127.0.0.1:9001
+export MC_HOST_siteb=http://minioadmin:minioadmin@127.0.0.1:9004
+
+./mc ready sitea
+./mc ready siteb
+
+./mc admin replicate add sitea siteb
+
+./mc admin user add sitea foobar foo12345
+
+./mc admin policy attach sitea/ consoleAdmin --user=foobar
+
+./mc admin user info siteb foobar
+
+killall -9 minio
+
+echo "turning off root access, however site replication must continue"
+export MINIO_API_ROOT_ACCESS=off
+
+minio server --address 127.0.0.1:9001 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \
+ "http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_1.log 2>&1 &
+minio server --address 127.0.0.1:9002 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \
+ "http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_2.log 2>&1 &
+
+minio server --address 127.0.0.1:9003 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \
+ "http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_1.log 2>&1 &
+minio server --address 127.0.0.1:9004 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \
+ "http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_2.log 2>&1 &
+
+export MC_HOST_sitea=http://foobar:foo12345@127.0.0.1:9001
+export MC_HOST_siteb=http://foobar:foo12345@127.0.0.1:9004
+
+./mc ready sitea
+./mc ready siteb
+
+./mc admin user add sitea foobar-admin foo12345
+
+sleep 2s
+
+./mc admin user info siteb foobar-admin
diff --git a/buildscripts/gen-ldflags.go b/buildscripts/gen-ldflags.go
new file mode 100644
index 00000000..a7649056
--- /dev/null
+++ b/buildscripts/gen-ldflags.go
@@ -0,0 +1,121 @@
+//go:build ignore
+// +build ignore
+
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package main
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "strconv"
+ "strings"
+ "time"
+)
+
+func genLDFlags(version string) string {
+ releaseTag, date := releaseTag(version)
+ copyrightYear := strconv.Itoa(date.Year())
+ ldflagsStr := "-s -w"
+ ldflagsStr += " -X github.com/minio/minio/cmd.Version=" + version
+ ldflagsStr += " -X github.com/minio/minio/cmd.CopyrightYear=" + copyrightYear
+ ldflagsStr += " -X github.com/minio/minio/cmd.ReleaseTag=" + releaseTag
+ ldflagsStr += " -X github.com/minio/minio/cmd.CommitID=" + commitID()
+ ldflagsStr += " -X github.com/minio/minio/cmd.ShortCommitID=" + commitID()[:12]
+ ldflagsStr += " -X github.com/minio/minio/cmd.GOPATH=" + os.Getenv("GOPATH")
+ ldflagsStr += " -X github.com/minio/minio/cmd.GOROOT=" + os.Getenv("GOROOT")
+ return ldflagsStr
+}
+
+// genReleaseTag prints release tag to the console for easy git tagging.
+func releaseTag(version string) (string, time.Time) {
+ relPrefix := "DEVELOPMENT"
+ if prefix := os.Getenv("MINIO_RELEASE"); prefix != "" {
+ relPrefix = prefix
+ }
+
+ relSuffix := ""
+ if hotfix := os.Getenv("MINIO_HOTFIX"); hotfix != "" {
+ relSuffix = hotfix
+ }
+
+ relTag := strings.Replace(version, " ", "-", -1)
+ relTag = strings.Replace(relTag, ":", "-", -1)
+ t, err := time.Parse("2006-01-02T15-04-05Z", relTag)
+ if err != nil {
+ panic(err)
+ }
+ relTag = strings.Replace(relTag, ",", "", -1)
+ relTag = relPrefix + "." + relTag
+ if relSuffix != "" {
+ relTag += "." + relSuffix
+ }
+
+ return relTag, t
+}
+
+// commitID returns the abbreviated commit-id hash of the last commit.
+func commitID() string {
+ // git log --format="%H" -n1
+ var (
+ commit []byte
+ err error
+ )
+ cmdName := "git"
+ cmdArgs := []string{"log", "--format=%H", "-n1"}
+ if commit, err = exec.Command(cmdName, cmdArgs...).Output(); err != nil {
+ fmt.Fprintln(os.Stderr, "Error generating git commit-id: ", err)
+ os.Exit(1)
+ }
+
+ return strings.TrimSpace(string(commit))
+}
+
+func commitTime() time.Time {
+ // git log --format=%cD -n1
+ var (
+ commitUnix []byte
+ err error
+ )
+ cmdName := "git"
+ cmdArgs := []string{"log", "--format=%cI", "-n1"}
+ if commitUnix, err = exec.Command(cmdName, cmdArgs...).Output(); err != nil {
+ fmt.Fprintln(os.Stderr, "Error generating git commit-time: ", err)
+ os.Exit(1)
+ }
+
+ t, err := time.Parse(time.RFC3339, strings.TrimSpace(string(commitUnix)))
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "Error generating git commit-time: ", err)
+ os.Exit(1)
+ }
+
+ return t.UTC()
+}
+
+func main() {
+ var version string
+ if len(os.Args) > 1 {
+ version = os.Args[1]
+ } else {
+ version = commitTime().Format(time.RFC3339)
+ }
+
+ fmt.Println(genLDFlags(version))
+}
diff --git a/buildscripts/heal-inconsistent-versions.sh b/buildscripts/heal-inconsistent-versions.sh
new file mode 100755
index 00000000..f4c918a6
--- /dev/null
+++ b/buildscripts/heal-inconsistent-versions.sh
@@ -0,0 +1,92 @@
+#!/bin/bash -e
+
+set -E
+set -o pipefail
+set -x
+
+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
+
+function start_minio_4drive() {
+ 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}/"
+ unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
+ 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" "${WORK_DIR}/disk{1...4}" >"${WORK_DIR}/server1.log" 2>&1 &
+ pid=$!
+ disown $pid
+ sleep 5
+
+ 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
+
+ "${PWD}/mc" mb --with-versioning minio/bucket
+
+ for i in $(seq 1 4); do
+ "${PWD}/mc" cp /etc/hosts minio/bucket/testobj
+
+ sudo chown -R root. "${WORK_DIR}/disk${i}"
+
+ "${PWD}/mc" cp /etc/hosts minio/bucket/testobj
+
+ sudo chown -R ${USER}. "${WORK_DIR}/disk${i}"
+ done
+
+ for vid in $("${PWD}/mc" ls --json --versions minio/bucket/testobj | jq -r .versionId); do
+ "${PWD}/mc" cat --vid "${vid}" minio/bucket/testobj | md5sum
+ done
+
+ pkill minio
+ sleep 3
+}
+
+function main() {
+ start_port=$(shuf -i 10000-65000 -n 1)
+
+ start_minio_4drive ${start_port}
+}
+
+function purge() {
+ rm -rf "$1"
+}
+
+(main "$@")
+rv=$?
+purge "$WORK_DIR"
+exit "$rv"
diff --git a/buildscripts/heal-manual.go b/buildscripts/heal-manual.go
new file mode 100644
index 00000000..27c4183f
--- /dev/null
+++ b/buildscripts/heal-manual.go
@@ -0,0 +1,86 @@
+//go:build ignore
+// +build ignore
+
+//
+// MinIO Object Storage (c) 2022 MinIO, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log"
+ "os"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+)
+
+func main() {
+ // Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY are
+ // dummy values, please replace them with original values.
+
+ // API requests are secure (HTTPS) if secure=true and insecure (HTTP) otherwise.
+ // New returns an MinIO Admin client object.
+ madmClnt, err := madmin.New(os.Args[1], os.Args[2], os.Args[3], false)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ opts := madmin.HealOpts{
+ Recursive: true, // recursively heal all objects at 'prefix'
+ Remove: true, // remove content that has lost quorum and not recoverable
+ ScanMode: madmin.HealNormalScan, // by default do not do 'deep' scanning
+ }
+
+ start, _, err := madmClnt.Heal(context.Background(), "healing-rewrite-bucket", "", opts, "", false, false)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ fmt.Println("Healstart sequence ===")
+ enc := json.NewEncoder(os.Stdout)
+ if err = enc.Encode(&start); err != nil {
+ log.Fatalln(err)
+ }
+
+ fmt.Println()
+ for {
+ _, status, err := madmClnt.Heal(context.Background(), "healing-rewrite-bucket", "", opts, start.ClientToken, false, false)
+ if status.Summary == "finished" {
+ fmt.Println("Healstatus on items ===")
+ for _, item := range status.Items {
+ if err = enc.Encode(&item); err != nil {
+ log.Fatalln(err)
+ }
+ }
+ break
+ }
+ if status.Summary == "stopped" {
+ fmt.Println("Healstatus on items ===")
+ fmt.Println("Heal failed with", status.FailureDetail)
+ break
+ }
+
+ for _, item := range status.Items {
+ if err = enc.Encode(&item); err != nil {
+ log.Fatalln(err)
+ }
+ }
+
+ time.Sleep(time.Second)
+ }
+}
diff --git a/buildscripts/minio-iam-ldap-upgrade-import-test.sh b/buildscripts/minio-iam-ldap-upgrade-import-test.sh
new file mode 100755
index 00000000..e7da2b10
--- /dev/null
+++ b/buildscripts/minio-iam-ldap-upgrade-import-test.sh
@@ -0,0 +1,127 @@
+#!/bin/bash
+
+# This script is used to test the migration of IAM content from old minio
+# instance to new minio instance.
+#
+# To run it locally, start the LDAP server in github.com/minio/minio-iam-testing
+# repo (e.g. make podman-run), and then run this script.
+#
+# This script assumes that LDAP server is at:
+#
+# `localhost:389`
+#
+# if this is not the case, set the environment variable
+# `_MINIO_LDAP_TEST_SERVER`.
+
+OLD_VERSION=RELEASE.2024-03-26T22-10-45Z
+OLD_BINARY_LINK=https://dl.min.io/server/minio/release/linux-amd64/archive/minio.${OLD_VERSION}
+
+__init__() {
+ if which curl &>/dev/null; then
+ echo "curl is already installed"
+ else
+ echo "Installing curl:"
+ sudo apt install curl -y
+ fi
+
+ export GOPATH=/tmp/gopath
+ export PATH="${PATH}":"${GOPATH}"/bin
+
+ if which mc &>/dev/null; then
+ echo "mc is already installed"
+ else
+ echo "Installing mc:"
+ go install github.com/minio/mc@latest
+ fi
+
+ if [ ! -x ./minio.${OLD_VERSION} ]; then
+ echo "Downloading minio.${OLD_VERSION} binary"
+ curl -o minio.${OLD_VERSION} ${OLD_BINARY_LINK}
+ chmod +x minio.${OLD_VERSION}
+ fi
+
+ if [ -z "$_MINIO_LDAP_TEST_SERVER" ]; then
+ export _MINIO_LDAP_TEST_SERVER=localhost:389
+ echo "Using default LDAP endpoint: $_MINIO_LDAP_TEST_SERVER"
+ fi
+
+ rm -rf /tmp/data
+}
+
+create_iam_content_in_old_minio() {
+ echo "Creating IAM content in old minio instance."
+
+ MINIO_CI_CD=1 ./minio.${OLD_VERSION} server /tmp/data/{1...4} &
+ sleep 5
+
+ set -x
+ mc alias set old-minio http://localhost:9000 minioadmin minioadmin
+ mc ready old-minio
+ mc idp ldap add old-minio \
+ server_addr=localhost:389 \
+ server_insecure=on \
+ lookup_bind_dn=cn=admin,dc=min,dc=io \
+ lookup_bind_password=admin \
+ user_dn_search_base_dn=dc=min,dc=io \
+ user_dn_search_filter="(uid=%s)" \
+ group_search_base_dn=ou=swengg,dc=min,dc=io \
+ group_search_filter="(&(objectclass=groupOfNames)(member=%d))"
+ mc admin service restart old-minio
+
+ mc idp ldap policy attach old-minio readwrite --user=UID=dillon,ou=people,ou=swengg,dc=min,dc=io
+ mc idp ldap policy attach old-minio readwrite --group=CN=project.c,ou=groups,ou=swengg,dc=min,dc=io
+
+ mc idp ldap policy entities old-minio
+
+ mc admin cluster iam export old-minio
+ set +x
+
+ mc admin service stop old-minio
+}
+
+import_iam_content_in_new_minio() {
+ echo "Importing IAM content in new minio instance."
+ # Assume current minio binary exists.
+ MINIO_CI_CD=1 ./minio server /tmp/data/{1...4} &
+ sleep 5
+
+ set -x
+ mc alias set new-minio http://localhost:9000 minioadmin minioadmin
+ echo "BEFORE IMPORT mappings:"
+ mc ready new-minio
+ mc idp ldap policy entities new-minio
+ mc admin cluster iam import new-minio ./old-minio-iam-info.zip
+ echo "AFTER IMPORT mappings:"
+ mc idp ldap policy entities new-minio
+ set +x
+
+ # mc admin service stop new-minio
+}
+
+verify_iam_content_in_new_minio() {
+ output=$(mc idp ldap policy entities new-minio --json)
+
+ groups=$(echo "$output" | jq -r '.result.policyMappings[] | select(.policy == "readwrite") | .groups[]')
+ if [ "$groups" != "cn=project.c,ou=groups,ou=swengg,dc=min,dc=io" ]; then
+ echo "Failed to verify groups: $groups"
+ exit 1
+ fi
+
+ users=$(echo "$output" | jq -r '.result.policyMappings[] | select(.policy == "readwrite") | .users[]')
+ if [ "$users" != "uid=dillon,ou=people,ou=swengg,dc=min,dc=io" ]; then
+ echo "Failed to verify users: $users"
+ exit 1
+ fi
+
+ mc admin service stop new-minio
+}
+
+main() {
+ create_iam_content_in_old_minio
+
+ import_iam_content_in_new_minio
+
+ verify_iam_content_in_new_minio
+}
+
+(__init__ "$@" && main "$@")
diff --git a/buildscripts/minio-upgrade.sh b/buildscripts/minio-upgrade.sh
new file mode 100755
index 00000000..deaaf460
--- /dev/null
+++ b/buildscripts/minio-upgrade.sh
@@ -0,0 +1,113 @@
+#!/bin/bash
+
+trap 'cleanup $LINENO' ERR
+
+# shellcheck disable=SC2120
+cleanup() {
+ MINIO_VERSION=dev /tmp/gopath/bin/docker-compose \
+ -f "buildscripts/upgrade-tests/compose.yml" \
+ down || true
+
+ MINIO_VERSION=dev /tmp/gopath/bin/docker-compose \
+ -f "buildscripts/upgrade-tests/compose.yml" \
+ rm || true
+
+ for volume in $(docker volume ls -q | grep upgrade); do
+ docker volume rm ${volume} || true
+ done
+
+ docker volume prune -f
+ docker system prune -f || true
+ docker volume prune -f || true
+ docker volume rm $(docker volume ls -q -f dangling=true) || true
+}
+
+verify_checksum_after_heal() {
+ local sum1
+ sum1=$(curl -s "$2" | sha256sum)
+ mc admin heal --json -r "$1" >/dev/null # test after healing
+ local sum1_heal
+ sum1_heal=$(curl -s "$2" | sha256sum)
+
+ if [ "${sum1_heal}" != "${sum1}" ]; then
+ echo "mismatch expected ${sum1_heal}, got ${sum1}"
+ exit 1
+ fi
+}
+
+verify_checksum_mc() {
+ local expected
+ expected=$(mc cat "$1" | sha256sum)
+ local got
+ got=$(mc cat "$2" | sha256sum)
+
+ if [ "${expected}" != "${got}" ]; then
+ echo "mismatch - expected ${expected}, got ${got}"
+ exit 1
+ fi
+ echo "matches - ${expected}, got ${got}"
+}
+
+add_alias() {
+ for i in $(seq 1 4); do
+ echo "... attempting to add alias $i"
+ until (mc alias set minio http://127.0.0.1:9000 minioadmin minioadmin); do
+ echo "...waiting... for 5secs" && sleep 5
+ done
+ done
+
+ echo "Sleeping for nginx"
+ sleep 20
+}
+
+__init__() {
+ sudo apt install curl -y
+ export GOPATH=/tmp/gopath
+ export PATH=${PATH}:${GOPATH}/bin
+
+ go install github.com/minio/mc@latest
+
+ ## this is needed because github actions don't have
+ ## docker-compose on all runners
+ COMPOSE_VERSION=v2.35.1
+ mkdir -p /tmp/gopath/bin/
+ wget -O /tmp/gopath/bin/docker-compose https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-linux-x86_64
+ chmod +x /tmp/gopath/bin/docker-compose
+
+ cleanup
+
+ TAG=minio/minio:dev make docker
+
+ MINIO_VERSION=RELEASE.2019-12-19T22-52-26Z docker-compose \
+ -f "buildscripts/upgrade-tests/compose.yml" \
+ up -d --build
+
+ add_alias
+
+ mc mb minio/minio-test/
+ mc cp ./minio minio/minio-test/to-read/
+ mc cp /etc/hosts minio/minio-test/to-read/hosts
+ mc anonymous set download minio/minio-test
+
+ verify_checksum_mc ./minio minio/minio-test/to-read/minio
+
+ curl -s http://127.0.0.1:9000/minio-test/to-read/hosts | sha256sum
+
+ MINIO_VERSION=dev /tmp/gopath/bin/docker-compose -f "buildscripts/upgrade-tests/compose.yml" stop
+}
+
+main() {
+ MINIO_VERSION=dev /tmp/gopath/bin/docker-compose -f "buildscripts/upgrade-tests/compose.yml" up -d --build
+
+ add_alias
+
+ verify_checksum_after_heal minio/minio-test http://127.0.0.1:9000/minio-test/to-read/hosts
+
+ verify_checksum_mc ./minio minio/minio-test/to-read/minio
+
+ verify_checksum_mc /etc/hosts minio/minio-test/to-read/hosts
+
+ cleanup
+}
+
+(__init__ "$@" && main "$@")
diff --git a/buildscripts/multipart-quorum-test.sh b/buildscripts/multipart-quorum-test.sh
new file mode 100644
index 00000000..f226b0e5
--- /dev/null
+++ b/buildscripts/multipart-quorum-test.sh
@@ -0,0 +1,126 @@
+#!/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 start_minio_10drive() {
+ 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}/"
+ unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
+ 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" "${WORK_DIR}/disk{1...10}" >"${WORK_DIR}/server1.log" 2>&1 &
+ pid=$!
+ disown $pid
+ sleep 5
+
+ 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
+
+ "${PWD}/mc" mb --with-versioning minio/bucket
+
+ export AWS_ACCESS_KEY_ID=minio
+ export AWS_SECRET_ACCESS_KEY=minio123
+ aws --endpoint-url http://localhost:"$start_port" s3api create-multipart-upload --bucket bucket --key obj-1 >upload-id.json
+ uploadId=$(jq -r '.UploadId' upload-id.json)
+
+ truncate -s 5MiB file-5mib
+ for i in {1..2}; do
+ aws --endpoint-url http://localhost:"$start_port" s3api upload-part \
+ --upload-id "$uploadId" --bucket bucket --key obj-1 \
+ --part-number "$i" --body ./file-5mib
+ done
+ for i in {1..6}; do
+ find ${WORK_DIR}/disk${i}/.minio.sys/multipart/ -type f -name "part.1" -delete
+ done
+ cat <parts.json
+{
+ "Parts": [
+ {
+ "PartNumber": 1,
+ "ETag": "5f363e0e58a95f06cbe9bbc662c5dfb6"
+ },
+ {
+ "PartNumber": 2,
+ "ETag": "5f363e0e58a95f06cbe9bbc662c5dfb6"
+ }
+ ]
+}
+EOF
+ err=$(aws --endpoint-url http://localhost:"$start_port" s3api complete-multipart-upload --upload-id "$uploadId" --bucket bucket --key obj-1 --multipart-upload file://./parts.json 2>&1)
+ rv=$?
+ if [ $rv -eq 0 ]; then
+ echo "Failed to receive an error"
+ exit 1
+ fi
+ echo "Received an error during complete-multipart as expected: $err"
+}
+
+function main() {
+ start_port=$(shuf -i 10000-65000 -n 1)
+ start_minio_10drive ${start_port}
+}
+
+main "$@"
diff --git a/buildscripts/race.sh b/buildscripts/race.sh
new file mode 100755
index 00000000..2931342d
--- /dev/null
+++ b/buildscripts/race.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+set -e
+
+export GORACE="history_size=7"
+export MINIO_API_REQUESTS_MAX=10000
+
+for d in $(go list ./...); do
+ CGO_ENABLED=1 go test -v -race --timeout 100m "$d"
+done
diff --git a/buildscripts/resolve-right-versions.sh b/buildscripts/resolve-right-versions.sh
new file mode 100755
index 00000000..afbdbb1c
--- /dev/null
+++ b/buildscripts/resolve-right-versions.sh
@@ -0,0 +1,72 @@
+#!/bin/bash -e
+
+set -E
+set -o pipefail
+set -x
+set -e
+
+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
+
+function start_minio_5drive() {
+ 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}/"
+ unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
+ export MINIO_CI_CD=1
+
+ 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 "$WORK_DIR/mc")
+
+ # remove mc source.
+ purge "${MC_BUILD_DIR}"
+
+ "${WORK_DIR}/mc" cp --quiet -r "buildscripts/cicd-corpus/" "${WORK_DIR}/cicd-corpus/"
+
+ "${MINIO[@]}" --address ":$start_port" "${WORK_DIR}/cicd-corpus/disk{1...5}" >"${WORK_DIR}/server1.log" 2>&1 &
+ pid=$!
+ disown $pid
+ sleep 5
+
+ 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
+
+ "${WORK_DIR}/mc" stat minio/bucket/testobj
+
+ pkill minio
+ sleep 3
+}
+
+function main() {
+ start_port=$(shuf -i 10000-65000 -n 1)
+
+ start_minio_5drive ${start_port}
+}
+
+function purge() {
+ rm -rf "$1"
+}
+
+(main "$@")
+rv=$?
+purge "$WORK_DIR"
+exit "$rv"
diff --git a/buildscripts/rewrite-old-new.sh b/buildscripts/rewrite-old-new.sh
new file mode 100755
index 00000000..3527f3a6
--- /dev/null
+++ b/buildscripts/rewrite-old-new.sh
@@ -0,0 +1,157 @@
+#!/bin/bash -e
+
+set -E
+set -o pipefail
+set -x
+
+WORK_DIR="$PWD/.verify-$RANDOM"
+MINIO_CONFIG_DIR="$WORK_DIR/.minio"
+MINIO_OLD=("$PWD/minio.RELEASE.2020-10-28T08-16-50Z" --config-dir "$MINIO_CONFIG_DIR" server)
+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
+
+function download_old_release() {
+ if [ ! -f minio.RELEASE.2020-10-28T08-16-50Z ]; then
+ curl --silent -O https://dl.minio.io/server/minio/release/linux-amd64/archive/minio.RELEASE.2020-10-28T08-16-50Z
+ chmod a+x minio.RELEASE.2020-10-28T08-16-50Z
+ fi
+}
+
+function verify_rewrite() {
+ start_port=$1
+
+ export MINIO_ACCESS_KEY=minio
+ export MINIO_SECRET_KEY=minio123
+ export MC_HOST_minio="http://minio:minio123@127.0.0.1:${start_port}/"
+ unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
+ export MINIO_CI_CD=1
+
+ 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 "$WORK_DIR/mc")
+
+ # remove mc source.
+ purge "${MC_BUILD_DIR}"
+
+ "${MINIO_OLD[@]}" --address ":$start_port" "${WORK_DIR}/xl{1...16}" >"${WORK_DIR}/server1.log" 2>&1 &
+ pid=$!
+ disown $pid
+
+ "${WORK_DIR}/mc" ready minio/
+
+ 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
+
+ "${WORK_DIR}/mc" mb minio/healing-rewrite-bucket --quiet --with-lock
+ "${WORK_DIR}/mc" cp \
+ buildscripts/verify-build.sh \
+ minio/healing-rewrite-bucket/ \
+ --disable-multipart --quiet
+
+ "${WORK_DIR}/mc" cp \
+ buildscripts/verify-build.sh \
+ minio/healing-rewrite-bucket/ \
+ --disable-multipart --quiet
+
+ "${WORK_DIR}/mc" cp \
+ buildscripts/verify-build.sh \
+ minio/healing-rewrite-bucket/ \
+ --disable-multipart --quiet
+
+ kill ${pid}
+ sleep 3
+
+ "${MINIO[@]}" --address ":$start_port" "${WORK_DIR}/xl{1...16}" >"${WORK_DIR}/server1.log" 2>&1 &
+ pid=$!
+ disown $pid
+
+ "${WORK_DIR}/mc" ready minio/
+
+ 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
+
+ if ! ./s3-check-md5 \
+ -debug \
+ -versions \
+ -access-key minio \
+ -secret-key minio123 \
+ -endpoint "http://127.0.0.1:${start_port}/" 2>&1 | grep INTACT; then
+ echo "server1 log:"
+ cat "${WORK_DIR}/server1.log"
+ echo "FAILED"
+ mkdir -p inspects
+ (
+ cd inspects
+ "${WORK_DIR}/mc" admin inspect minio/healing-rewrite-bucket/verify-build.sh/**
+ )
+
+ "${WORK_DIR}/mc" mb play/inspects
+ "${WORK_DIR}/mc" mirror inspects play/inspects
+
+ purge "$WORK_DIR"
+ exit 1
+ fi
+
+ go run ./buildscripts/heal-manual.go "127.0.0.1:${start_port}" "minio" "minio123"
+ sleep 1
+
+ if ! ./s3-check-md5 \
+ -debug \
+ -versions \
+ -access-key minio \
+ -secret-key minio123 \
+ -endpoint http://127.0.0.1:${start_port}/ 2>&1 | grep INTACT; then
+ echo "server1 log:"
+ cat "${WORK_DIR}/server1.log"
+ echo "FAILED"
+ mkdir -p inspects
+ (
+ cd inspects
+ "${WORK_DIR}/mc" admin inspect minio/healing-rewrite-bucket/verify-build.sh/**
+ )
+
+ "${WORK_DIR}/mc" mb play/inspects
+ "${WORK_DIR}/mc" mirror inspects play/inspects
+
+ purge "$WORK_DIR"
+ exit 1
+ fi
+
+ kill ${pid}
+}
+
+function main() {
+ download_old_release
+
+ start_port=$(shuf -i 10000-65000 -n 1)
+
+ verify_rewrite ${start_port}
+}
+
+function purge() {
+ rm -rf "$1"
+}
+
+(main "$@")
+rv=$?
+purge "$WORK_DIR"
+exit "$rv"
diff --git a/buildscripts/test-timeout.sh b/buildscripts/test-timeout.sh
new file mode 100644
index 00000000..77a248a6
--- /dev/null
+++ b/buildscripts/test-timeout.sh
@@ -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 "$@"
diff --git a/buildscripts/upgrade-tests/compose.yml b/buildscripts/upgrade-tests/compose.yml
new file mode 100644
index 00000000..e820a6c5
--- /dev/null
+++ b/buildscripts/upgrade-tests/compose.yml
@@ -0,0 +1,74 @@
+# Settings and configurations that are common for all containers
+x-minio-common: &minio-common
+ image: minio/minio:${MINIO_VERSION}
+ command: server http://minio{1...4}/data{1...3}
+ env_file:
+ - ./minio.env
+ expose:
+ - "9000"
+ - "9001"
+
+# starts 4 docker containers running minio server instances.
+# using nginx reverse proxy, load balancing, you can access
+# it through port 9000.
+services:
+ minio1:
+ <<: *minio-common
+ hostname: minio1
+ volumes:
+ - data1-1:/data1
+ - data1-2:/data2
+ - data1-3:/data3
+
+ minio2:
+ <<: *minio-common
+ hostname: minio2
+ volumes:
+ - data2-1:/data1
+ - data2-2:/data2
+ - data2-3:/data3
+
+ minio3:
+ <<: *minio-common
+ hostname: minio3
+ volumes:
+ - data3-1:/data1
+ - data3-2:/data2
+ - data3-3:/data3
+
+ minio4:
+ <<: *minio-common
+ hostname: minio4
+ volumes:
+ - data4-1:/data1
+ - data4-2:/data2
+ - data4-3:/data3
+
+ nginx:
+ image: nginx:1.19.2-alpine
+ volumes:
+ - ./nginx.conf:/etc/nginx/nginx.conf:ro
+ ports:
+ - "9000:9000"
+ - "9001:9001"
+ depends_on:
+ - minio1
+ - minio2
+ - minio3
+ - minio4
+
+## By default this config uses default local driver,
+## For custom volumes replace with volume driver configuration.
+volumes:
+ data1-1:
+ data1-2:
+ data1-3:
+ data2-1:
+ data2-2:
+ data2-3:
+ data3-1:
+ data3-2:
+ data3-3:
+ data4-1:
+ data4-2:
+ data4-3:
diff --git a/buildscripts/upgrade-tests/minio.env b/buildscripts/upgrade-tests/minio.env
new file mode 100644
index 00000000..a957766f
--- /dev/null
+++ b/buildscripts/upgrade-tests/minio.env
@@ -0,0 +1,3 @@
+MINIO_ACCESS_KEY=minioadmin
+MINIO_SECRET_KEY=minioadmin
+MINIO_BROWSER=off
diff --git a/buildscripts/upgrade-tests/nginx.conf b/buildscripts/upgrade-tests/nginx.conf
new file mode 100644
index 00000000..0c46dc92
--- /dev/null
+++ b/buildscripts/upgrade-tests/nginx.conf
@@ -0,0 +1,68 @@
+user nginx;
+worker_processes auto;
+
+error_log /var/log/nginx/error.log warn;
+pid /var/run/nginx.pid;
+
+
+events {
+ worker_connections 1024;
+}
+
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ access_log /var/log/nginx/access.log main;
+
+ sendfile on;
+ #tcp_nopush on;
+
+ keepalive_timeout 65;
+
+ #gzip on;
+
+ # include /etc/nginx/conf.d/*.conf;
+
+ upstream minio {
+ server minio1:9000;
+ server minio2:9000;
+ server minio3:9000;
+ server minio4:9000;
+ }
+
+ # main minio
+ server {
+ listen 9000;
+ listen [::]:9000;
+ server_name localhost;
+
+ # To allow special characters in headers
+ ignore_invalid_headers off;
+ # Allow any size file to be uploaded.
+ # Set to a value such as 1000m; to restrict file size to a specific value
+ client_max_body_size 0;
+ # To disable buffering
+ proxy_buffering off;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ proxy_connect_timeout 300;
+ # Default is HTTP/1, keepalive is only enabled in HTTP/1.1
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ chunked_transfer_encoding off;
+
+ proxy_pass http://minio;
+ }
+ }
+}
diff --git a/buildscripts/verify-build.sh b/buildscripts/verify-build.sh
new file mode 100755
index 00000000..e15d6475
--- /dev/null
+++ b/buildscripts/verify-build.sh
@@ -0,0 +1,298 @@
+#!/bin/bash
+#
+
+set -e
+set -E
+set -o pipefail
+
+if [ ! -x "$PWD/minio" ]; then
+ echo "minio executable binary not found in current directory"
+ exit 1
+fi
+
+WORK_DIR="$PWD/.verify-$RANDOM"
+
+export MINT_MODE=core
+export MINT_DATA_DIR="$WORK_DIR/data"
+export SERVER_ENDPOINT="127.0.0.1:9000"
+export MC_HOST_verify="http://minio:minio123@${SERVER_ENDPOINT}/"
+export MC_HOST_verify_ipv6="http://minio:minio123@[::1]:9000/"
+export ACCESS_KEY="minio"
+export SECRET_KEY="minio123"
+export ENABLE_HTTPS=0
+export GO111MODULE=on
+export GOGC=25
+export ENABLE_ADMIN=1
+export MINIO_CI_CD=1
+
+MINIO_CONFIG_DIR="$WORK_DIR/.minio"
+MINIO=("$PWD/minio" --config-dir "$MINIO_CONFIG_DIR")
+
+FILE_1_MB="$MINT_DATA_DIR/datafile-1-MB"
+FILE_65_MB="$MINT_DATA_DIR/datafile-65-MB"
+
+FUNCTIONAL_TESTS="$WORK_DIR/functional-tests.sh"
+
+function start_minio_fs() {
+ export MINIO_ROOT_USER=$ACCESS_KEY
+ export MINIO_ROOT_PASSWORD=$SECRET_KEY
+ "${MINIO[@]}" server "${WORK_DIR}/fs-disk" >"$WORK_DIR/fs-minio.log" 2>&1 &
+
+ "${WORK_DIR}/mc" ready verify
+}
+
+function start_minio_erasure() {
+ "${MINIO[@]}" server "${WORK_DIR}/erasure-disk1" "${WORK_DIR}/erasure-disk2" "${WORK_DIR}/erasure-disk3" "${WORK_DIR}/erasure-disk4" >"$WORK_DIR/erasure-minio.log" 2>&1 &
+
+ "${WORK_DIR}/mc" ready verify
+}
+
+function start_minio_erasure_sets() {
+ export MINIO_ENDPOINTS="${WORK_DIR}/erasure-disk-sets{1...32}"
+ "${MINIO[@]}" server >"$WORK_DIR/erasure-minio-sets.log" 2>&1 &
+
+ "${WORK_DIR}/mc" ready verify
+}
+
+function start_minio_pool_erasure_sets() {
+ export MINIO_ROOT_USER=$ACCESS_KEY
+ export MINIO_ROOT_PASSWORD=$SECRET_KEY
+ export MINIO_ENDPOINTS="http://127.0.0.1:9000${WORK_DIR}/pool-disk-sets{1...4} http://127.0.0.1:9001${WORK_DIR}/pool-disk-sets{5...8}"
+ "${MINIO[@]}" server --address ":9000" >"$WORK_DIR/pool-minio-9000.log" 2>&1 &
+ "${MINIO[@]}" server --address ":9001" >"$WORK_DIR/pool-minio-9001.log" 2>&1 &
+
+ "${WORK_DIR}/mc" ready verify
+}
+
+function start_minio_pool_erasure_sets_ipv6() {
+ export MINIO_ROOT_USER=$ACCESS_KEY
+ export MINIO_ROOT_PASSWORD=$SECRET_KEY
+ export MINIO_ENDPOINTS="http://[::1]:9000${WORK_DIR}/pool-disk-sets-ipv6{1...4} http://[::1]:9001${WORK_DIR}/pool-disk-sets-ipv6{5...8}"
+ "${MINIO[@]}" server --address="[::1]:9000" >"$WORK_DIR/pool-minio-ipv6-9000.log" 2>&1 &
+ "${MINIO[@]}" server --address="[::1]:9001" >"$WORK_DIR/pool-minio-ipv6-9001.log" 2>&1 &
+
+ "${WORK_DIR}/mc" ready verify_ipv6
+}
+
+function start_minio_dist_erasure() {
+ export MINIO_ROOT_USER=$ACCESS_KEY
+ export MINIO_ROOT_PASSWORD=$SECRET_KEY
+ export MINIO_ENDPOINTS="http://127.0.0.1:9000${WORK_DIR}/dist-disk1 http://127.0.0.1:9001${WORK_DIR}/dist-disk2 http://127.0.0.1:9002${WORK_DIR}/dist-disk3 http://127.0.0.1:9003${WORK_DIR}/dist-disk4"
+ for i in $(seq 0 3); do
+ "${MINIO[@]}" server --address ":900${i}" >"$WORK_DIR/dist-minio-900${i}.log" 2>&1 &
+ done
+
+ "${WORK_DIR}/mc" ready verify
+}
+
+function run_test_fs() {
+ start_minio_fs
+
+ (cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
+ rv=$?
+
+ pkill minio
+ sleep 3
+
+ if [ "$rv" -ne 0 ]; then
+ cat "$WORK_DIR/fs-minio.log"
+ fi
+ rm -f "$WORK_DIR/fs-minio.log"
+
+ return "$rv"
+}
+
+function run_test_erasure_sets() {
+ start_minio_erasure_sets
+
+ (cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
+ rv=$?
+
+ pkill minio
+ sleep 3
+
+ if [ "$rv" -ne 0 ]; then
+ cat "$WORK_DIR/erasure-minio-sets.log"
+ fi
+ rm -f "$WORK_DIR/erasure-minio-sets.log"
+
+ return "$rv"
+}
+
+function run_test_pool_erasure_sets() {
+ start_minio_pool_erasure_sets
+
+ (cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
+ rv=$?
+
+ pkill minio
+ sleep 3
+
+ if [ "$rv" -ne 0 ]; then
+ for i in $(seq 0 1); do
+ echo "server$i log:"
+ cat "$WORK_DIR/pool-minio-900$i.log"
+ done
+ fi
+
+ for i in $(seq 0 1); do
+ rm -f "$WORK_DIR/pool-minio-900$i.log"
+ done
+
+ return "$rv"
+}
+
+function run_test_pool_erasure_sets_ipv6() {
+ start_minio_pool_erasure_sets_ipv6
+
+ export SERVER_ENDPOINT="[::1]:9000"
+
+ (cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
+ rv=$?
+
+ pkill minio
+ sleep 3
+
+ if [ "$rv" -ne 0 ]; then
+ for i in $(seq 0 1); do
+ echo "server$i log:"
+ cat "$WORK_DIR/pool-minio-ipv6-900$i.log"
+ done
+ fi
+
+ for i in $(seq 0 1); do
+ rm -f "$WORK_DIR/pool-minio-ipv6-900$i.log"
+ done
+
+ return "$rv"
+}
+
+function run_test_erasure() {
+ start_minio_erasure
+
+ (cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
+ rv=$?
+
+ pkill minio
+ sleep 3
+
+ if [ "$rv" -ne 0 ]; then
+ cat "$WORK_DIR/erasure-minio.log"
+ fi
+ rm -f "$WORK_DIR/erasure-minio.log"
+
+ return "$rv"
+}
+
+function run_test_dist_erasure() {
+ start_minio_dist_erasure
+
+ (cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
+ rv=$?
+
+ pkill minio
+ sleep 3
+
+ if [ "$rv" -ne 0 ]; then
+ echo "server1 log:"
+ cat "$WORK_DIR/dist-minio-9000.log"
+ echo "server2 log:"
+ cat "$WORK_DIR/dist-minio-9001.log"
+ echo "server3 log:"
+ cat "$WORK_DIR/dist-minio-9002.log"
+ echo "server4 log:"
+ cat "$WORK_DIR/dist-minio-9003.log"
+ fi
+
+ rm -f "$WORK_DIR/dist-minio-9000.log" "$WORK_DIR/dist-minio-9001.log" "$WORK_DIR/dist-minio-9002.log" "$WORK_DIR/dist-minio-9003.log"
+
+ return "$rv"
+}
+
+function purge() {
+ rm -rf "$1"
+}
+
+function __init__() {
+ echo "Initializing environment"
+ mkdir -p "$WORK_DIR"
+ mkdir -p "$MINIO_CONFIG_DIR"
+ mkdir -p "$MINT_DATA_DIR"
+
+ 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 "${WORK_DIR}/mc")
+
+ # remove mc source.
+ purge "${MC_BUILD_DIR}"
+
+ shred -n 1 -s 1M - 1>"$FILE_1_MB" 2>/dev/null
+ shred -n 1 -s 65M - 1>"$FILE_65_MB" 2>/dev/null
+
+ ## version is purposefully set to '3' for minio to migrate configuration file
+ echo '{"version": "3", "credential": {"accessKey": "minio", "secretKey": "minio123"}, "region": "us-east-1"}' >"$MINIO_CONFIG_DIR/config.json"
+
+ if ! wget -q -O "$FUNCTIONAL_TESTS" https://raw.githubusercontent.com/minio/mc/master/functional-tests.sh; then
+ echo "failed to download https://raw.githubusercontent.com/minio/mc/master/functional-tests.sh"
+ exit 1
+ fi
+
+ sed -i 's|-sS|-sSg|g' "$FUNCTIONAL_TESTS"
+ chmod a+x "$FUNCTIONAL_TESTS"
+}
+
+function main() {
+ echo "Testing in FS setup"
+ if ! run_test_fs; then
+ echo "FAILED"
+ purge "$WORK_DIR"
+ exit 1
+ fi
+
+ echo "Testing in Erasure setup"
+ if ! run_test_erasure; then
+ echo "FAILED"
+ purge "$WORK_DIR"
+ exit 1
+ fi
+
+ echo "Testing in Distributed Erasure setup"
+ if ! run_test_dist_erasure; then
+ echo "FAILED"
+ purge "$WORK_DIR"
+ exit 1
+ fi
+
+ echo "Testing in Erasure setup as sets"
+ if ! run_test_erasure_sets; then
+ echo "FAILED"
+ purge "$WORK_DIR"
+ exit 1
+ fi
+
+ echo "Testing in Distributed Eraure expanded setup"
+ if ! run_test_pool_erasure_sets; then
+ echo "FAILED"
+ purge "$WORK_DIR"
+ exit 1
+ fi
+
+ echo "Testing in Distributed Erasure expanded setup with ipv6"
+ if ! run_test_pool_erasure_sets_ipv6; then
+ echo "FAILED"
+ purge "$WORK_DIR"
+ exit 1
+ fi
+
+ purge "$WORK_DIR"
+}
+
+(__init__ "$@" && main "$@")
+rv=$?
+purge "$WORK_DIR"
+exit "$rv"
diff --git a/buildscripts/verify-healing-empty-erasure-set.sh b/buildscripts/verify-healing-empty-erasure-set.sh
new file mode 100755
index 00000000..ddbbc1c0
--- /dev/null
+++ b/buildscripts/verify-healing-empty-erasure-set.sh
@@ -0,0 +1,151 @@
+#!/bin/bash -e
+#
+
+set -E
+set -o pipefail
+
+if [ ! -x "$PWD/minio" ]; then
+ echo "minio executable binary not found in current directory"
+ exit 1
+fi
+
+WORK_DIR="$PWD/.verify-$RANDOM"
+MINIO_CONFIG_DIR="$WORK_DIR/.minio"
+MINIO=("$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" server)
+
+function start_minio_3_node() {
+ export MINIO_ROOT_USER=minio
+ export MINIO_ROOT_PASSWORD=minio123
+ export MINIO_ERASURE_SET_DRIVE_COUNT=6
+ export MINIO_CI_CD=1
+
+ start_port=$1
+ args=""
+ for i in $(seq 1 3); do
+ args="$args http://127.0.0.1:$((start_port + i))${WORK_DIR}/$i/1/ http://127.0.0.1:$((start_port + i))${WORK_DIR}/$i/2/ http://127.0.0.1:$((start_port + i))${WORK_DIR}/$i/3/ http://127.0.0.1:$((start_port + i))${WORK_DIR}/$i/4/ http://127.0.0.1:$((start_port + i))${WORK_DIR}/$i/5/ http://127.0.0.1:$((start_port + i))${WORK_DIR}/$i/6/"
+ done
+
+ "${MINIO[@]}" --address ":$((start_port + 1))" $args >"${WORK_DIR}/dist-minio-server1.log" 2>&1 &
+ pid1=$!
+ disown ${pid1}
+
+ "${MINIO[@]}" --address ":$((start_port + 2))" $args >"${WORK_DIR}/dist-minio-server2.log" 2>&1 &
+ pid2=$!
+ disown $pid2
+
+ "${MINIO[@]}" --address ":$((start_port + 3))" $args >"${WORK_DIR}/dist-minio-server3.log" 2>&1 &
+ pid3=$!
+ disown $pid3
+
+ export MC_HOST_myminio="http://minio:minio123@127.0.0.1:$((start_port + 1))"
+
+ timeout 15m /tmp/mc ready myminio || fail
+
+ # Wait for all drives to be online and formatted
+ while [ $(/tmp/mc admin info --json myminio | jq '.info.servers[].drives[].state | select(. != "ok")' | wc -l) -gt 0 ]; do sleep 1; done
+ # Wait for all drives to be healed
+ while [ $(/tmp/mc admin info --json myminio | jq '.info.servers[].drives[].healing | select(. != null) | select(. == true)' | wc -l) -gt 0 ]; do sleep 1; done
+
+ # Wait for Status: in MinIO output
+ while true; do
+ rv=$(check_online)
+ if [ "$rv" != "1" ]; then
+ # success
+ break
+ fi
+
+ # Check if we should retry
+ retry=$((retry + 1))
+ if [ $retry -le 20 ]; then
+ sleep 5
+ continue
+ fi
+
+ # Failure
+ fail
+ done
+
+ if ! ps -p $pid1 1>&2 >/dev/null; then
+ echo "minio-server-1 is not running." && fail
+ fi
+
+ if ! ps -p $pid2 1>&2 >/dev/null; then
+ echo "minio-server-2 is not running." && fail
+ fi
+
+ if ! ps -p $pid3 1>&2 >/dev/null; then
+ echo "minio-server-3 is not running." && fail
+ fi
+
+ if ! pkill minio; then
+ fail
+ fi
+
+ sleep 1
+ if pgrep minio; then
+ # forcibly killing, to proceed further properly.
+ if ! pkill -9 minio; then
+ echo "no minio process running anymore, proceed."
+ fi
+ fi
+}
+
+function fail() {
+ for i in $(seq 1 3); do
+ echo "server$i log:"
+ cat "${WORK_DIR}/dist-minio-server$i.log"
+ done
+ echo "FAILED"
+ purge "$WORK_DIR"
+ exit 1
+}
+
+function check_online() {
+ if ! grep -q 'API:' ${WORK_DIR}/dist-minio-*.log; then
+ echo "1"
+ fi
+}
+
+function purge() {
+ echo rm -rf "$1"
+}
+
+function __init__() {
+ echo "Initializing environment"
+ mkdir -p "$WORK_DIR"
+ mkdir -p "$MINIO_CONFIG_DIR"
+
+ ## version is purposefully set to '3' for minio to migrate configuration file
+ echo '{"version": "3", "credential": {"accessKey": "minio", "secretKey": "minio123"}, "region": "us-east-1"}' >"$MINIO_CONFIG_DIR/config.json"
+
+ if [ ! -f /tmp/mc ]; then
+ wget --quiet -O /tmp/mc https://dl.minio.io/client/mc/release/linux-amd64/mc &&
+ chmod +x /tmp/mc
+ fi
+}
+
+function perform_test() {
+ start_minio_3_node $2
+
+ echo "Testing Distributed Erasure setup healing of drives"
+ echo "Remove the contents of the disks belonging to '${1}' erasure set"
+
+ rm -rf ${WORK_DIR}/${1}/*/
+
+ set -x
+ start_minio_3_node $2
+}
+
+function main() {
+ # use same ports for all tests
+ start_port=$(shuf -i 10000-65000 -n 1)
+
+ perform_test "2" ${start_port}
+ perform_test "1" ${start_port}
+ perform_test "3" ${start_port}
+}
+
+(__init__ "$@" && main "$@")
+rv=$?
+purge "$WORK_DIR"
+exit "$rv"
diff --git a/buildscripts/verify-healing-with-root-disks.sh b/buildscripts/verify-healing-with-root-disks.sh
new file mode 100755
index 00000000..6166eddd
--- /dev/null
+++ b/buildscripts/verify-healing-with-root-disks.sh
@@ -0,0 +1,96 @@
+#!/bin/bash -e
+
+set -E
+set -o pipefail
+set -x
+
+if [ ! -x "$PWD/minio" ]; then
+ echo "minio executable binary not found in current directory"
+ exit 1
+fi
+
+WORK_DIR="$(mktemp -d)"
+MINIO_CONFIG_DIR="$WORK_DIR/.minio"
+MINIO=("$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" server)
+
+function start_minio() {
+ start_port=$1
+
+ export MINIO_ROOT_USER=minio
+ export MINIO_ROOT_PASSWORD=minio123
+ unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
+ unset MINIO_CI_CD
+ unset CI
+
+ args=()
+ for i in $(seq 1 4); do
+ args+=("http://localhost:$((start_port + i))${WORK_DIR}/mnt/disk$i/ ")
+ done
+
+ for i in $(seq 1 4); do
+ "${MINIO[@]}" --address ":$((start_port + i))" ${args[@]} 2>&1 >"${WORK_DIR}/server$i.log" &
+ done
+
+ # Wait until all nodes return 403
+ for i in $(seq 1 4); do
+ while [ "$(curl -m 1 -s -o /dev/null -w "%{http_code}" http://localhost:$((start_port + i)))" -ne "403" ]; do
+ echo -n "."
+ sleep 1
+ done
+ done
+
+}
+
+# Prepare fake disks with losetup
+function prepare_block_devices() {
+ set -e
+ mkdir -p ${WORK_DIR}/disks/ ${WORK_DIR}/mnt/
+ sudo modprobe loop
+ for i in 1 2 3 4; do
+ dd if=/dev/zero of=${WORK_DIR}/disks/img.${i} bs=1M count=2000
+ device=$(sudo losetup --find --show ${WORK_DIR}/disks/img.${i})
+ sudo mkfs.ext4 -F ${device}
+ mkdir -p ${WORK_DIR}/mnt/disk${i}/
+ sudo mount ${device} ${WORK_DIR}/mnt/disk${i}/
+ sudo chown "$(id -u):$(id -g)" ${device} ${WORK_DIR}/mnt/disk${i}/
+ done
+ set +e
+}
+
+# Start a distributed MinIO setup, unmount one disk and check if it is formatted
+function main() {
+ start_port=$(shuf -i 10000-65000 -n 1)
+ start_minio ${start_port}
+
+ # Unmount the disk, after the unmount the device id
+ # /tmp/xxx/mnt/disk4 will be the same as '/' and it
+ # will be detected as root disk
+ while [ "$u" != "0" ]; do
+ sudo umount ${WORK_DIR}/mnt/disk4/
+ u=$?
+ sleep 1
+ done
+
+ # Wait until MinIO self heal kicks in
+ sleep 60
+
+ if [ -f ${WORK_DIR}/mnt/disk4/.minio.sys/format.json ]; then
+ echo "A root disk is formatted unexpectedely"
+ cat "${WORK_DIR}/server4.log"
+ exit -1
+ fi
+}
+
+function cleanup() {
+ pkill minio
+ sudo umount ${WORK_DIR}/mnt/disk{1..3}/
+ sudo rm /dev/minio-loopdisk*
+ rm -rf "$WORK_DIR"
+}
+
+(prepare_block_devices)
+(main "$@")
+rv=$?
+
+cleanup
+exit "$rv"
diff --git a/buildscripts/verify-healing.sh b/buildscripts/verify-healing.sh
new file mode 100755
index 00000000..66778c17
--- /dev/null
+++ b/buildscripts/verify-healing.sh
@@ -0,0 +1,167 @@
+#!/bin/bash -e
+#
+
+set -E
+set -o pipefail
+
+if [ ! -x "$PWD/minio" ]; then
+ echo "minio executable binary not found in current directory"
+ exit 1
+fi
+
+WORK_DIR="$PWD/.verify-$RANDOM"
+MINIO_CONFIG_DIR="$WORK_DIR/.minio"
+MINIO=("$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" server)
+GOPATH=/tmp/gopath
+
+function start_minio_3_node() {
+ for i in $(seq 1 3); do
+ rm "${WORK_DIR}/dist-minio-server$i.log"
+ done
+
+ export MINIO_ROOT_USER=minio
+ export MINIO_ROOT_PASSWORD=minio123
+ export MINIO_ERASURE_SET_DRIVE_COUNT=6
+ export MINIO_CI_CD=1
+
+ first_time=$(find ${WORK_DIR}/ | grep format.json | wc -l)
+
+ start_port=$1
+ args=""
+ for d in $(seq 1 3 5); do
+ args="$args http://127.0.0.1:$((start_port + 1))${WORK_DIR}/1/${d}/ http://127.0.0.1:$((start_port + 2))${WORK_DIR}/2/${d}/ http://127.0.0.1:$((start_port + 3))${WORK_DIR}/3/${d}/ "
+ d=$((d + 1))
+ args="$args http://127.0.0.1:$((start_port + 1))${WORK_DIR}/1/${d}/ http://127.0.0.1:$((start_port + 2))${WORK_DIR}/2/${d}/ http://127.0.0.1:$((start_port + 3))${WORK_DIR}/3/${d}/ "
+ done
+
+ "${MINIO[@]}" --address ":$((start_port + 1))" $args >"${WORK_DIR}/dist-minio-server1.log" 2>&1 &
+ pid1=$!
+ disown ${pid1}
+
+ "${MINIO[@]}" --address ":$((start_port + 2))" $args >"${WORK_DIR}/dist-minio-server2.log" 2>&1 &
+ pid2=$!
+ disown $pid2
+
+ "${MINIO[@]}" --address ":$((start_port + 3))" $args >"${WORK_DIR}/dist-minio-server3.log" 2>&1 &
+ pid3=$!
+ disown $pid3
+
+ export MC_HOST_myminio="http://minio:minio123@127.0.0.1:$((start_port + 1))"
+ timeout 15m /tmp/mc ready myminio || fail
+
+ [ ${first_time} -eq 0 ] && upload_objects
+ [ ${first_time} -ne 0 ] && sleep 120
+
+ if ! ps -p $pid1 1>&2 >/dev/null; then
+ echo "minio server 1 is not running" && fail
+ fi
+
+ if ! ps -p $pid2 1>&2 >/dev/null; then
+ echo "minio server 2 is not running" && fail
+ fi
+
+ if ! ps -p $pid3 1>&2 >/dev/null; then
+ echo "minio server 3 is not running" && fail
+ fi
+
+ if ! pkill minio; then
+ fail
+ fi
+
+ sleep 1
+ if pgrep minio; then
+ # forcibly killing, to proceed further properly.
+ if ! pkill -9 minio; then
+ echo "no minio process running anymore, proceed."
+ fi
+ fi
+}
+
+function check_heal() {
+ if ! grep -q 'API:' ${WORK_DIR}/dist-minio-*.log; then
+ return 1
+ fi
+
+ for ((i = 0; i < 20; i++)); do
+ test -f ${WORK_DIR}/$1/1/.minio.sys/format.json
+ v1=$?
+ nextInES=$(($1 + 1)) && [ $nextInES -gt 3 ] && nextInES=1
+ foundFiles1=$(find ${WORK_DIR}/$1/1/ | grep -v .minio.sys | grep xl.meta | wc -l)
+ foundFiles2=$(find ${WORK_DIR}/$nextInES/1/ | grep -v .minio.sys | grep xl.meta | wc -l)
+ test $foundFiles1 -eq $foundFiles2
+ v2=$?
+ [ $v1 == 0 -a $v2 == 0 ] && return 0
+ sleep 10
+ done
+ return 1
+}
+
+function purge() {
+ rm -rf "$1"
+}
+
+function fail() {
+ for i in $(seq 1 3); do
+ echo "server$i log:"
+ cat "${WORK_DIR}/dist-minio-server$i.log"
+ done
+ pkill -9 minio
+ echo "FAILED"
+ purge "$WORK_DIR"
+ exit 1
+}
+
+function __init__() {
+ echo "Initializing environment"
+ mkdir -p "$WORK_DIR"
+ mkdir -p "$MINIO_CONFIG_DIR"
+
+ ## version is purposefully set to '3' for minio to migrate configuration file
+ echo '{"version": "3", "credential": {"accessKey": "minio", "secretKey": "minio123"}, "region": "us-east-1"}' >"$MINIO_CONFIG_DIR/config.json"
+
+ if [ ! -f /tmp/mc ]; then
+ wget --quiet -O /tmp/mc https://dl.minio.io/client/mc/release/linux-amd64/mc &&
+ chmod +x /tmp/mc
+ fi
+}
+
+function upload_objects() {
+ /tmp/mc mb myminio/testbucket/
+ for ((i = 0; i < 20; i++)); do
+ echo "my content" | /tmp/mc pipe myminio/testbucket/file-$i
+ done
+}
+
+function perform_test() {
+ start_port=$2
+
+ start_minio_3_node $start_port
+
+ echo "Testing Distributed Erasure setup healing of drives"
+ echo "Remove the contents of the disks belonging to '${1}' node"
+
+ rm -rf ${WORK_DIR}/${1}/*/
+
+ set -x
+ start_minio_3_node $start_port
+
+ check_heal ${1}
+ rv=$?
+ if [ "$rv" == "1" ]; then
+ fail
+ fi
+}
+
+function main() {
+ # use same ports for all tests
+ start_port=$(shuf -i 10000-65000 -n 1)
+
+ perform_test "2" ${start_port}
+ perform_test "1" ${start_port}
+ perform_test "3" ${start_port}
+}
+
+(__init__ "$@" && main "$@")
+rv=$?
+purge "$WORK_DIR"
+exit "$rv"
diff --git a/cmd/acl-handlers.go b/cmd/acl-handlers.go
new file mode 100644
index 00000000..eb1f3c1e
--- /dev/null
+++ b/cmd/acl-handlers.go
@@ -0,0 +1,280 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "encoding/xml"
+ "io"
+ "net/http"
+
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/mux"
+ "github.com/minio/pkg/v3/policy"
+)
+
+// Data types used for returning dummy access control
+// policy XML, these variables shouldn't be used elsewhere
+// they are only defined to be used in this file alone.
+type grantee struct {
+ XMLNS string `xml:"xmlns:xsi,attr"`
+ XMLXSI string `xml:"xsi:type,attr"`
+ Type string `xml:"Type"`
+ ID string `xml:"ID,omitempty"`
+ DisplayName string `xml:"DisplayName,omitempty"`
+ URI string `xml:"URI,omitempty"`
+}
+
+type grant struct {
+ Grantee grantee `xml:"Grantee"`
+ Permission string `xml:"Permission"`
+}
+
+type accessControlPolicy struct {
+ XMLName xml.Name `xml:"AccessControlPolicy"`
+ Owner Owner `xml:"Owner"`
+ AccessControlList struct {
+ Grants []grant `xml:"Grant"`
+ } `xml:"AccessControlList"`
+}
+
+// PutBucketACLHandler - PUT Bucket ACL
+// -----------------
+// This operation uses the ACL subresource
+// to set ACL for a bucket, this is a dummy call
+// only responds success if the ACL is private.
+func (api objectAPIHandlers) PutBucketACLHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "PutBucketACL")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ // Allow putBucketACL if policy action is set, since this is a dummy call
+ // we are simply re-purposing the bucketPolicyAction.
+ if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketPolicyAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Before proceeding validate if bucket exists.
+ _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ aclHeader := r.Header.Get(xhttp.AmzACL)
+ if aclHeader == "" {
+ acl := &accessControlPolicy{}
+ if err = xmlDecoder(r.Body, acl, r.ContentLength); err != nil {
+ if terr, ok := err.(*xml.SyntaxError); ok && terr.Msg == io.EOF.Error() {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedXML),
+ r.URL)
+ return
+ }
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if len(acl.AccessControlList.Grants) == 0 {
+ writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL)
+ return
+ }
+
+ if acl.AccessControlList.Grants[0].Permission != "FULL_CONTROL" {
+ writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL)
+ return
+ }
+ }
+
+ if aclHeader != "" && aclHeader != "private" {
+ writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL)
+ return
+ }
+}
+
+// GetBucketACLHandler - GET Bucket ACL
+// -----------------
+// This operation uses the ACL
+// subresource to return the ACL of a specified bucket.
+func (api objectAPIHandlers) GetBucketACLHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetBucketACL")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ // Allow getBucketACL if policy action is set, since this is a dummy call
+ // we are simply re-purposing the bucketPolicyAction.
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketPolicyAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Before proceeding validate if bucket exists.
+ _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ acl := &accessControlPolicy{}
+ acl.AccessControlList.Grants = append(acl.AccessControlList.Grants, grant{
+ Grantee: grantee{
+ XMLNS: "http://www.w3.org/2001/XMLSchema-instance",
+ XMLXSI: "CanonicalUser",
+ Type: "CanonicalUser",
+ },
+ Permission: "FULL_CONTROL",
+ })
+
+ if err := xml.NewEncoder(w).Encode(acl); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+}
+
+// PutObjectACLHandler - PUT Object ACL
+// -----------------
+// This operation uses the ACL subresource
+// to set ACL for a bucket, this is a dummy call
+// only responds success if the ACL is private.
+func (api objectAPIHandlers) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "PutObjectACL")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+ object, err := unescapePath(vars["object"])
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ // Allow putObjectACL if policy action is set, since this is a dummy call
+ // we are simply re-purposing the bucketPolicyAction.
+ if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketPolicyAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Before proceeding validate if object exists.
+ _, err = objAPI.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ aclHeader := r.Header.Get(xhttp.AmzACL)
+ if aclHeader == "" {
+ acl := &accessControlPolicy{}
+ if err = xmlDecoder(r.Body, acl, r.ContentLength); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if len(acl.AccessControlList.Grants) == 0 {
+ writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL)
+ return
+ }
+
+ if acl.AccessControlList.Grants[0].Permission != "FULL_CONTROL" {
+ writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL)
+ return
+ }
+ }
+
+ if aclHeader != "" && aclHeader != "private" {
+ writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL)
+ return
+ }
+}
+
+// GetObjectACLHandler - GET Object ACL
+// -----------------
+// This operation uses the ACL
+// subresource to return the ACL of a specified object.
+func (api objectAPIHandlers) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetObjectACL")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+ object, err := unescapePath(vars["object"])
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ // Allow getObjectACL if policy action is set, since this is a dummy call
+ // we are simply re-purposing the bucketPolicyAction.
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketPolicyAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Before proceeding validate if object exists.
+ _, err = objAPI.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ acl := &accessControlPolicy{}
+ acl.AccessControlList.Grants = append(acl.AccessControlList.Grants, grant{
+ Grantee: grantee{
+ XMLNS: "http://www.w3.org/2001/XMLSchema-instance",
+ XMLXSI: "CanonicalUser",
+ Type: "CanonicalUser",
+ },
+ Permission: "FULL_CONTROL",
+ })
+ if err := xml.NewEncoder(w).Encode(acl); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+}
diff --git a/cmd/admin-bucket-handlers.go b/cmd/admin-bucket-handlers.go
new file mode 100644
index 00000000..4ea93878
--- /dev/null
+++ b/cmd/admin-bucket-handlers.go
@@ -0,0 +1,1115 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "encoding/base64"
+ "encoding/json"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+
+ jsoniter "github.com/json-iterator/go"
+ "github.com/klauspost/compress/zip"
+ "github.com/minio/kms-go/kes"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio-go/v7/pkg/tags"
+ "github.com/minio/minio/internal/bucket/lifecycle"
+ objectlock "github.com/minio/minio/internal/bucket/object/lock"
+ "github.com/minio/minio/internal/bucket/versioning"
+ "github.com/minio/minio/internal/event"
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/minio/internal/kms"
+ "github.com/minio/mux"
+ "github.com/minio/pkg/v3/policy"
+)
+
+const (
+ bucketQuotaConfigFile = "quota.json"
+ bucketTargetsFile = "bucket-targets.json"
+)
+
+// PutBucketQuotaConfigHandler - PUT Bucket quota configuration.
+// ----------
+// Places a quota configuration on the specified bucket. The quota
+// specified in the quota configuration will be applied by default
+// to enforce total quota for the specified bucket.
+func (a adminAPIHandlers) PutBucketQuotaConfigHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.SetBucketQuotaAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := pathClean(vars["bucket"])
+
+ if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ data, err := io.ReadAll(r.Body)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
+ return
+ }
+
+ quotaConfig, err := parseBucketQuota(bucket, data)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, data)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ bucketMeta := madmin.SRBucketMeta{
+ Type: madmin.SRBucketMetaTypeQuotaConfig,
+ Bucket: bucket,
+ Quota: data,
+ UpdatedAt: updatedAt,
+ }
+ if quotaConfig.Size == 0 && quotaConfig.Quota == 0 {
+ bucketMeta.Quota = nil
+ }
+
+ // Call site replication hook.
+ replLogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, bucketMeta))
+
+ // Write success response.
+ writeSuccessResponseHeadersOnly(w)
+}
+
+// GetBucketQuotaConfigHandler - gets bucket quota configuration
+func (a adminAPIHandlers) GetBucketQuotaConfigHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.GetBucketQuotaAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := pathClean(vars["bucket"])
+
+ if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ config, _, err := globalBucketMetadataSys.GetQuotaConfig(ctx, bucket)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ configData, err := json.Marshal(config)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Write success response.
+ writeSuccessResponseJSON(w, configData)
+}
+
+// SetRemoteTargetHandler - sets a remote target for bucket
+func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ vars := mux.Vars(r)
+ bucket := pathClean(vars["bucket"])
+ update := r.Form.Get("update") == "true"
+
+ // Get current object layer instance.
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.SetBucketTargetAction)
+ if objectAPI == nil {
+ return
+ }
+
+ // Check if bucket exists.
+ if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ cred, _, s3Err := validateAdminSignature(ctx, r, "")
+ if s3Err != ErrNone {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
+ return
+ }
+ password := cred.SecretKey
+
+ reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
+ return
+ }
+ var target madmin.BucketTarget
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ if err = json.Unmarshal(reqBytes, &target); err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
+ return
+ }
+ sameTarget, _ := isLocalHost(target.URL().Hostname(), target.URL().Port(), globalMinioPort)
+ if sameTarget && bucket == target.TargetBucket {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBucketRemoteIdenticalToSource), r.URL)
+ return
+ }
+
+ target.SourceBucket = bucket
+ var ops []madmin.TargetUpdateType
+ if update {
+ ops = madmin.GetTargetUpdateOps(r.Form)
+ } else {
+ var exists bool // true if arn exists
+ target.Arn, exists = globalBucketTargetSys.getRemoteARN(bucket, &target, "")
+ if exists && target.Arn != "" { // return pre-existing ARN
+ data, err := json.Marshal(target.Arn)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ // Write success response.
+ writeSuccessResponseJSON(w, data)
+ return
+ }
+ }
+ if target.Arn == "" {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
+ return
+ }
+ if globalSiteReplicationSys.isEnabled() && !update {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrRemoteTargetDenyAddError, err), r.URL)
+ return
+ }
+
+ if update {
+ // overlay the updates on existing target
+ tgt := globalBucketTargetSys.GetRemoteBucketTargetByArn(ctx, bucket, target.Arn)
+ if tgt.Empty() {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrRemoteTargetNotFoundError, err), r.URL)
+ return
+ }
+ for _, op := range ops {
+ switch op {
+ case madmin.CredentialsUpdateType:
+ if !globalSiteReplicationSys.isEnabled() {
+ // credentials update is possible only in bucket replication. User will never
+ // know the site replicator creds.
+ tgt.Credentials = target.Credentials
+ tgt.TargetBucket = target.TargetBucket
+ tgt.Secure = target.Secure
+ tgt.Endpoint = target.Endpoint
+ }
+ case madmin.SyncUpdateType:
+ tgt.ReplicationSync = target.ReplicationSync
+ case madmin.ProxyUpdateType:
+ tgt.DisableProxy = target.DisableProxy
+ case madmin.PathUpdateType:
+ tgt.Path = target.Path
+ case madmin.BandwidthLimitUpdateType:
+ tgt.BandwidthLimit = target.BandwidthLimit
+ case madmin.HealthCheckDurationUpdateType:
+ tgt.HealthCheckDuration = target.HealthCheckDuration
+ }
+ }
+ target = tgt
+ }
+
+ // enforce minimum bandwidth limit as 100MBps
+ if target.BandwidthLimit > 0 && target.BandwidthLimit < 100*1000*1000 {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationBandwidthLimitError, err), r.URL)
+ return
+ }
+ if err = globalBucketTargetSys.SetTarget(ctx, bucket, &target, update); err != nil {
+ switch err.(type) {
+ case RemoteTargetConnectionErr:
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationRemoteConnectionError, err), r.URL)
+ default:
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ }
+ return
+ }
+ targets, err := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ tgtBytes, err := json.Marshal(&targets)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
+ return
+ }
+ if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketTargetsFile, tgtBytes); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ data, err := json.Marshal(target.Arn)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ // Write success response.
+ writeSuccessResponseJSON(w, data)
+}
+
+// ListRemoteTargetsHandler - lists remote target(s) for a bucket or gets a target
+// for a particular ARN type
+func (a adminAPIHandlers) ListRemoteTargetsHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ vars := mux.Vars(r)
+ bucket := pathClean(vars["bucket"])
+ arnType := vars["type"]
+
+ // Get current object layer instance.
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.GetBucketTargetAction)
+ if objectAPI == nil {
+ return
+ }
+ if bucket != "" {
+ // Check if bucket exists.
+ if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ if _, err := globalBucketMetadataSys.GetBucketTargetsConfig(bucket); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ }
+ targets := globalBucketTargetSys.ListTargets(ctx, bucket, arnType)
+ data, err := json.Marshal(targets)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ // Write success response.
+ writeSuccessResponseJSON(w, data)
+}
+
+// RemoveRemoteTargetHandler - removes a remote target for bucket with specified ARN
+func (a adminAPIHandlers) RemoveRemoteTargetHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ vars := mux.Vars(r)
+ bucket := pathClean(vars["bucket"])
+ arn := vars["arn"]
+
+ // Get current object layer instance.
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.SetBucketTargetAction)
+ if objectAPI == nil {
+ return
+ }
+
+ // Check if bucket exists.
+ if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if err := globalBucketTargetSys.RemoveTarget(ctx, bucket, arn); err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ targets, err := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ tgtBytes, err := json.Marshal(&targets)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
+ return
+ }
+ if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketTargetsFile, tgtBytes); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Write success response.
+ writeSuccessNoContent(w)
+}
+
+// ExportBucketMetadataHandler - exports all bucket metadata as a zipped file
+func (a adminAPIHandlers) ExportBucketMetadataHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ bucket := pathClean(r.Form.Get("bucket"))
+ // Get current object layer instance.
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.ExportBucketMetadataAction)
+ if objectAPI == nil {
+ return
+ }
+
+ var (
+ buckets []BucketInfo
+ err error
+ )
+ if bucket != "" {
+ // Check if bucket exists.
+ if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ buckets = append(buckets, BucketInfo{Name: bucket})
+ } else {
+ buckets, err = objectAPI.ListBuckets(ctx, BucketOptions{})
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ }
+
+ // Initialize a zip writer which will provide a zipped content
+ // of bucket metadata
+ zipWriter := zip.NewWriter(w)
+ defer zipWriter.Close()
+
+ rawDataFn := func(r io.Reader, filename string, sz int) {
+ header, zerr := zip.FileInfoHeader(dummyFileInfo{
+ name: filename,
+ size: int64(sz),
+ mode: 0o600,
+ modTime: time.Now(),
+ isDir: false,
+ sys: nil,
+ })
+ if zerr == nil {
+ header.Method = zip.Deflate
+ zwriter, zerr := zipWriter.CreateHeader(header)
+ if zerr == nil {
+ io.Copy(zwriter, r)
+ }
+ }
+ }
+
+ cfgFiles := []string{
+ bucketPolicyConfig,
+ bucketNotificationConfig,
+ bucketLifecycleConfig,
+ bucketSSEConfig,
+ bucketTaggingConfig,
+ bucketQuotaConfigFile,
+ objectLockConfig,
+ bucketVersioningConfig,
+ bucketReplicationConfig,
+ bucketTargetsFile,
+ }
+ for _, bi := range buckets {
+ for _, cfgFile := range cfgFiles {
+ cfgPath := pathJoin(bi.Name, cfgFile)
+ bucket := bi.Name
+ switch cfgFile {
+ case bucketPolicyConfig:
+ config, _, err := globalBucketMetadataSys.GetBucketPolicy(bucket)
+ if err != nil {
+ if errors.Is(err, BucketPolicyNotFound{Bucket: bucket}) {
+ continue
+ }
+ writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
+ return
+ }
+ configData, err := json.Marshal(config)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
+ return
+ }
+ rawDataFn(bytes.NewReader(configData), cfgPath, len(configData))
+ case bucketNotificationConfig:
+ config, err := globalBucketMetadataSys.GetNotificationConfig(bucket)
+ if err != nil {
+ adminLogIf(ctx, err)
+ writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
+ return
+ }
+ configData, err := xml.Marshal(config)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
+ return
+ }
+ rawDataFn(bytes.NewReader(configData), cfgPath, len(configData))
+ case bucketLifecycleConfig:
+ config, _, err := globalBucketMetadataSys.GetLifecycleConfig(bucket)
+ if err != nil {
+ if errors.Is(err, BucketLifecycleNotFound{Bucket: bucket}) {
+ continue
+ }
+ adminLogIf(ctx, err)
+ writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
+ return
+ }
+ configData, err := xml.Marshal(config)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
+ return
+ }
+ rawDataFn(bytes.NewReader(configData), cfgPath, len(configData))
+ case bucketQuotaConfigFile:
+ config, _, err := globalBucketMetadataSys.GetQuotaConfig(ctx, bucket)
+ if err != nil {
+ if errors.Is(err, BucketQuotaConfigNotFound{Bucket: bucket}) {
+ continue
+ }
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ configData, err := json.Marshal(config)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ rawDataFn(bytes.NewReader(configData), cfgPath, len(configData))
+ case bucketSSEConfig:
+ config, _, err := globalBucketMetadataSys.GetSSEConfig(bucket)
+ if err != nil {
+ if errors.Is(err, BucketSSEConfigNotFound{Bucket: bucket}) {
+ continue
+ }
+ writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
+ return
+ }
+ configData, err := xml.Marshal(config)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
+ return
+ }
+ rawDataFn(bytes.NewReader(configData), cfgPath, len(configData))
+ case bucketTaggingConfig:
+ config, _, err := globalBucketMetadataSys.GetTaggingConfig(bucket)
+ if err != nil {
+ if errors.Is(err, BucketTaggingNotFound{Bucket: bucket}) {
+ continue
+ }
+ writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
+ return
+ }
+ configData, err := xml.Marshal(config)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
+ return
+ }
+ rawDataFn(bytes.NewReader(configData), cfgPath, len(configData))
+ case objectLockConfig:
+ config, _, err := globalBucketMetadataSys.GetObjectLockConfig(bucket)
+ if err != nil {
+ if errors.Is(err, BucketObjectLockConfigNotFound{Bucket: bucket}) {
+ continue
+ }
+ writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
+ return
+ }
+
+ configData, err := xml.Marshal(config)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
+ return
+ }
+ rawDataFn(bytes.NewReader(configData), cfgPath, len(configData))
+ case bucketVersioningConfig:
+ config, _, err := globalBucketMetadataSys.GetVersioningConfig(bucket)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
+ return
+ }
+ // ignore empty versioning configs
+ if config.Status != versioning.Enabled && config.Status != versioning.Suspended {
+ continue
+ }
+ configData, err := xml.Marshal(config)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
+ return
+ }
+ rawDataFn(bytes.NewReader(configData), cfgPath, len(configData))
+ case bucketReplicationConfig:
+ config, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
+ if err != nil {
+ if errors.Is(err, BucketReplicationConfigNotFound{Bucket: bucket}) {
+ continue
+ }
+ writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
+ return
+ }
+ configData, err := xml.Marshal(config)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
+ return
+ }
+ rawDataFn(bytes.NewReader(configData), cfgPath, len(configData))
+ case bucketTargetsFile:
+ config, err := globalBucketMetadataSys.GetBucketTargetsConfig(bucket)
+ if err != nil {
+ if errors.Is(err, BucketRemoteTargetNotFound{Bucket: bucket}) {
+ continue
+ }
+
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ configData, err := xml.Marshal(config)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
+ return
+ }
+ rawDataFn(bytes.NewReader(configData), cfgPath, len(configData))
+ }
+ }
+ }
+}
+
+type importMetaReport struct {
+ madmin.BucketMetaImportErrs
+}
+
+func (i *importMetaReport) SetStatus(bucket, fname string, err error) {
+ st := i.Buckets[bucket]
+ var errMsg string
+ if err != nil {
+ errMsg = err.Error()
+ }
+ switch fname {
+ case bucketPolicyConfig:
+ st.Policy = madmin.MetaStatus{IsSet: true, Err: errMsg}
+ case bucketNotificationConfig:
+ st.Notification = madmin.MetaStatus{IsSet: true, Err: errMsg}
+ case bucketLifecycleConfig:
+ st.Lifecycle = madmin.MetaStatus{IsSet: true, Err: errMsg}
+ case bucketSSEConfig:
+ st.SSEConfig = madmin.MetaStatus{IsSet: true, Err: errMsg}
+ case bucketTaggingConfig:
+ st.Tagging = madmin.MetaStatus{IsSet: true, Err: errMsg}
+ case bucketQuotaConfigFile:
+ st.Quota = madmin.MetaStatus{IsSet: true, Err: errMsg}
+ case objectLockConfig:
+ st.ObjectLock = madmin.MetaStatus{IsSet: true, Err: errMsg}
+ case bucketVersioningConfig:
+ st.Versioning = madmin.MetaStatus{IsSet: true, Err: errMsg}
+ default:
+ st.Err = errMsg
+ }
+ i.Buckets[bucket] = st
+}
+
+// ImportBucketMetadataHandler - imports all bucket metadata from a zipped file and overwrite bucket metadata config
+// There are some caveats regarding the following:
+// 1. object lock config - object lock should have been specified at time of bucket creation. Only default retention settings are imported here.
+// 2. Replication config - is omitted from import as remote target credentials are not available from exported data for security reasons.
+// 3. lifecycle config - if transition rules are present, tier name needs to have been defined.
+func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ // Get current object layer instance.
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.ImportBucketMetadataAction)
+ if objectAPI == nil {
+ return
+ }
+ data, err := io.ReadAll(r.Body)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
+ return
+ }
+ reader := bytes.NewReader(data)
+ zr, err := zip.NewReader(reader, int64(len(data)))
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
+ return
+ }
+ rpt := importMetaReport{
+ madmin.BucketMetaImportErrs{
+ Buckets: make(map[string]madmin.BucketStatus, len(zr.File)),
+ },
+ }
+
+ bucketMap := make(map[string]*BucketMetadata, len(zr.File))
+
+ updatedAt := UTCNow()
+
+ for _, file := range zr.File {
+ slc := strings.Split(file.Name, slashSeparator)
+ if len(slc) != 2 { // expecting bucket/configfile in the zipfile
+ rpt.SetStatus(file.Name, "", fmt.Errorf("malformed zip - expecting format bucket/"))
+ continue
+ }
+ bucket := slc[0]
+ meta, err := readBucketMetadata(ctx, objectAPI, bucket)
+ if err == nil {
+ bucketMap[bucket] = &meta
+ } else if err != errConfigNotFound {
+ rpt.SetStatus(bucket, "", err)
+ }
+ }
+
+ // import object lock config if any - order of import matters here.
+ for _, file := range zr.File {
+ slc := strings.Split(file.Name, slashSeparator)
+ if len(slc) != 2 { // expecting bucket/configfile in the zipfile
+ rpt.SetStatus(file.Name, "", fmt.Errorf("malformed zip - expecting format bucket/"))
+ continue
+ }
+ bucket, fileName := slc[0], slc[1]
+ if fileName == objectLockConfig {
+ reader, err := file.Open()
+ if err != nil {
+ rpt.SetStatus(bucket, fileName, err)
+ continue
+ }
+ config, err := objectlock.ParseObjectLockConfig(reader)
+ if err != nil {
+ rpt.SetStatus(bucket, fileName, fmt.Errorf("%s (%s)", errorCodes[ErrMalformedXML].Description, err))
+ continue
+ }
+
+ configData, err := xml.Marshal(config)
+ if err != nil {
+ rpt.SetStatus(bucket, fileName, err)
+ continue
+ }
+ if _, ok := bucketMap[bucket]; !ok {
+ opts := MakeBucketOptions{
+ LockEnabled: config.Enabled(),
+ ForceCreate: true, // ignore if it already exists
+ }
+ err = objectAPI.MakeBucket(ctx, bucket, opts)
+ if err != nil {
+ rpt.SetStatus(bucket, fileName, err)
+ continue
+ }
+ v, _ := globalBucketMetadataSys.Get(bucket)
+ bucketMap[bucket] = &v
+ }
+
+ bucketMap[bucket].ObjectLockConfigXML = configData
+ bucketMap[bucket].ObjectLockConfigUpdatedAt = updatedAt
+ rpt.SetStatus(bucket, fileName, nil)
+ }
+ }
+
+ // import versioning metadata
+ for _, file := range zr.File {
+ slc := strings.Split(file.Name, slashSeparator)
+ if len(slc) != 2 { // expecting bucket/configfile in the zipfile
+ rpt.SetStatus(file.Name, "", fmt.Errorf("malformed zip - expecting format bucket/"))
+ continue
+ }
+ bucket, fileName := slc[0], slc[1]
+ if fileName == bucketVersioningConfig {
+ reader, err := file.Open()
+ if err != nil {
+ rpt.SetStatus(bucket, fileName, err)
+ continue
+ }
+ v, err := versioning.ParseConfig(io.LimitReader(reader, maxBucketVersioningConfigSize))
+ if err != nil {
+ rpt.SetStatus(bucket, fileName, err)
+ continue
+ }
+ if _, ok := bucketMap[bucket]; !ok {
+ if err = objectAPI.MakeBucket(ctx, bucket, MakeBucketOptions{
+ ForceCreate: true, // ignore if it already exists
+ }); err != nil {
+ rpt.SetStatus(bucket, fileName, err)
+ continue
+ }
+ v, _ := globalBucketMetadataSys.Get(bucket)
+ bucketMap[bucket] = &v
+ }
+
+ if globalSiteReplicationSys.isEnabled() && v.Suspended() {
+ rpt.SetStatus(bucket, fileName, fmt.Errorf("Cluster replication is enabled for this site, so the versioning state cannot be suspended."))
+ continue
+ }
+
+ if rcfg, _ := globalBucketObjectLockSys.Get(bucket); rcfg.LockEnabled && v.Suspended() {
+ rpt.SetStatus(bucket, fileName, fmt.Errorf("An Object Lock configuration is present on this bucket, so the versioning state cannot be suspended."))
+ continue
+ }
+ if rcfg, _ := getReplicationConfig(ctx, bucket); rcfg != nil && v.Suspended() {
+ rpt.SetStatus(bucket, fileName, fmt.Errorf("A replication configuration is present on this bucket, so the versioning state cannot be suspended."))
+ continue
+ }
+
+ configData, err := xml.Marshal(v)
+ if err != nil {
+ rpt.SetStatus(bucket, fileName, fmt.Errorf("%s (%s)", errorCodes[ErrMalformedXML].Description, err))
+ continue
+ }
+
+ bucketMap[bucket].VersioningConfigXML = configData
+ bucketMap[bucket].VersioningConfigUpdatedAt = updatedAt
+ rpt.SetStatus(bucket, fileName, nil)
+ }
+ }
+
+ for _, file := range zr.File {
+ reader, err := file.Open()
+ if err != nil {
+ rpt.SetStatus(file.Name, "", err)
+ continue
+ }
+ sz := file.FileInfo().Size()
+ slc := strings.Split(file.Name, slashSeparator)
+ if len(slc) != 2 { // expecting bucket/configfile in the zipfile
+ rpt.SetStatus(file.Name, "", fmt.Errorf("malformed zip - expecting format bucket/"))
+ continue
+ }
+ bucket, fileName := slc[0], slc[1]
+
+ // create bucket if it does not exist yet.
+ if _, ok := bucketMap[bucket]; !ok {
+ err = objectAPI.MakeBucket(ctx, bucket, MakeBucketOptions{
+ ForceCreate: true, // ignore if it already exists
+ })
+ if err != nil {
+ rpt.SetStatus(bucket, "", err)
+ continue
+ }
+ v, _ := globalBucketMetadataSys.Get(bucket)
+ bucketMap[bucket] = &v
+ }
+ if _, ok := bucketMap[bucket]; !ok {
+ continue
+ }
+ switch fileName {
+ case bucketNotificationConfig:
+ config, err := event.ParseConfig(io.LimitReader(reader, sz), globalSite.Region(), globalEventNotifier.targetList)
+ if err != nil {
+ rpt.SetStatus(bucket, fileName, fmt.Errorf("%s (%s)", errorCodes[ErrMalformedXML].Description, err))
+ continue
+ }
+
+ configData, err := xml.Marshal(config)
+ if err != nil {
+ rpt.SetStatus(bucket, fileName, err)
+ continue
+ }
+
+ bucketMap[bucket].NotificationConfigXML = configData
+ bucketMap[bucket].NotificationConfigUpdatedAt = updatedAt
+ rpt.SetStatus(bucket, fileName, nil)
+ case bucketPolicyConfig:
+ // Error out if Content-Length is beyond allowed size.
+ if sz > maxBucketPolicySize {
+ rpt.SetStatus(bucket, fileName, errors.New(ErrPolicyTooLarge.String()))
+ continue
+ }
+
+ bucketPolicyBytes, err := io.ReadAll(io.LimitReader(reader, sz))
+ if err != nil {
+ rpt.SetStatus(bucket, fileName, err)
+ continue
+ }
+
+ bucketPolicy, err := policy.ParseBucketPolicyConfig(bytes.NewReader(bucketPolicyBytes), bucket)
+ if err != nil {
+ rpt.SetStatus(bucket, fileName, err)
+ continue
+ }
+
+ // Version in policy must not be empty
+ if bucketPolicy.Version == "" {
+ rpt.SetStatus(bucket, fileName, errors.New(ErrPolicyInvalidVersion.String()))
+ continue
+ }
+
+ configData, err := json.Marshal(bucketPolicy)
+ if err != nil {
+ rpt.SetStatus(bucket, fileName, err)
+ continue
+ }
+
+ bucketMap[bucket].PolicyConfigJSON = configData
+ bucketMap[bucket].PolicyConfigUpdatedAt = updatedAt
+ rpt.SetStatus(bucket, fileName, nil)
+ case bucketLifecycleConfig:
+ bucketLifecycle, err := lifecycle.ParseLifecycleConfig(io.LimitReader(reader, sz))
+ if err != nil {
+ rpt.SetStatus(bucket, fileName, err)
+ continue
+ }
+ rcfg, err := globalBucketObjectLockSys.Get(bucket)
+ if err != nil {
+ rpt.SetStatus(bucket, fileName, err)
+ continue
+ }
+ // Validate the received bucket policy document
+ if err = bucketLifecycle.Validate(rcfg); err != nil {
+ rpt.SetStatus(bucket, fileName, err)
+ continue
+ }
+
+ // Validate the transition storage ARNs
+ if err = validateTransitionTier(bucketLifecycle); err != nil {
+ rpt.SetStatus(bucket, fileName, err)
+ continue
+ }
+
+ configData, err := xml.Marshal(bucketLifecycle)
+ if err != nil {
+ rpt.SetStatus(bucket, fileName, err)
+ continue
+ }
+
+ bucketMap[bucket].LifecycleConfigXML = configData
+ bucketMap[bucket].LifecycleConfigUpdatedAt = updatedAt
+ rpt.SetStatus(bucket, fileName, nil)
+ case bucketSSEConfig:
+ // Parse bucket encryption xml
+ encConfig, err := validateBucketSSEConfig(io.LimitReader(reader, maxBucketSSEConfigSize))
+ if err != nil {
+ rpt.SetStatus(bucket, fileName, fmt.Errorf("%s (%s)", errorCodes[ErrMalformedXML].Description, err))
+ continue
+ }
+
+ // Return error if KMS is not initialized
+ if GlobalKMS == nil {
+ rpt.SetStatus(bucket, fileName, fmt.Errorf("%s", errorCodes[ErrKMSNotConfigured].Description))
+ continue
+ }
+ kmsKey := encConfig.KeyID()
+ if kmsKey != "" {
+ _, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{
+ Name: kmsKey,
+ AssociatedData: kms.Context{"MinIO admin API": "ServerInfoHandler"}, // Context for a test key operation
+ })
+ if err != nil {
+ if errors.Is(err, kes.ErrKeyNotFound) {
+ rpt.SetStatus(bucket, fileName, errKMSKeyNotFound)
+ continue
+ }
+ rpt.SetStatus(bucket, fileName, err)
+ continue
+ }
+ }
+
+ configData, err := xml.Marshal(encConfig)
+ if err != nil {
+ rpt.SetStatus(bucket, fileName, err)
+ continue
+ }
+
+ bucketMap[bucket].EncryptionConfigXML = configData
+ bucketMap[bucket].EncryptionConfigUpdatedAt = updatedAt
+ rpt.SetStatus(bucket, fileName, nil)
+ case bucketTaggingConfig:
+ tags, err := tags.ParseBucketXML(io.LimitReader(reader, sz))
+ if err != nil {
+ rpt.SetStatus(bucket, fileName, fmt.Errorf("%s (%s)", errorCodes[ErrMalformedXML].Description, err))
+ continue
+ }
+
+ configData, err := xml.Marshal(tags)
+ if err != nil {
+ rpt.SetStatus(bucket, fileName, err)
+ continue
+ }
+
+ bucketMap[bucket].TaggingConfigXML = configData
+ bucketMap[bucket].TaggingConfigUpdatedAt = updatedAt
+ rpt.SetStatus(bucket, fileName, nil)
+ case bucketQuotaConfigFile:
+ data, err := io.ReadAll(reader)
+ if err != nil {
+ rpt.SetStatus(bucket, fileName, err)
+ continue
+ }
+
+ _, err = parseBucketQuota(bucket, data)
+ if err != nil {
+ rpt.SetStatus(bucket, fileName, err)
+ continue
+ }
+
+ bucketMap[bucket].QuotaConfigJSON = data
+ bucketMap[bucket].QuotaConfigUpdatedAt = updatedAt
+ rpt.SetStatus(bucket, fileName, nil)
+ }
+ }
+
+ enc := func(b []byte) *string {
+ if b == nil {
+ return nil
+ }
+ v := base64.StdEncoding.EncodeToString(b)
+ return &v
+ }
+
+ for bucket, meta := range bucketMap {
+ err := globalBucketMetadataSys.save(ctx, *meta)
+ if err != nil {
+ rpt.SetStatus(bucket, "", err)
+ continue
+ }
+ // Call site replication hook.
+ if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
+ Bucket: bucket,
+ Quota: meta.QuotaConfigJSON,
+ Policy: meta.PolicyConfigJSON,
+ Versioning: enc(meta.VersioningConfigXML),
+ Tags: enc(meta.TaggingConfigXML),
+ ObjectLockConfig: enc(meta.ObjectLockConfigXML),
+ SSEConfig: enc(meta.EncryptionConfigXML),
+ UpdatedAt: updatedAt,
+ }); err != nil {
+ rpt.SetStatus(bucket, "", err)
+ continue
+ }
+ }
+
+ rptData, err := json.Marshal(rpt.BucketMetaImportErrs)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, rptData)
+}
+
+// ReplicationDiffHandler - POST returns info on unreplicated versions for a remote target ARN
+// to the connected HTTP client.
+func (a adminAPIHandlers) ReplicationDiffHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.ReplicationDiff)
+ if objectAPI == nil {
+ return
+ }
+
+ // Check if bucket exists.
+ if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ opts := extractReplicateDiffOpts(r.Form)
+ if opts.ARN != "" {
+ tgt := globalBucketTargetSys.GetRemoteBucketTargetByArn(ctx, bucket, opts.ARN)
+ if tgt.Empty() {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrInvalidRequest, fmt.Errorf("invalid arn : '%s'", opts.ARN)), r.URL)
+ return
+ }
+ }
+
+ keepAliveTicker := time.NewTicker(500 * time.Millisecond)
+ defer keepAliveTicker.Stop()
+
+ diffCh, err := getReplicationDiff(ctx, objectAPI, bucket, opts)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ enc := json.NewEncoder(w)
+ for {
+ select {
+ case entry, ok := <-diffCh:
+ if !ok {
+ return
+ }
+ if err := enc.Encode(entry); err != nil {
+ return
+ }
+ if len(diffCh) == 0 {
+ // Flush if nothing is queued
+ xhttp.Flush(w)
+ }
+ case <-keepAliveTicker.C:
+ if len(diffCh) > 0 {
+ continue
+ }
+ if _, err := w.Write([]byte(" ")); err != nil {
+ return
+ }
+ xhttp.Flush(w)
+ case <-ctx.Done():
+ return
+ }
+ }
+}
+
+// ReplicationMRFHandler - POST returns info on entries in the MRF backlog for a node or all nodes
+func (a adminAPIHandlers) ReplicationMRFHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.ReplicationDiff)
+ if objectAPI == nil {
+ return
+ }
+
+ // Check if bucket exists.
+ if bucket != "" {
+ if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ }
+
+ q := r.Form
+ node := q.Get("node")
+
+ keepAliveTicker := time.NewTicker(500 * time.Millisecond)
+ defer keepAliveTicker.Stop()
+
+ mrfCh, err := globalNotificationSys.GetReplicationMRF(ctx, bucket, node)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ enc := json.NewEncoder(w)
+ for {
+ select {
+ case entry, ok := <-mrfCh:
+ if !ok {
+ return
+ }
+ if err := enc.Encode(entry); err != nil {
+ return
+ }
+ if len(mrfCh) == 0 {
+ // Flush if nothing is queued
+ xhttp.Flush(w)
+ }
+ case <-keepAliveTicker.C:
+ if len(mrfCh) > 0 {
+ continue
+ }
+ if _, err := w.Write([]byte(" ")); err != nil {
+ return
+ }
+ xhttp.Flush(w)
+ case <-ctx.Done():
+ return
+ }
+ }
+}
diff --git a/cmd/admin-handler-utils.go b/cmd/admin-handler-utils.go
new file mode 100644
index 00000000..cdfb7987
--- /dev/null
+++ b/cmd/admin-handler-utils.go
@@ -0,0 +1,263 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+
+ "github.com/minio/kms-go/kes"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/auth"
+ "github.com/minio/minio/internal/config"
+ "github.com/minio/pkg/v3/policy"
+)
+
+// validateAdminReq will validate request against and return whether it is allowed.
+// If any of the supplied actions are allowed it will be successful.
+// If nil ObjectLayer is returned, the operation is not permitted.
+// When nil ObjectLayer has been returned an error has always been sent to w.
+func validateAdminReq(ctx context.Context, w http.ResponseWriter, r *http.Request, actions ...policy.AdminAction) (ObjectLayer, auth.Credentials) {
+ // Get current object layer instance.
+ objectAPI := newObjectLayerFn()
+ if objectAPI == nil || globalNotificationSys == nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return nil, auth.Credentials{}
+ }
+
+ for _, action := range actions {
+ // Validate request signature.
+ cred, adminAPIErr := checkAdminRequestAuth(ctx, r, action, "")
+ switch adminAPIErr {
+ case ErrNone:
+ return objectAPI, cred
+ case ErrAccessDenied:
+ // Try another
+ continue
+ default:
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
+ return nil, cred
+ }
+ }
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return nil, auth.Credentials{}
+}
+
+// AdminError - is a generic error for all admin APIs.
+type AdminError struct {
+ Code string
+ Message string
+ StatusCode int
+}
+
+func (ae AdminError) Error() string {
+ return ae.Message
+}
+
+func toAdminAPIErr(ctx context.Context, err error) APIError {
+ if err == nil {
+ return noError
+ }
+
+ var apiErr APIError
+ switch e := err.(type) {
+ case policy.Error:
+ apiErr = APIError{
+ Code: "XMinioMalformedIAMPolicy",
+ Description: e.Error(),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ case config.ErrConfigNotFound:
+ apiErr = APIError{
+ Code: "XMinioConfigNotFoundError",
+ Description: e.Error(),
+ HTTPStatusCode: http.StatusNotFound,
+ }
+ case config.ErrConfigGeneric:
+ apiErr = APIError{
+ Code: "XMinioConfigError",
+ Description: e.Error(),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ case AdminError:
+ apiErr = APIError{
+ Code: e.Code,
+ Description: e.Message,
+ HTTPStatusCode: e.StatusCode,
+ }
+ case SRError:
+ apiErr = errorCodes.ToAPIErrWithErr(e.Code, e.Cause)
+ case decomError:
+ apiErr = APIError{
+ Code: "XMinioDecommissionNotAllowed",
+ Description: e.Err,
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ default:
+ switch {
+ case errors.Is(err, errTooManyPolicies):
+ apiErr = APIError{
+ Code: "XMinioAdminInvalidRequest",
+ Description: err.Error(),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ case errors.Is(err, errDecommissionAlreadyRunning):
+ apiErr = APIError{
+ Code: "XMinioDecommissionNotAllowed",
+ Description: err.Error(),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ case errors.Is(err, errDecommissionComplete):
+ apiErr = APIError{
+ Code: "XMinioDecommissionNotAllowed",
+ Description: err.Error(),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ case errors.Is(err, errDecommissionRebalanceAlreadyRunning):
+ apiErr = APIError{
+ Code: "XMinioDecommissionNotAllowed",
+ Description: err.Error(),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ case errors.Is(err, errRebalanceDecommissionAlreadyRunning):
+ apiErr = APIError{
+ Code: "XMinioRebalanceNotAllowed",
+ Description: err.Error(),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ case errors.Is(err, errConfigNotFound):
+ apiErr = APIError{
+ Code: "XMinioConfigError",
+ Description: err.Error(),
+ HTTPStatusCode: http.StatusNotFound,
+ }
+ case errors.Is(err, errIAMActionNotAllowed):
+ apiErr = APIError{
+ Code: "XMinioIAMActionNotAllowed",
+ Description: err.Error(),
+ HTTPStatusCode: http.StatusForbidden,
+ }
+ case errors.Is(err, errIAMServiceAccountNotAllowed):
+ apiErr = APIError{
+ Code: "XMinioIAMServiceAccountNotAllowed",
+ Description: err.Error(),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ case errors.Is(err, errIAMNotInitialized):
+ apiErr = APIError{
+ Code: "XMinioIAMNotInitialized",
+ Description: err.Error(),
+ HTTPStatusCode: http.StatusServiceUnavailable,
+ }
+ case errors.Is(err, errPolicyInUse):
+ apiErr = APIError{
+ Code: "XMinioIAMPolicyInUse",
+ Description: "The policy cannot be removed, as it is in use",
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ case errors.Is(err, errSessionPolicyTooLarge):
+ apiErr = APIError{
+ Code: "XMinioIAMServiceAccountSessionPolicyTooLarge",
+ Description: err.Error(),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ case errors.Is(err, kes.ErrKeyExists):
+ apiErr = APIError{
+ Code: "XMinioKMSKeyExists",
+ Description: err.Error(),
+ HTTPStatusCode: http.StatusConflict,
+ }
+
+ // Tier admin API errors
+ case errors.Is(err, madmin.ErrTierNameEmpty):
+ apiErr = APIError{
+ Code: "XMinioAdminTierNameEmpty",
+ Description: err.Error(),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ case errors.Is(err, madmin.ErrTierInvalidConfig):
+ apiErr = APIError{
+ Code: "XMinioAdminTierInvalidConfig",
+ Description: err.Error(),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ case errors.Is(err, madmin.ErrTierInvalidConfigVersion):
+ apiErr = APIError{
+ Code: "XMinioAdminTierInvalidConfigVersion",
+ Description: err.Error(),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ case errors.Is(err, madmin.ErrTierTypeUnsupported):
+ apiErr = APIError{
+ Code: "XMinioAdminTierTypeUnsupported",
+ Description: err.Error(),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ case errIsTierPermError(err):
+ apiErr = APIError{
+ Code: "XMinioAdminTierInsufficientPermissions",
+ Description: err.Error(),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ case errors.Is(err, errTierInvalidConfig):
+ apiErr = APIError{
+ Code: "XMinioAdminTierInvalidConfig",
+ Description: err.Error(),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ default:
+ apiErr = errorCodes.ToAPIErrWithErr(toAdminAPIErrCode(ctx, err), err)
+ }
+ }
+ return apiErr
+}
+
+// toAdminAPIErrCode - converts errErasureWriteQuorum error to admin API
+// specific error.
+func toAdminAPIErrCode(ctx context.Context, err error) APIErrorCode {
+ if errors.Is(err, errErasureWriteQuorum) {
+ return ErrAdminConfigNoQuorum
+ }
+ return toAPIErrorCode(ctx, err)
+}
+
+// wraps export error for more context
+func exportError(ctx context.Context, err error, fname, entity string) APIError {
+ if entity == "" {
+ return toAPIError(ctx, fmt.Errorf("error exporting %s with: %w", fname, err))
+ }
+ return toAPIError(ctx, fmt.Errorf("error exporting %s from %s with: %w", entity, fname, err))
+}
+
+// wraps import error for more context
+func importError(ctx context.Context, err error, fname, entity string) APIError {
+ if entity == "" {
+ return toAPIError(ctx, fmt.Errorf("error importing %s with: %w", fname, err))
+ }
+ return toAPIError(ctx, fmt.Errorf("error importing %s from %s with: %w", entity, fname, err))
+}
+
+// wraps import error for more context
+func importErrorWithAPIErr(ctx context.Context, apiErr APIErrorCode, err error, fname, entity string) APIError {
+ if entity == "" {
+ return errorCodes.ToAPIErrWithErr(apiErr, fmt.Errorf("error importing %s with: %w", fname, err))
+ }
+ return errorCodes.ToAPIErrWithErr(apiErr, fmt.Errorf("error importing %s from %s with: %w", entity, fname, err))
+}
diff --git a/cmd/admin-handlers-config-kv.go b/cmd/admin-handlers-config-kv.go
new file mode 100644
index 00000000..2169103d
--- /dev/null
+++ b/cmd/admin-handlers-config-kv.go
@@ -0,0 +1,542 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/config"
+ "github.com/minio/minio/internal/config/etcd"
+ xldap "github.com/minio/minio/internal/config/identity/ldap"
+ "github.com/minio/minio/internal/config/identity/openid"
+ idplugin "github.com/minio/minio/internal/config/identity/plugin"
+ polplugin "github.com/minio/minio/internal/config/policy/plugin"
+ "github.com/minio/minio/internal/config/storageclass"
+ "github.com/minio/minio/internal/config/subnet"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/mux"
+ "github.com/minio/pkg/v3/policy"
+)
+
+// DelConfigKVHandler - DELETE /minio/admin/v3/del-config-kv
+func (a adminAPIHandlers) DelConfigKVHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
+ // More than maxConfigSize bytes were available
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
+ return
+ }
+
+ password := cred.SecretKey
+ kvBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
+ if err != nil {
+ adminLogIf(ctx, err)
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
+ return
+ }
+
+ subSys, _, _, err := config.GetSubSys(string(kvBytes))
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ cfg, err := readServerConfig(ctx, objectAPI, nil)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ if err = cfg.DelFrom(bytes.NewReader(kvBytes)); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ if err = validateConfig(ctx, cfg, subSys); err != nil {
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
+ return
+ }
+
+ // Check if subnet proxy being deleted and if so the value of proxy of subnet
+ // target of logger webhook configuration also should be deleted
+ loggerWebhookProxyDeleted := setLoggerWebhookSubnetProxy(subSys, cfg)
+
+ if err = saveServerConfig(ctx, objectAPI, cfg); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // freshly retrieve the config so that default values are loaded for reset config
+ if cfg, err = getValidConfig(objectAPI); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ dynamic := config.SubSystemsDynamic.Contains(subSys)
+ if dynamic {
+ applyDynamic(ctx, objectAPI, cfg, subSys, r, w)
+ if subSys == config.SubnetSubSys && loggerWebhookProxyDeleted {
+ // Logger webhook proxy deleted, apply the dynamic changes
+ applyDynamic(ctx, objectAPI, cfg, config.LoggerWebhookSubSys, r, w)
+ }
+ }
+}
+
+func applyDynamic(ctx context.Context, objectAPI ObjectLayer, cfg config.Config, subSys string,
+ r *http.Request, w http.ResponseWriter,
+) {
+ // Apply dynamic values.
+ if err := applyDynamicConfigForSubSys(GlobalContext, objectAPI, cfg, subSys); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ globalNotificationSys.SignalConfigReload(subSys)
+ // Tell the client that dynamic config was applied.
+ w.Header().Set(madmin.ConfigAppliedHeader, madmin.ConfigAppliedTrue)
+}
+
+type badConfigErr struct {
+ Err error
+}
+
+// Error - return the error message
+func (bce badConfigErr) Error() string {
+ return bce.Err.Error()
+}
+
+// Unwrap the error to its underlying error.
+func (bce badConfigErr) Unwrap() error {
+ return bce.Err
+}
+
+type setConfigResult struct {
+ Cfg config.Config
+ SubSys string
+ Dynamic bool
+ LoggerWebhookCfgUpdated bool
+}
+
+// SetConfigKVHandler - PUT /minio/admin/v3/set-config-kv
+func (a adminAPIHandlers) SetConfigKVHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
+ // More than maxConfigSize bytes were available
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
+ return
+ }
+
+ password := cred.SecretKey
+ kvBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
+ if err != nil {
+ adminLogIf(ctx, err)
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
+ return
+ }
+
+ result, err := setConfigKV(ctx, objectAPI, kvBytes)
+ if err != nil {
+ switch err.(type) {
+ case badConfigErr:
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
+ default:
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ }
+ return
+ }
+
+ if result.Dynamic {
+ applyDynamic(ctx, objectAPI, result.Cfg, result.SubSys, r, w)
+ // If logger webhook config updated (proxy due to callhome), explicitly dynamically
+ // apply the config
+ if result.LoggerWebhookCfgUpdated {
+ applyDynamic(ctx, objectAPI, result.Cfg, config.LoggerWebhookSubSys, r, w)
+ }
+ }
+
+ writeSuccessResponseHeadersOnly(w)
+}
+
+func setConfigKV(ctx context.Context, objectAPI ObjectLayer, kvBytes []byte) (result setConfigResult, err error) {
+ result.Cfg, err = readServerConfig(ctx, objectAPI, nil)
+ if err != nil {
+ return result, err
+ }
+
+ result.Dynamic, err = result.Cfg.ReadConfig(bytes.NewReader(kvBytes))
+ if err != nil {
+ return result, err
+ }
+
+ result.SubSys, _, _, err = config.GetSubSys(string(kvBytes))
+ if err != nil {
+ return result, err
+ }
+
+ tgts, err := config.ParseConfigTargetID(bytes.NewReader(kvBytes))
+ if err != nil {
+ return result, err
+ }
+ ctx = context.WithValue(ctx, config.ContextKeyForTargetFromConfig, tgts)
+ if verr := validateConfig(ctx, result.Cfg, result.SubSys); verr != nil {
+ err = badConfigErr{Err: verr}
+ return result, err
+ }
+
+ // Check if subnet proxy being set and if so set the same value to proxy of subnet
+ // target of logger webhook configuration
+ result.LoggerWebhookCfgUpdated = setLoggerWebhookSubnetProxy(result.SubSys, result.Cfg)
+
+ // Update the actual server config on disk.
+ if err = saveServerConfig(ctx, objectAPI, result.Cfg); err != nil {
+ return result, err
+ }
+
+ // Write the config input KV to history.
+ err = saveServerConfigHistory(ctx, objectAPI, kvBytes)
+ return result, err
+}
+
+// GetConfigKVHandler - GET /minio/admin/v3/get-config-kv?key={key}
+//
+// `key` can be one of three forms:
+// 1. `subsys:target` -> request for config of a single subsystem and target pair.
+// 2. `subsys:` -> request for config of a single subsystem and the default target.
+// 3. `subsys` -> request for config of all targets for the given subsystem.
+//
+// This is a reporting API and config secrets are redacted in the response.
+func (a adminAPIHandlers) GetConfigKVHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ cfg := globalServerConfig.Clone()
+ vars := mux.Vars(r)
+ key := vars["key"]
+
+ var subSys, target string
+ {
+ ws := strings.SplitN(key, madmin.SubSystemSeparator, 2)
+ subSys = ws[0]
+ if len(ws) == 2 {
+ if ws[1] == "" {
+ target = madmin.Default
+ } else {
+ target = ws[1]
+ }
+ }
+ }
+
+ subSysConfigs, err := cfg.GetSubsysInfo(subSys, target, true)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ var s strings.Builder
+ for _, subSysConfig := range subSysConfigs {
+ subSysConfig.WriteTo(&s, false)
+ }
+
+ password := cred.SecretKey
+ econfigData, err := madmin.EncryptData(password, []byte(s.String()))
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, econfigData)
+}
+
+func (a adminAPIHandlers) ClearConfigHistoryKVHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ vars := mux.Vars(r)
+ restoreID := vars["restoreId"]
+ if restoreID == "" {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
+ return
+ }
+ if restoreID == "all" {
+ chEntries, err := listServerConfigHistory(ctx, objectAPI, false, -1)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ for _, chEntry := range chEntries {
+ if err = delServerConfigHistory(ctx, objectAPI, chEntry.RestoreID); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ }
+ } else if err := delServerConfigHistory(ctx, objectAPI, restoreID); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+}
+
+// RestoreConfigHistoryKVHandler - restores a config with KV settings for the given KV id.
+func (a adminAPIHandlers) RestoreConfigHistoryKVHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ vars := mux.Vars(r)
+ restoreID := vars["restoreId"]
+ if restoreID == "" {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
+ return
+ }
+
+ kvBytes, err := readServerConfigHistory(ctx, objectAPI, restoreID)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ cfg, err := readServerConfig(ctx, objectAPI, nil)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ if _, err = cfg.ReadConfig(bytes.NewReader(kvBytes)); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ if err = validateConfig(ctx, cfg, ""); err != nil {
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
+ return
+ }
+
+ if err = saveServerConfig(ctx, objectAPI, cfg); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ delServerConfigHistory(ctx, objectAPI, restoreID)
+}
+
+// ListConfigHistoryKVHandler - lists all the KV ids.
+func (a adminAPIHandlers) ListConfigHistoryKVHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ vars := mux.Vars(r)
+ count, err := strconv.Atoi(vars["count"])
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ chEntries, err := listServerConfigHistory(ctx, objectAPI, true, count)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ data, err := json.Marshal(chEntries)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ password := cred.SecretKey
+ econfigData, err := madmin.EncryptData(password, data)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, econfigData)
+}
+
+// HelpConfigKVHandler - GET /minio/admin/v3/help-config-kv?subSys={subSys}&key={key}
+func (a adminAPIHandlers) HelpConfigKVHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ vars := mux.Vars(r)
+
+ subSys := vars["subSys"]
+ key := vars["key"]
+
+ _, envOnly := r.Form["env"]
+
+ rd, err := GetHelp(subSys, key, envOnly)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ json.NewEncoder(w).Encode(rd)
+}
+
+// SetConfigHandler - PUT /minio/admin/v3/config
+func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
+ // More than maxConfigSize bytes were available
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
+ return
+ }
+
+ password := cred.SecretKey
+ kvBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
+ if err != nil {
+ adminLogIf(ctx, err)
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
+ return
+ }
+
+ cfg := newServerConfig()
+ if _, err = cfg.ReadConfig(bytes.NewReader(kvBytes)); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ if err = validateConfig(ctx, cfg, ""); err != nil {
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
+ return
+ }
+
+ // Update the actual server config on disk.
+ if err = saveServerConfig(ctx, objectAPI, cfg); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Write to the config input KV to history.
+ if err = saveServerConfigHistory(ctx, objectAPI, kvBytes); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseHeadersOnly(w)
+}
+
+// GetConfigHandler - GET /minio/admin/v3/config
+//
+// This endpoint is mainly for exporting and backing up the configuration.
+// Secrets are not redacted.
+func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ cfg := globalServerConfig.Clone()
+
+ var s strings.Builder
+ hkvs := config.HelpSubSysMap[""]
+ for _, hkv := range hkvs {
+ // We ignore the error below, as we cannot get one.
+ cfgSubsysItems, _ := cfg.GetSubsysInfo(hkv.Key, "", false)
+
+ for _, item := range cfgSubsysItems {
+ off := item.Config.Get(config.Enable) == config.EnableOff
+ switch hkv.Key {
+ case config.EtcdSubSys:
+ off = !etcd.Enabled(item.Config)
+ case config.StorageClassSubSys:
+ off = !storageclass.Enabled(item.Config)
+ case config.PolicyPluginSubSys:
+ off = !polplugin.Enabled(item.Config)
+ case config.IdentityOpenIDSubSys:
+ off = !openid.Enabled(item.Config)
+ case config.IdentityLDAPSubSys:
+ off = !xldap.Enabled(item.Config)
+ case config.IdentityTLSSubSys:
+ off = !globalIAMSys.STSTLSConfig.Enabled
+ case config.IdentityPluginSubSys:
+ off = !idplugin.Enabled(item.Config)
+ }
+ item.WriteTo(&s, off)
+ }
+ }
+
+ password := cred.SecretKey
+ econfigData, err := madmin.EncryptData(password, []byte(s.String()))
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, econfigData)
+}
+
+// setLoggerWebhookSubnetProxy - Sets the logger webhook's subnet proxy value to
+// one being set for subnet proxy
+func setLoggerWebhookSubnetProxy(subSys string, cfg config.Config) bool {
+ if subSys == config.SubnetSubSys || subSys == config.LoggerWebhookSubSys {
+ subnetWebhookCfg := cfg[config.LoggerWebhookSubSys][subnet.LoggerWebhookName]
+ loggerWebhookSubnetProxy := subnetWebhookCfg.Get(logger.Proxy)
+ subnetProxy := cfg[config.SubnetSubSys][config.Default].Get(logger.Proxy)
+ if loggerWebhookSubnetProxy != subnetProxy {
+ subnetWebhookCfg.Set(logger.Proxy, subnetProxy)
+ return true
+ }
+ }
+ return false
+}
diff --git a/cmd/admin-handlers-idp-config.go b/cmd/admin-handlers-idp-config.go
new file mode 100644
index 00000000..7b5792f0
--- /dev/null
+++ b/cmd/admin-handlers-idp-config.go
@@ -0,0 +1,439 @@
+// Copyright (c) 2015-2022 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio-go/v7/pkg/set"
+ "github.com/minio/minio/internal/config"
+ cfgldap "github.com/minio/minio/internal/config/identity/ldap"
+ "github.com/minio/minio/internal/config/identity/openid"
+ "github.com/minio/mux"
+ "github.com/minio/pkg/v3/ldap"
+ "github.com/minio/pkg/v3/policy"
+)
+
+func addOrUpdateIDPHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, isUpdate bool) {
+ objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
+ // More than maxConfigSize bytes were available
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
+ return
+ }
+
+ // Ensure body content type is opaque to ensure that request body has not
+ // been interpreted as form data.
+ contentType := r.Header.Get("Content-Type")
+ if contentType != "application/octet-stream" {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
+ return
+ }
+
+ password := cred.SecretKey
+ reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
+ if err != nil {
+ adminLogIf(ctx, err)
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
+ return
+ }
+
+ idpCfgType := mux.Vars(r)["type"]
+ if !madmin.ValidIDPConfigTypes.Contains(idpCfgType) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigInvalidIDPType), r.URL)
+ return
+ }
+
+ var subSys string
+ switch idpCfgType {
+ case madmin.OpenidIDPCfg:
+ subSys = madmin.IdentityOpenIDSubSys
+ case madmin.LDAPIDPCfg:
+ subSys = madmin.IdentityLDAPSubSys
+ }
+
+ cfgName := mux.Vars(r)["name"]
+ cfgTarget := madmin.Default
+ if cfgName != "" {
+ cfgTarget = cfgName
+ if idpCfgType == madmin.LDAPIDPCfg && cfgName != madmin.Default {
+ // LDAP does not support multiple configurations. So cfgName must be
+ // empty or `madmin.Default`.
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigLDAPNonDefaultConfigName), r.URL)
+ return
+ }
+ }
+
+ // Check that this is a valid Create vs Update API call.
+ s := globalServerConfig.Clone()
+ if apiErrCode := handleCreateUpdateValidation(s, subSys, cfgTarget, isUpdate); apiErrCode != ErrNone {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(apiErrCode), r.URL)
+ return
+ }
+
+ cfgData := ""
+ {
+ tgtSuffix := ""
+ if cfgTarget != madmin.Default {
+ tgtSuffix = config.SubSystemSeparator + cfgTarget
+ }
+ cfgData = subSys + tgtSuffix + config.KvSpaceSeparator + string(reqBytes)
+ }
+
+ cfg, err := readServerConfig(ctx, objectAPI, nil)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ dynamic, err := cfg.ReadConfig(strings.NewReader(cfgData))
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // IDP config is not dynamic. Sanity check.
+ if dynamic {
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), "", r.URL)
+ return
+ }
+
+ if err = validateConfig(ctx, cfg, subSys); err != nil {
+ var validationErr ldap.Validation
+ if errors.As(err, &validationErr) {
+ // If we got an LDAP validation error, we need to send appropriate
+ // error message back to client (likely mc).
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigLDAPValidation),
+ validationErr.FormatError(), r.URL)
+ return
+ }
+
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
+ return
+ }
+
+ // Update the actual server config on disk.
+ if err = saveServerConfig(ctx, objectAPI, cfg); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Write to the config input KV to history.
+ if err = saveServerConfigHistory(ctx, objectAPI, []byte(cfgData)); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseHeadersOnly(w)
+}
+
+func handleCreateUpdateValidation(s config.Config, subSys, cfgTarget string, isUpdate bool) APIErrorCode {
+ if cfgTarget != madmin.Default {
+ // This cannot give an error at this point.
+ subSysTargets, _ := s.GetAvailableTargets(subSys)
+ subSysTargetsSet := set.CreateStringSet(subSysTargets...)
+ if isUpdate && !subSysTargetsSet.Contains(cfgTarget) {
+ return ErrAdminConfigIDPCfgNameDoesNotExist
+ }
+ if !isUpdate && subSysTargetsSet.Contains(cfgTarget) {
+ return ErrAdminConfigIDPCfgNameAlreadyExists
+ }
+
+ return ErrNone
+ }
+
+ // For the default configuration name, since it will always be an available
+ // target, we need to check if a configuration value has been set previously
+ // to figure out if this is a valid create or update API call.
+
+ // This cannot really error (FIXME: improve the type for GetConfigInfo)
+ var cfgInfos []madmin.IDPCfgInfo
+ switch subSys {
+ case madmin.IdentityOpenIDSubSys:
+ cfgInfos, _ = globalIAMSys.OpenIDConfig.GetConfigInfo(s, cfgTarget)
+ case madmin.IdentityLDAPSubSys:
+ cfgInfos, _ = globalIAMSys.LDAPConfig.GetConfigInfo(s, cfgTarget)
+ }
+
+ if len(cfgInfos) > 0 && !isUpdate {
+ return ErrAdminConfigIDPCfgNameAlreadyExists
+ }
+ if len(cfgInfos) == 0 && isUpdate {
+ return ErrAdminConfigIDPCfgNameDoesNotExist
+ }
+ return ErrNone
+}
+
+// AddIdentityProviderCfg: adds a new IDP config for openid/ldap.
+//
+// PUT /idp-cfg/openid/dex1 -> create named config `dex1`
+//
+// PUT /idp-cfg/openid/_ -> create (default) named config `_`
+func (a adminAPIHandlers) AddIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ addOrUpdateIDPHandler(ctx, w, r, false)
+}
+
+// UpdateIdentityProviderCfg: updates an existing IDP config for openid/ldap.
+//
+// POST /idp-cfg/openid/dex1 -> update named config `dex1`
+//
+// POST /idp-cfg/openid/_ -> update (default) named config `_`
+func (a adminAPIHandlers) UpdateIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ addOrUpdateIDPHandler(ctx, w, r, true)
+}
+
+// ListIdentityProviderCfg:
+//
+// GET /idp-cfg/openid -> lists openid provider configs.
+func (a adminAPIHandlers) ListIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
+ if objectAPI == nil {
+ return
+ }
+ password := cred.SecretKey
+
+ idpCfgType := mux.Vars(r)["type"]
+ if !madmin.ValidIDPConfigTypes.Contains(idpCfgType) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigInvalidIDPType), r.URL)
+ return
+ }
+
+ var cfgList []madmin.IDPListItem
+ var err error
+ switch idpCfgType {
+ case madmin.OpenidIDPCfg:
+ cfg := globalServerConfig.Clone()
+ cfgList, err = globalIAMSys.OpenIDConfig.GetConfigList(cfg)
+ case madmin.LDAPIDPCfg:
+ cfg := globalServerConfig.Clone()
+ cfgList, err = globalIAMSys.LDAPConfig.GetConfigList(cfg)
+
+ default:
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+ return
+ }
+
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ data, err := json.Marshal(cfgList)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ econfigData, err := madmin.EncryptData(password, data)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, econfigData)
+}
+
+// GetIdentityProviderCfg:
+//
+// GET /idp-cfg/openid/dex_test
+func (a adminAPIHandlers) GetIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ idpCfgType := mux.Vars(r)["type"]
+ cfgName := mux.Vars(r)["name"]
+ password := cred.SecretKey
+
+ if !madmin.ValidIDPConfigTypes.Contains(idpCfgType) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigInvalidIDPType), r.URL)
+ return
+ }
+
+ cfg := globalServerConfig.Clone()
+ var cfgInfos []madmin.IDPCfgInfo
+ var err error
+ switch idpCfgType {
+ case madmin.OpenidIDPCfg:
+ cfgInfos, err = globalIAMSys.OpenIDConfig.GetConfigInfo(cfg, cfgName)
+ case madmin.LDAPIDPCfg:
+ cfgInfos, err = globalIAMSys.LDAPConfig.GetConfigInfo(cfg, cfgName)
+ }
+ if err != nil {
+ if errors.Is(err, openid.ErrProviderConfigNotFound) || errors.Is(err, cfgldap.ErrProviderConfigNotFound) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchConfigTarget), r.URL)
+ return
+ }
+
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ res := madmin.IDPConfig{
+ Type: idpCfgType,
+ Name: cfgName,
+ Info: cfgInfos,
+ }
+ data, err := json.Marshal(res)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ econfigData, err := madmin.EncryptData(password, data)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, econfigData)
+}
+
+// DeleteIdentityProviderCfg:
+//
+// DELETE /idp-cfg/openid/dex_test
+func (a adminAPIHandlers) DeleteIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ idpCfgType := mux.Vars(r)["type"]
+ cfgName := mux.Vars(r)["name"]
+ if !madmin.ValidIDPConfigTypes.Contains(idpCfgType) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigInvalidIDPType), r.URL)
+ return
+ }
+
+ cfgCopy := globalServerConfig.Clone()
+ var subSys string
+ switch idpCfgType {
+ case madmin.OpenidIDPCfg:
+ subSys = config.IdentityOpenIDSubSys
+ cfgInfos, err := globalIAMSys.OpenIDConfig.GetConfigInfo(cfgCopy, cfgName)
+ if err != nil {
+ if errors.Is(err, openid.ErrProviderConfigNotFound) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchConfigTarget), r.URL)
+ return
+ }
+
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ hasEnv := false
+ for _, ci := range cfgInfos {
+ if ci.IsCfg && ci.IsEnv {
+ hasEnv = true
+ break
+ }
+ }
+
+ if hasEnv {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigEnvOverridden), r.URL)
+ return
+ }
+ case madmin.LDAPIDPCfg:
+ subSys = config.IdentityLDAPSubSys
+ cfgInfos, err := globalIAMSys.LDAPConfig.GetConfigInfo(cfgCopy, cfgName)
+ if err != nil {
+ if errors.Is(err, openid.ErrProviderConfigNotFound) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchConfigTarget), r.URL)
+ return
+ }
+
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ hasEnv := false
+ for _, ci := range cfgInfos {
+ if ci.IsCfg && ci.IsEnv {
+ hasEnv = true
+ break
+ }
+ }
+
+ if hasEnv {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigEnvOverridden), r.URL)
+ return
+ }
+ default:
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+ return
+ }
+
+ cfg, err := readServerConfig(ctx, objectAPI, nil)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ cfgKey := fmt.Sprintf("%s:%s", subSys, cfgName)
+ if cfgName == madmin.Default {
+ cfgKey = subSys
+ }
+ if err = cfg.DelKVS(cfgKey); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ if err = validateConfig(ctx, cfg, subSys); err != nil {
+ var validationErr ldap.Validation
+ if errors.As(err, &validationErr) {
+ // If we got an LDAP validation error, we need to send appropriate
+ // error message back to client (likely mc).
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigLDAPValidation),
+ validationErr.FormatError(), r.URL)
+ return
+ }
+
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
+ return
+ }
+ if err = saveServerConfig(ctx, objectAPI, cfg); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ dynamic := config.SubSystemsDynamic.Contains(subSys)
+ if dynamic {
+ applyDynamic(ctx, objectAPI, cfg, subSys, r, w)
+ }
+}
diff --git a/cmd/admin-handlers-idp-ldap.go b/cmd/admin-handlers-idp-ldap.go
new file mode 100644
index 00000000..6807de18
--- /dev/null
+++ b/cmd/admin-handlers-idp-ldap.go
@@ -0,0 +1,657 @@
+// Copyright (c) 2015-2022 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/auth"
+ "github.com/minio/mux"
+ xldap "github.com/minio/pkg/v3/ldap"
+ "github.com/minio/pkg/v3/policy"
+)
+
+// ListLDAPPolicyMappingEntities lists users/groups mapped to given/all policies.
+//
+// GET /idp/ldap/policy-entities?[query-params]
+//
+// Query params:
+//
+// user=... -> repeatable query parameter, specifying users to query for
+// policy mapping
+//
+// group=... -> repeatable query parameter, specifying groups to query for
+// policy mapping
+//
+// policy=... -> repeatable query parameter, specifying policy to query for
+// user/group mapping
+//
+// When all query parameters are omitted, returns mappings for all policies.
+func (a adminAPIHandlers) ListLDAPPolicyMappingEntities(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ // Check authorization.
+
+ objectAPI, cred := validateAdminReq(ctx, w, r,
+ policy.ListGroupsAdminAction, policy.ListUsersAdminAction, policy.ListUserPoliciesAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ // Validate API arguments.
+
+ q := madmin.PolicyEntitiesQuery{
+ Users: r.Form["user"],
+ Groups: r.Form["group"],
+ Policy: r.Form["policy"],
+ }
+
+ // Query IAM
+
+ res, err := globalIAMSys.QueryLDAPPolicyEntities(r.Context(), q)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Encode result and send response.
+
+ data, err := json.Marshal(res)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ password := cred.SecretKey
+ econfigData, err := madmin.EncryptData(password, data)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ writeSuccessResponseJSON(w, econfigData)
+}
+
+// AttachDetachPolicyLDAP attaches or detaches policies from an LDAP entity
+// (user or group).
+//
+// POST /idp/ldap/policy/{operation}
+func (a adminAPIHandlers) AttachDetachPolicyLDAP(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ // Check authorization.
+
+ objectAPI, cred := validateAdminReq(ctx, w, r, policy.UpdatePolicyAssociationAction)
+ if objectAPI == nil {
+ return
+ }
+
+ // fail if ldap is not enabled
+ if !globalIAMSys.LDAPConfig.Enabled() {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminLDAPNotEnabled), r.URL)
+ return
+ }
+
+ if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
+ // More than maxConfigSize bytes were available
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
+ return
+ }
+
+ // Ensure body content type is opaque to ensure that request body has not
+ // been interpreted as form data.
+ contentType := r.Header.Get("Content-Type")
+ if contentType != "application/octet-stream" {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
+ return
+ }
+
+ // Validate operation
+ operation := mux.Vars(r)["operation"]
+ if operation != "attach" && operation != "detach" {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminInvalidArgument), r.URL)
+ return
+ }
+
+ isAttach := operation == "attach"
+
+ // Validate API arguments in body.
+ password := cred.SecretKey
+ reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
+ if err != nil {
+ adminLogIf(ctx, err)
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
+ return
+ }
+
+ var par madmin.PolicyAssociationReq
+ err = json.Unmarshal(reqBytes, &par)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
+ return
+ }
+
+ if err := par.IsValid(); err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
+ return
+ }
+
+ // Call IAM subsystem
+ updatedAt, addedOrRemoved, _, err := globalIAMSys.PolicyDBUpdateLDAP(ctx, isAttach, par)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ respBody := madmin.PolicyAssociationResp{
+ UpdatedAt: updatedAt,
+ }
+ if isAttach {
+ respBody.PoliciesAttached = addedOrRemoved
+ } else {
+ respBody.PoliciesDetached = addedOrRemoved
+ }
+
+ data, err := json.Marshal(respBody)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ encryptedData, err := madmin.EncryptData(password, data)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, encryptedData)
+}
+
+// AddServiceAccountLDAP adds a new service account for provided LDAP username or DN
+//
+// PUT /minio/admin/v3/idp/ldap/add-service-account
+func (a adminAPIHandlers) AddServiceAccountLDAP(w http.ResponseWriter, r *http.Request) {
+ ctx, cred, opts, createReq, targetUser, APIError := commonAddServiceAccount(r, true)
+ if APIError.Code != "" {
+ writeErrorResponseJSON(ctx, w, APIError, r.URL)
+ return
+ }
+
+ // fail if ldap is not enabled
+ if !globalIAMSys.LDAPConfig.Enabled() {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminLDAPNotEnabled), r.URL)
+ return
+ }
+
+ // Find the user for the request sender (as it may be sent via a service
+ // account or STS account):
+ requestorUser := cred.AccessKey
+ requestorParentUser := cred.AccessKey
+ requestorGroups := cred.Groups
+ requestorIsDerivedCredential := false
+ if cred.IsServiceAccount() || cred.IsTemp() {
+ requestorParentUser = cred.ParentUser
+ requestorIsDerivedCredential = true
+ }
+
+ // Check if we are creating svc account for request sender.
+ isSvcAccForRequestor := targetUser == requestorUser || targetUser == requestorParentUser
+
+ var (
+ targetGroups []string
+ err error
+ )
+
+ // If we are creating svc account for request sender, ensure that targetUser
+ // is a real user (i.e. not derived credentials).
+ if isSvcAccForRequestor {
+ if requestorIsDerivedCredential {
+ if requestorParentUser == "" {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx,
+ errors.New("service accounts cannot be generated for temporary credentials without parent")), r.URL)
+ return
+ }
+ targetUser = requestorParentUser
+ }
+ targetGroups = requestorGroups
+
+ // Deny if the target user is not LDAP
+ foundResult, err := globalIAMSys.LDAPConfig.GetValidatedDNForUsername(targetUser)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ if foundResult == nil {
+ err := errors.New("Specified user does not exist on LDAP server")
+ APIErr := errorCodes.ToAPIErrWithErr(ErrAdminNoSuchUser, err)
+ writeErrorResponseJSON(ctx, w, APIErr, r.URL)
+ return
+ }
+
+ // In case of LDAP/OIDC we need to set `opts.claims` to ensure
+ // it is associated with the LDAP/OIDC user properly.
+ for k, v := range cred.Claims {
+ if k == expClaim {
+ continue
+ }
+ opts.claims[k] = v
+ }
+ } else {
+ // We still need to ensure that the target user is a valid LDAP user.
+ //
+ // The target user may be supplied as a (short) username or a DN.
+ // However, for now, we only support using the short username.
+
+ isDN := globalIAMSys.LDAPConfig.ParsesAsDN(targetUser)
+ opts.claims[ldapUserN] = targetUser // simple username
+ var lookupResult *xldap.DNSearchResult
+ lookupResult, targetGroups, err = globalIAMSys.LDAPConfig.LookupUserDN(targetUser)
+ if err != nil {
+ // if not found, check if DN
+ if strings.Contains(err.Error(), "User DN not found for:") {
+ if isDN {
+ // warn user that DNs are not allowed
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminLDAPExpectedLoginName, err), r.URL)
+ } else {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminNoSuchUser, err), r.URL)
+ }
+ }
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ targetUser = lookupResult.NormDN
+ opts.claims[ldapUser] = targetUser // DN
+ opts.claims[ldapActualUser] = lookupResult.ActualDN
+
+ // Check if this user or their groups have a policy applied.
+ ldapPolicies, err := globalIAMSys.PolicyDBGet(targetUser, targetGroups...)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ if len(ldapPolicies) == 0 {
+ err = fmt.Errorf("No policy set for user `%s` or any of their groups: `%s`", opts.claims[ldapActualUser], strings.Join(targetGroups, "`,`"))
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminNoSuchUser, err), r.URL)
+ return
+ }
+
+ // Add LDAP attributes that were looked up into the claims.
+ for attribKey, attribValue := range lookupResult.Attributes {
+ opts.claims[ldapAttribPrefix+attribKey] = attribValue
+ }
+ }
+
+ newCred, updatedAt, err := globalIAMSys.NewServiceAccount(ctx, targetUser, targetGroups, opts)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ createResp := madmin.AddServiceAccountResp{
+ Credentials: madmin.Credentials{
+ AccessKey: newCred.AccessKey,
+ SecretKey: newCred.SecretKey,
+ Expiration: newCred.Expiration,
+ },
+ }
+
+ data, err := json.Marshal(createResp)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, encryptedData)
+
+ // Call hook for cluster-replication if the service account is not for a
+ // root user.
+ if newCred.ParentUser != globalActiveCred.AccessKey {
+ replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
+ Type: madmin.SRIAMItemSvcAcc,
+ SvcAccChange: &madmin.SRSvcAccChange{
+ Create: &madmin.SRSvcAccCreate{
+ Parent: newCred.ParentUser,
+ AccessKey: newCred.AccessKey,
+ SecretKey: newCred.SecretKey,
+ Groups: newCred.Groups,
+ Name: newCred.Name,
+ Description: newCred.Description,
+ Claims: opts.claims,
+ SessionPolicy: madmin.SRSessionPolicy(createReq.Policy),
+ Status: auth.AccountOn,
+ Expiration: createReq.Expiration,
+ },
+ },
+ UpdatedAt: updatedAt,
+ }))
+ }
+}
+
+// ListAccessKeysLDAP - GET /minio/admin/v3/idp/ldap/list-access-keys
+func (a adminAPIHandlers) ListAccessKeysLDAP(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
+ }
+
+ userDN := r.Form.Get("userDN")
+
+ // If listing is requested for a specific user (who is not the request
+ // sender), check that the user has permissions.
+ if userDN != "" && userDN != cred.ParentUser {
+ if !globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.ListServiceAccountsAdminAction,
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ Claims: cred.Claims,
+ }) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+ } else {
+ if !globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.ListServiceAccountsAdminAction,
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ Claims: cred.Claims,
+ DenyOnly: true,
+ }) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+ userDN = cred.AccessKey
+ if cred.ParentUser != "" {
+ userDN = cred.ParentUser
+ }
+ }
+
+ dnResult, err := globalIAMSys.LDAPConfig.GetValidatedDNForUsername(userDN)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ if dnResult == nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errNoSuchUser), r.URL)
+ return
+ }
+ targetAccount := dnResult.NormDN
+
+ listType := r.Form.Get("listType")
+ if listType != "sts-only" && listType != "svcacc-only" && listType != "" {
+ // default to both
+ listType = ""
+ }
+
+ var serviceAccounts []auth.Credentials
+ var stsKeys []auth.Credentials
+
+ if listType == "" || listType == "sts-only" {
+ stsKeys, err = globalIAMSys.ListSTSAccounts(ctx, targetAccount)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ }
+ if listType == "" || listType == "svcacc-only" {
+ serviceAccounts, err = globalIAMSys.ListServiceAccounts(ctx, targetAccount)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ }
+
+ var serviceAccountList []madmin.ServiceAccountInfo
+ var stsKeyList []madmin.ServiceAccountInfo
+
+ for _, svc := range serviceAccounts {
+ expiryTime := svc.Expiration
+ serviceAccountList = append(serviceAccountList, madmin.ServiceAccountInfo{
+ AccessKey: svc.AccessKey,
+ Expiration: &expiryTime,
+ Name: svc.Name,
+ Description: svc.Description,
+ })
+ }
+ for _, sts := range stsKeys {
+ expiryTime := sts.Expiration
+ stsKeyList = append(stsKeyList, madmin.ServiceAccountInfo{
+ AccessKey: sts.AccessKey,
+ Expiration: &expiryTime,
+ })
+ }
+
+ listResp := madmin.ListAccessKeysLDAPResp{
+ ServiceAccounts: serviceAccountList,
+ STSKeys: stsKeyList,
+ }
+
+ data, err := json.Marshal(listResp)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, encryptedData)
+}
+
+// ListAccessKeysLDAPBulk - GET /minio/admin/v3/idp/ldap/list-access-keys-bulk
+func (a adminAPIHandlers) ListAccessKeysLDAPBulk(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
+ }
+
+ dnList := r.Form["userDNs"]
+ isAll := r.Form.Get("all") == "true"
+ selfOnly := !isAll && len(dnList) == 0
+
+ if isAll && len(dnList) > 0 {
+ // This should be checked on client side, so return generic error
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
+ return
+ }
+
+ // Empty DN list and not self, list access keys for all users
+ if isAll {
+ if !globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.ListUsersAdminAction,
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ Claims: cred.Claims,
+ }) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+ } else if len(dnList) == 1 {
+ var dn string
+ foundResult, err := globalIAMSys.LDAPConfig.GetValidatedDNForUsername(dnList[0])
+ if err == nil {
+ dn = foundResult.NormDN
+ }
+ if dn == cred.ParentUser || dnList[0] == cred.ParentUser {
+ selfOnly = true
+ }
+ }
+
+ if !globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.ListServiceAccountsAdminAction,
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ Claims: cred.Claims,
+ DenyOnly: selfOnly,
+ }) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+
+ if selfOnly && len(dnList) == 0 {
+ selfDN := cred.AccessKey
+ if cred.ParentUser != "" {
+ selfDN = cred.ParentUser
+ }
+ dnList = append(dnList, selfDN)
+ }
+
+ var ldapUserList []string
+ if isAll {
+ ldapUsers, err := globalIAMSys.ListLDAPUsers(ctx)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ for user := range ldapUsers {
+ ldapUserList = append(ldapUserList, user)
+ }
+ } else {
+ for _, userDN := range dnList {
+ // Validate the userDN
+ foundResult, err := globalIAMSys.LDAPConfig.GetValidatedDNForUsername(userDN)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ if foundResult == nil {
+ continue
+ }
+ ldapUserList = append(ldapUserList, foundResult.NormDN)
+ }
+ }
+
+ listType := r.Form.Get("listType")
+ var listSTSKeys, listServiceAccounts bool
+ switch listType {
+ case madmin.AccessKeyListUsersOnly:
+ listSTSKeys = false
+ listServiceAccounts = false
+ case madmin.AccessKeyListSTSOnly:
+ listSTSKeys = true
+ listServiceAccounts = false
+ case madmin.AccessKeyListSvcaccOnly:
+ listSTSKeys = false
+ listServiceAccounts = true
+ case madmin.AccessKeyListAll:
+ listSTSKeys = true
+ listServiceAccounts = true
+ default:
+ err := errors.New("invalid list type")
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrInvalidRequest, err), r.URL)
+ return
+ }
+
+ accessKeyMap := make(map[string]madmin.ListAccessKeysLDAPResp)
+ for _, internalDN := range ldapUserList {
+ externalDN := globalIAMSys.LDAPConfig.DecodeDN(internalDN)
+ accessKeys := madmin.ListAccessKeysLDAPResp{}
+ if listSTSKeys {
+ stsKeys, err := globalIAMSys.ListSTSAccounts(ctx, internalDN)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ for _, sts := range stsKeys {
+ accessKeys.STSKeys = append(accessKeys.STSKeys, madmin.ServiceAccountInfo{
+ AccessKey: sts.AccessKey,
+ Expiration: &sts.Expiration,
+ })
+ }
+ // if only STS keys, skip if user has no STS keys
+ if !listServiceAccounts && len(stsKeys) == 0 {
+ continue
+ }
+ }
+
+ if listServiceAccounts {
+ serviceAccounts, err := globalIAMSys.ListServiceAccounts(ctx, internalDN)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ for _, svc := range serviceAccounts {
+ accessKeys.ServiceAccounts = append(accessKeys.ServiceAccounts, madmin.ServiceAccountInfo{
+ AccessKey: svc.AccessKey,
+ Expiration: &svc.Expiration,
+ Name: svc.Name,
+ Description: svc.Description,
+ })
+ }
+ // if only service accounts, skip if user has no service accounts
+ if !listSTSKeys && len(serviceAccounts) == 0 {
+ continue
+ }
+ }
+ accessKeyMap[externalDN] = accessKeys
+ }
+
+ data, err := json.Marshal(accessKeyMap)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, encryptedData)
+}
diff --git a/cmd/admin-handlers-idp-openid.go b/cmd/admin-handlers-idp-openid.go
new file mode 100644
index 00000000..7e238783
--- /dev/null
+++ b/cmd/admin-handlers-idp-openid.go
@@ -0,0 +1,248 @@
+// Copyright (c) 2015-2025 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "encoding/json"
+ "errors"
+ "net/http"
+ "sort"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio-go/v7/pkg/set"
+ "github.com/minio/pkg/v3/policy"
+)
+
+const dummyRoleARN = "dummy-internal"
+
+// ListAccessKeysOpenIDBulk - GET /minio/admin/v3/idp/openid/list-access-keys-bulk
+func (a adminAPIHandlers) ListAccessKeysOpenIDBulk(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
+ }
+
+ if !globalIAMSys.OpenIDConfig.Enabled {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminOpenIDNotEnabled), r.URL)
+ return
+ }
+
+ userList := r.Form["users"]
+ isAll := r.Form.Get("all") == "true"
+ selfOnly := !isAll && len(userList) == 0
+ cfgName := r.Form.Get("configName")
+ allConfigs := r.Form.Get("allConfigs") == "true"
+ if cfgName == "" && !allConfigs {
+ cfgName = madmin.Default
+ }
+
+ if isAll && len(userList) > 0 {
+ // This should be checked on client side, so return generic error
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
+ return
+ }
+
+ // Empty DN list and not self, list access keys for all users
+ if isAll {
+ if !globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.ListUsersAdminAction,
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ Claims: cred.Claims,
+ }) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+ } else if len(userList) == 1 && userList[0] == cred.ParentUser {
+ selfOnly = true
+ }
+
+ if !globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.ListServiceAccountsAdminAction,
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ Claims: cred.Claims,
+ DenyOnly: selfOnly,
+ }) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+
+ if selfOnly && len(userList) == 0 {
+ selfDN := cred.AccessKey
+ if cred.ParentUser != "" {
+ selfDN = cred.ParentUser
+ }
+ userList = append(userList, selfDN)
+ }
+
+ listType := r.Form.Get("listType")
+ var listSTSKeys, listServiceAccounts bool
+ switch listType {
+ case madmin.AccessKeyListUsersOnly:
+ listSTSKeys = false
+ listServiceAccounts = false
+ case madmin.AccessKeyListSTSOnly:
+ listSTSKeys = true
+ listServiceAccounts = false
+ case madmin.AccessKeyListSvcaccOnly:
+ listSTSKeys = false
+ listServiceAccounts = true
+ case madmin.AccessKeyListAll:
+ listSTSKeys = true
+ listServiceAccounts = true
+ default:
+ err := errors.New("invalid list type")
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrInvalidRequest, err), r.URL)
+ return
+ }
+
+ s := globalServerConfig.Clone()
+ roleArnMap := make(map[string]string)
+ // Map of configs to a map of users to their access keys
+ cfgToUsersMap := make(map[string]map[string]madmin.OpenIDUserAccessKeys)
+ configs, err := globalIAMSys.OpenIDConfig.GetConfigList(s)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ for _, config := range configs {
+ if !allConfigs && cfgName != config.Name {
+ continue
+ }
+ arn := dummyRoleARN
+ if config.RoleARN != "" {
+ arn = config.RoleARN
+ }
+ roleArnMap[arn] = config.Name
+ newResp := make(map[string]madmin.OpenIDUserAccessKeys)
+ cfgToUsersMap[config.Name] = newResp
+ }
+ if len(roleArnMap) == 0 {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchConfigTarget), r.URL)
+ return
+ }
+
+ userSet := set.CreateStringSet(userList...)
+ accessKeys, err := globalIAMSys.ListAllAccessKeys(ctx)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ for _, accessKey := range accessKeys {
+ // Filter out any disqualifying access keys
+ _, ok := accessKey.Claims[subClaim]
+ if !ok {
+ continue // OpenID access keys must have a sub claim
+ }
+ if (!listSTSKeys && !accessKey.IsServiceAccount()) || (!listServiceAccounts && accessKey.IsServiceAccount()) {
+ continue // skip if not the type we want
+ }
+ arn, ok := accessKey.Claims[roleArnClaim].(string)
+ if !ok {
+ if _, ok := accessKey.Claims[iamPolicyClaimNameOpenID()]; !ok {
+ continue // skip if no roleArn and no policy claim
+ }
+ // claim-based provider is in the roleArnMap under dummy ARN
+ arn = dummyRoleARN
+ }
+ matchingCfgName, ok := roleArnMap[arn]
+ if !ok {
+ continue // skip if not part of the target config
+ }
+ var id string
+ if idClaim := globalIAMSys.OpenIDConfig.GetUserIDClaim(matchingCfgName); idClaim != "" {
+ id, _ = accessKey.Claims[idClaim].(string)
+ }
+ if !userSet.IsEmpty() && !userSet.Contains(accessKey.ParentUser) && !userSet.Contains(id) {
+ continue // skip if not in the user list
+ }
+ openIDUserAccessKeys, ok := cfgToUsersMap[matchingCfgName][accessKey.ParentUser]
+
+ // Add new user to map if not already present
+ if !ok {
+ var readableClaim string
+ if rc := globalIAMSys.OpenIDConfig.GetUserReadableClaim(matchingCfgName); rc != "" {
+ readableClaim, _ = accessKey.Claims[rc].(string)
+ }
+ openIDUserAccessKeys = madmin.OpenIDUserAccessKeys{
+ MinioAccessKey: accessKey.ParentUser,
+ ID: id,
+ ReadableName: readableClaim,
+ }
+ }
+ svcAccInfo := madmin.ServiceAccountInfo{
+ AccessKey: accessKey.AccessKey,
+ Expiration: &accessKey.Expiration,
+ }
+ if accessKey.IsServiceAccount() {
+ openIDUserAccessKeys.ServiceAccounts = append(openIDUserAccessKeys.ServiceAccounts, svcAccInfo)
+ } else {
+ openIDUserAccessKeys.STSKeys = append(openIDUserAccessKeys.STSKeys, svcAccInfo)
+ }
+ cfgToUsersMap[matchingCfgName][accessKey.ParentUser] = openIDUserAccessKeys
+ }
+
+ // Convert map to slice and sort
+ resp := make([]madmin.ListAccessKeysOpenIDResp, 0, len(cfgToUsersMap))
+ for cfgName, usersMap := range cfgToUsersMap {
+ users := make([]madmin.OpenIDUserAccessKeys, 0, len(usersMap))
+ for _, user := range usersMap {
+ users = append(users, user)
+ }
+ sort.Slice(users, func(i, j int) bool {
+ return users[i].MinioAccessKey < users[j].MinioAccessKey
+ })
+ resp = append(resp, madmin.ListAccessKeysOpenIDResp{
+ ConfigName: cfgName,
+ Users: users,
+ })
+ }
+ sort.Slice(resp, func(i, j int) bool {
+ return resp[i].ConfigName < resp[j].ConfigName
+ })
+
+ data, err := json.Marshal(resp)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, encryptedData)
+}
diff --git a/cmd/admin-handlers-pools.go b/cmd/admin-handlers-pools.go
new file mode 100644
index 00000000..c4f98c45
--- /dev/null
+++ b/cmd/admin-handlers-pools.go
@@ -0,0 +1,393 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "github.com/minio/mux"
+ "github.com/minio/pkg/v3/env"
+ "github.com/minio/pkg/v3/policy"
+)
+
+var (
+ errRebalanceDecommissionAlreadyRunning = errors.New("Rebalance cannot be started, decommission is already in progress")
+ errDecommissionRebalanceAlreadyRunning = errors.New("Decommission cannot be started, rebalance is already in progress")
+)
+
+func (a adminAPIHandlers) StartDecommission(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.DecommissionAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ // Legacy args style such as non-ellipses style is not supported with this API.
+ if globalEndpoints.Legacy() {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+ return
+ }
+
+ z, ok := objectAPI.(*erasureServerPools)
+ if !ok || len(z.serverPools) == 1 {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+ return
+ }
+
+ if z.IsDecommissionRunning() {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errDecommissionAlreadyRunning), r.URL)
+ return
+ }
+
+ if z.IsRebalanceStarted(ctx) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminRebalanceAlreadyStarted), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ v := vars["pool"]
+ byID := vars["by-id"] == "true"
+
+ pools := strings.Split(v, ",")
+ poolIndices := make([]int, 0, len(pools))
+
+ for _, pool := range pools {
+ var idx int
+ if byID {
+ var err error
+ idx, err = strconv.Atoi(pool)
+ if err != nil {
+ // We didn't find any matching pools, invalid input
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL)
+ return
+ }
+ } else {
+ idx = globalEndpoints.GetPoolIdx(pool)
+ if idx == -1 {
+ // We didn't find any matching pools, invalid input
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL)
+ return
+ }
+ }
+ var pool *erasureSets
+ for pidx := range z.serverPools {
+ if pidx == idx {
+ pool = z.serverPools[idx]
+ break
+ }
+ }
+ if pool == nil {
+ // We didn't find any matching pools, invalid input
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL)
+ return
+ }
+
+ poolIndices = append(poolIndices, idx)
+ }
+
+ if len(poolIndices) == 0 || !proxyDecommissionRequest(ctx, globalEndpoints[poolIndices[0]].Endpoints[0], w, r) {
+ if err := z.Decommission(r.Context(), poolIndices...); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ }
+}
+
+func (a adminAPIHandlers) CancelDecommission(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.DecommissionAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ // Legacy args style such as non-ellipses style is not supported with this API.
+ if globalEndpoints.Legacy() {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+ return
+ }
+
+ pools, ok := objectAPI.(*erasureServerPools)
+ if !ok {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ v := vars["pool"]
+ byID := vars["by-id"] == "true"
+ idx := -1
+
+ if byID {
+ if i, err := strconv.Atoi(v); err == nil && i >= 0 && i < len(globalEndpoints) {
+ idx = i
+ }
+ } else {
+ idx = globalEndpoints.GetPoolIdx(v)
+ }
+
+ if idx == -1 {
+ // We didn't find any matching pools, invalid input
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL)
+ return
+ }
+
+ if !proxyDecommissionRequest(ctx, globalEndpoints[idx].Endpoints[0], w, r) {
+ if err := pools.DecommissionCancel(ctx, idx); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ }
+}
+
+func (a adminAPIHandlers) StatusPool(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.ServerInfoAdminAction, policy.DecommissionAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ // Legacy args style such as non-ellipses style is not supported with this API.
+ if globalEndpoints.Legacy() {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+ return
+ }
+
+ pools, ok := objectAPI.(*erasureServerPools)
+ if !ok {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ v := vars["pool"]
+ byID := vars["by-id"] == "true"
+ idx := -1
+
+ if byID {
+ if i, err := strconv.Atoi(v); err == nil && i >= 0 && i < len(globalEndpoints) {
+ idx = i
+ }
+ } else {
+ idx = globalEndpoints.GetPoolIdx(v)
+ }
+
+ if idx == -1 {
+ apiErr := toAdminAPIErr(ctx, errInvalidArgument)
+ apiErr.Description = fmt.Sprintf("specified pool '%s' not found, please specify a valid pool", v)
+ // We didn't find any matching pools, invalid input
+ writeErrorResponseJSON(ctx, w, apiErr, r.URL)
+ return
+ }
+
+ status, err := pools.Status(r.Context(), idx)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ adminLogIf(r.Context(), json.NewEncoder(w).Encode(&status))
+}
+
+func (a adminAPIHandlers) ListPools(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.ServerInfoAdminAction, policy.DecommissionAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ // Legacy args style such as non-ellipses style is not supported with this API.
+ if globalEndpoints.Legacy() {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+ return
+ }
+
+ pools, ok := objectAPI.(*erasureServerPools)
+ if !ok {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+ return
+ }
+
+ poolsStatus := make([]PoolStatus, len(globalEndpoints))
+ for idx := range globalEndpoints {
+ status, err := pools.Status(r.Context(), idx)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ poolsStatus[idx] = status
+ }
+
+ adminLogIf(r.Context(), json.NewEncoder(w).Encode(poolsStatus))
+}
+
+func (a adminAPIHandlers) RebalanceStart(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.RebalanceAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ // NB rebalance-start admin API is always coordinated from first pool's
+ // first node. The following is required to serialize (the effects of)
+ // concurrent rebalance-start commands.
+ if ep := globalEndpoints[0].Endpoints[0]; !ep.IsLocal {
+ for nodeIdx, proxyEp := range globalProxyEndpoints {
+ if proxyEp.Host == ep.Host {
+ if proxied, success := proxyRequestByNodeIndex(ctx, w, r, nodeIdx, false); proxied && success {
+ return
+ }
+ }
+ }
+ }
+
+ pools, ok := objectAPI.(*erasureServerPools)
+ if !ok || len(pools.serverPools) == 1 {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+ return
+ }
+
+ if pools.IsDecommissionRunning() {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errRebalanceDecommissionAlreadyRunning), r.URL)
+ return
+ }
+
+ if pools.IsRebalanceStarted(ctx) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminRebalanceAlreadyStarted), r.URL)
+ return
+ }
+
+ bucketInfos, err := objectAPI.ListBuckets(ctx, BucketOptions{})
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ buckets := make([]string, 0, len(bucketInfos))
+ for _, bInfo := range bucketInfos {
+ buckets = append(buckets, bInfo.Name)
+ }
+
+ var id string
+ if id, err = pools.initRebalanceMeta(ctx, buckets); err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Rebalance routine is run on the first node of any pool participating in rebalance.
+ pools.StartRebalance()
+
+ b, err := json.Marshal(struct {
+ ID string `json:"id"`
+ }{ID: id})
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, b)
+ // Notify peers to load rebalance.bin and start rebalance routine if they happen to be
+ // participating pool's leader node
+ globalNotificationSys.LoadRebalanceMeta(ctx, true)
+}
+
+func (a adminAPIHandlers) RebalanceStatus(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.RebalanceAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ // Proxy rebalance-status to first pool first node, so that users see a
+ // consistent view of rebalance progress even though different rebalancing
+ // pools may temporarily have out of date info on the others.
+ if ep := globalEndpoints[0].Endpoints[0]; !ep.IsLocal {
+ for nodeIdx, proxyEp := range globalProxyEndpoints {
+ if proxyEp.Host == ep.Host {
+ if proxied, success := proxyRequestByNodeIndex(ctx, w, r, nodeIdx, false); proxied && success {
+ return
+ }
+ }
+ }
+ }
+
+ pools, ok := objectAPI.(*erasureServerPools)
+ if !ok {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+ return
+ }
+
+ rs, err := rebalanceStatus(ctx, pools)
+ if err != nil {
+ if errors.Is(err, errRebalanceNotStarted) || errors.Is(err, errConfigNotFound) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminRebalanceNotStarted), r.URL)
+ return
+ }
+ adminLogIf(ctx, fmt.Errorf("failed to fetch rebalance status: %w", err))
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ adminLogIf(r.Context(), json.NewEncoder(w).Encode(rs))
+}
+
+func (a adminAPIHandlers) RebalanceStop(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.RebalanceAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ pools, ok := objectAPI.(*erasureServerPools)
+ if !ok {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+ return
+ }
+
+ // Cancel any ongoing rebalance operation
+ globalNotificationSys.StopRebalance(r.Context())
+ writeSuccessResponseHeadersOnly(w)
+ adminLogIf(ctx, pools.saveRebalanceStats(GlobalContext, 0, rebalSaveStoppedAt))
+ globalNotificationSys.LoadRebalanceMeta(ctx, false)
+}
+
+func proxyDecommissionRequest(ctx context.Context, defaultEndPoint Endpoint, w http.ResponseWriter, r *http.Request) (proxy bool) {
+ host := env.Get("_MINIO_DECOM_ENDPOINT_HOST", defaultEndPoint.Host)
+ if host == "" {
+ return proxy
+ }
+ for nodeIdx, proxyEp := range globalProxyEndpoints {
+ if proxyEp.Host == host && !proxyEp.IsLocal {
+ if proxied, success := proxyRequestByNodeIndex(ctx, w, r, nodeIdx, false); proxied && success {
+ return true
+ }
+ }
+ }
+ return proxy
+}
diff --git a/cmd/admin-handlers-site-replication.go b/cmd/admin-handlers-site-replication.go
new file mode 100644
index 00000000..bda09395
--- /dev/null
+++ b/cmd/admin-handlers-site-replication.go
@@ -0,0 +1,623 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "encoding/gob"
+ "encoding/json"
+ "errors"
+ "io"
+ "net/http"
+ "strings"
+ "sync/atomic"
+ "time"
+
+ "github.com/dustin/go-humanize"
+ "github.com/minio/madmin-go/v3"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/mux"
+ "github.com/minio/pkg/v3/policy"
+)
+
+// SiteReplicationAdd - PUT /minio/admin/v3/site-replication/add
+func (a adminAPIHandlers) SiteReplicationAdd(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, cred := validateAdminReq(ctx, w, r, policy.SiteReplicationAddAction)
+ if objectAPI == nil {
+ return
+ }
+
+ var sites []madmin.PeerSite
+ if err := parseJSONBody(ctx, r.Body, &sites, cred.SecretKey); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ opts := getSRAddOptions(r)
+ status, err := globalSiteReplicationSys.AddPeerClusters(ctx, sites, opts)
+ if err != nil {
+ adminLogIf(ctx, err)
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ body, err := json.Marshal(status)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, body)
+}
+
+func getSRAddOptions(r *http.Request) (opts madmin.SRAddOptions) {
+ opts.ReplicateILMExpiry = r.Form.Get("replicateILMExpiry") == "true"
+ return opts
+}
+
+// SRPeerJoin - PUT /minio/admin/v3/site-replication/join
+//
+// used internally to tell current cluster to enable SR with
+// the provided peer clusters and service account.
+func (a adminAPIHandlers) SRPeerJoin(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, cred := validateAdminReq(ctx, w, r, policy.SiteReplicationAddAction)
+ if objectAPI == nil {
+ return
+ }
+
+ var joinArg madmin.SRPeerJoinReq
+ if err := parseJSONBody(ctx, r.Body, &joinArg, cred.SecretKey); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ if err := globalSiteReplicationSys.PeerJoinReq(ctx, joinArg); err != nil {
+ adminLogIf(ctx, err)
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+}
+
+// SRPeerBucketOps - PUT /minio/admin/v3/site-replication/bucket-ops?bucket=x&operation=y
+func (a adminAPIHandlers) SRPeerBucketOps(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationOperationAction)
+ if objectAPI == nil {
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+ operation := madmin.BktOp(vars["operation"])
+
+ var err error
+ switch operation {
+ default:
+ err = errSRInvalidRequest(errInvalidArgument)
+ case madmin.MakeWithVersioningBktOp:
+ createdAt, cerr := time.Parse(time.RFC3339Nano, strings.TrimSpace(r.Form.Get("createdAt")))
+ if cerr != nil {
+ createdAt = timeSentinel
+ }
+
+ opts := MakeBucketOptions{
+ LockEnabled: r.Form.Get("lockEnabled") == "true",
+ VersioningEnabled: r.Form.Get("versioningEnabled") == "true",
+ ForceCreate: r.Form.Get("forceCreate") == "true",
+ CreatedAt: createdAt,
+ }
+ err = globalSiteReplicationSys.PeerBucketMakeWithVersioningHandler(ctx, bucket, opts)
+ case madmin.ConfigureReplBktOp:
+ err = globalSiteReplicationSys.PeerBucketConfigureReplHandler(ctx, bucket)
+ case madmin.DeleteBucketBktOp, madmin.ForceDeleteBucketBktOp:
+ err = globalSiteReplicationSys.PeerBucketDeleteHandler(ctx, bucket, DeleteBucketOptions{
+ Force: operation == madmin.ForceDeleteBucketBktOp,
+ SRDeleteOp: getSRBucketDeleteOp(true),
+ })
+ case madmin.PurgeDeletedBucketOp:
+ globalSiteReplicationSys.purgeDeletedBucket(ctx, objectAPI, bucket)
+ }
+ if err != nil {
+ adminLogIf(ctx, err)
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+}
+
+// SRPeerReplicateIAMItem - PUT /minio/admin/v3/site-replication/iam-item
+func (a adminAPIHandlers) SRPeerReplicateIAMItem(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationOperationAction)
+ if objectAPI == nil {
+ return
+ }
+
+ var item madmin.SRIAMItem
+ if err := parseJSONBody(ctx, r.Body, &item, ""); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ var err error
+ switch item.Type {
+ default:
+ err = errSRInvalidRequest(errInvalidArgument)
+ case madmin.SRIAMItemPolicy:
+ if item.Policy == nil {
+ err = globalSiteReplicationSys.PeerAddPolicyHandler(ctx, item.Name, nil, item.UpdatedAt)
+ } else {
+ policy, perr := policy.ParseConfig(bytes.NewReader(item.Policy))
+ if perr != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, perr), r.URL)
+ return
+ }
+ if policy.IsEmpty() {
+ err = globalSiteReplicationSys.PeerAddPolicyHandler(ctx, item.Name, nil, item.UpdatedAt)
+ } else {
+ err = globalSiteReplicationSys.PeerAddPolicyHandler(ctx, item.Name, policy, item.UpdatedAt)
+ }
+ }
+ case madmin.SRIAMItemSvcAcc:
+ err = globalSiteReplicationSys.PeerSvcAccChangeHandler(ctx, item.SvcAccChange, item.UpdatedAt)
+ case madmin.SRIAMItemPolicyMapping:
+ err = globalSiteReplicationSys.PeerPolicyMappingHandler(ctx, item.PolicyMapping, item.UpdatedAt)
+ case madmin.SRIAMItemSTSAcc:
+ err = globalSiteReplicationSys.PeerSTSAccHandler(ctx, item.STSCredential, item.UpdatedAt)
+ case madmin.SRIAMItemIAMUser:
+ err = globalSiteReplicationSys.PeerIAMUserChangeHandler(ctx, item.IAMUser, item.UpdatedAt)
+ case madmin.SRIAMItemGroupInfo:
+ err = globalSiteReplicationSys.PeerGroupInfoChangeHandler(ctx, item.GroupInfo, item.UpdatedAt)
+ }
+ if err != nil {
+ adminLogIf(ctx, err)
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+}
+
+// SRPeerReplicateBucketItem - PUT /minio/admin/v3/site-replication/peer/bucket-meta
+func (a adminAPIHandlers) SRPeerReplicateBucketItem(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationOperationAction)
+ if objectAPI == nil {
+ return
+ }
+
+ var item madmin.SRBucketMeta
+ if err := parseJSONBody(ctx, r.Body, &item, ""); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ if item.Bucket == "" {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errSRInvalidRequest(errInvalidArgument)), r.URL)
+ return
+ }
+
+ var err error
+ switch item.Type {
+ default:
+ err = globalSiteReplicationSys.PeerBucketMetadataUpdateHandler(ctx, item)
+ case madmin.SRBucketMetaTypePolicy:
+ if item.Policy == nil {
+ err = globalSiteReplicationSys.PeerBucketPolicyHandler(ctx, item.Bucket, nil, item.UpdatedAt)
+ } else {
+ bktPolicy, berr := policy.ParseBucketPolicyConfig(bytes.NewReader(item.Policy), item.Bucket)
+ if berr != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, berr), r.URL)
+ return
+ }
+ if bktPolicy.IsEmpty() {
+ err = globalSiteReplicationSys.PeerBucketPolicyHandler(ctx, item.Bucket, nil, item.UpdatedAt)
+ } else {
+ err = globalSiteReplicationSys.PeerBucketPolicyHandler(ctx, item.Bucket, bktPolicy, item.UpdatedAt)
+ }
+ }
+ case madmin.SRBucketMetaTypeQuotaConfig:
+ if item.Quota == nil {
+ err = globalSiteReplicationSys.PeerBucketQuotaConfigHandler(ctx, item.Bucket, nil, item.UpdatedAt)
+ } else {
+ quotaConfig, err := parseBucketQuota(item.Bucket, item.Quota)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ if err = globalSiteReplicationSys.PeerBucketQuotaConfigHandler(ctx, item.Bucket, quotaConfig, item.UpdatedAt); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ }
+ case madmin.SRBucketMetaTypeVersionConfig:
+ err = globalSiteReplicationSys.PeerBucketVersioningHandler(ctx, item.Bucket, item.Versioning, item.UpdatedAt)
+ case madmin.SRBucketMetaTypeTags:
+ err = globalSiteReplicationSys.PeerBucketTaggingHandler(ctx, item.Bucket, item.Tags, item.UpdatedAt)
+ case madmin.SRBucketMetaTypeObjectLockConfig:
+ err = globalSiteReplicationSys.PeerBucketObjectLockConfigHandler(ctx, item.Bucket, item.ObjectLockConfig, item.UpdatedAt)
+ case madmin.SRBucketMetaTypeSSEConfig:
+ err = globalSiteReplicationSys.PeerBucketSSEConfigHandler(ctx, item.Bucket, item.SSEConfig, item.UpdatedAt)
+ case madmin.SRBucketMetaLCConfig:
+ err = globalSiteReplicationSys.PeerBucketLCConfigHandler(ctx, item.Bucket, item.ExpiryLCConfig, item.UpdatedAt)
+ }
+ if err != nil {
+ adminLogIf(ctx, err)
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+}
+
+// SiteReplicationInfo - GET /minio/admin/v3/site-replication/info
+func (a adminAPIHandlers) SiteReplicationInfo(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationInfoAction)
+ if objectAPI == nil {
+ return
+ }
+
+ info, err := globalSiteReplicationSys.GetClusterInfo(ctx)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ if err = json.NewEncoder(w).Encode(info); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+}
+
+func (a adminAPIHandlers) SRPeerGetIDPSettings(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationAddAction)
+ if objectAPI == nil {
+ return
+ }
+
+ idpSettings := globalSiteReplicationSys.GetIDPSettings(ctx)
+ if err := json.NewEncoder(w).Encode(idpSettings); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+}
+
+func parseJSONBody(ctx context.Context, body io.Reader, v any, encryptionKey string) error {
+ data, err := io.ReadAll(body)
+ if err != nil {
+ return SRError{
+ Cause: err,
+ Code: ErrSiteReplicationInvalidRequest,
+ }
+ }
+ if encryptionKey != "" {
+ data, err = madmin.DecryptData(encryptionKey, bytes.NewReader(data))
+ if err != nil {
+ return SRError{
+ Cause: err,
+ Code: ErrSiteReplicationInvalidRequest,
+ }
+ }
+ }
+ return json.Unmarshal(data, v)
+}
+
+// SiteReplicationStatus - GET /minio/admin/v3/site-replication/status
+func (a adminAPIHandlers) SiteReplicationStatus(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationInfoAction)
+ if objectAPI == nil {
+ return
+ }
+ opts := getSRStatusOptions(r)
+ // default options to all if status options are unset for backward compatibility
+ var dfltOpts madmin.SRStatusOptions
+ if opts == dfltOpts {
+ opts.Buckets = true
+ opts.Users = true
+ opts.Policies = true
+ opts.Groups = true
+ opts.ILMExpiryRules = true
+ }
+ info, err := globalSiteReplicationSys.SiteReplicationStatus(ctx, objectAPI, opts)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ // Report the ILMExpiryStats only if at least one site has replication of ILM expiry enabled
+ var replicateILMExpiry bool
+ for _, site := range info.Sites {
+ if site.ReplicateILMExpiry {
+ replicateILMExpiry = true
+ break
+ }
+ }
+ if !replicateILMExpiry {
+ // explicitly send nil for ILMExpiryStats
+ info.ILMExpiryStats = nil
+ }
+
+ if err = json.NewEncoder(w).Encode(info); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+}
+
+// SiteReplicationMetaInfo - GET /minio/admin/v3/site-replication/metainfo
+func (a adminAPIHandlers) SiteReplicationMetaInfo(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationInfoAction)
+ if objectAPI == nil {
+ return
+ }
+
+ opts := getSRStatusOptions(r)
+ info, err := globalSiteReplicationSys.SiteReplicationMetaInfo(ctx, objectAPI, opts)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ if err = json.NewEncoder(w).Encode(info); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+}
+
+// SiteReplicationEdit - PUT /minio/admin/v3/site-replication/edit
+func (a adminAPIHandlers) SiteReplicationEdit(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, cred := validateAdminReq(ctx, w, r, policy.SiteReplicationAddAction)
+ if objectAPI == nil {
+ return
+ }
+ var site madmin.PeerInfo
+ err := parseJSONBody(ctx, r.Body, &site, cred.SecretKey)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ opts := getSREditOptions(r)
+ status, err := globalSiteReplicationSys.EditPeerCluster(ctx, site, opts)
+ if err != nil {
+ adminLogIf(ctx, err)
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ body, err := json.Marshal(status)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, body)
+}
+
+func getSREditOptions(r *http.Request) (opts madmin.SREditOptions) {
+ opts.DisableILMExpiryReplication = r.Form.Get("disableILMExpiryReplication") == "true"
+ opts.EnableILMExpiryReplication = r.Form.Get("enableILMExpiryReplication") == "true"
+ return opts
+}
+
+// SRPeerEdit - PUT /minio/admin/v3/site-replication/peer/edit
+//
+// used internally to tell current cluster to update endpoint for peer
+func (a adminAPIHandlers) SRPeerEdit(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationAddAction)
+ if objectAPI == nil {
+ return
+ }
+
+ var pi madmin.PeerInfo
+ if err := parseJSONBody(ctx, r.Body, &pi, ""); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ if err := globalSiteReplicationSys.PeerEditReq(ctx, pi); err != nil {
+ adminLogIf(ctx, err)
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+}
+
+// SRStateEdit - PUT /minio/admin/v3/site-replication/state/edit
+//
+// used internally to tell current cluster to update site replication state
+func (a adminAPIHandlers) SRStateEdit(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationOperationAction)
+ if objectAPI == nil {
+ return
+ }
+
+ var state madmin.SRStateEditReq
+ if err := parseJSONBody(ctx, r.Body, &state, ""); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ if err := globalSiteReplicationSys.PeerStateEditReq(ctx, state); err != nil {
+ adminLogIf(ctx, err)
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+}
+
+func getSRStatusOptions(r *http.Request) (opts madmin.SRStatusOptions) {
+ q := r.Form
+ opts.Buckets = q.Get("buckets") == "true"
+ opts.Policies = q.Get("policies") == "true"
+ opts.Groups = q.Get("groups") == "true"
+ opts.Users = q.Get("users") == "true"
+ opts.ILMExpiryRules = q.Get("ilm-expiry-rules") == "true"
+ opts.PeerState = q.Get("peer-state") == "true"
+ opts.Entity = madmin.GetSREntityType(q.Get("entity"))
+ opts.EntityValue = q.Get("entityvalue")
+ opts.ShowDeleted = q.Get("showDeleted") == "true"
+ opts.Metrics = q.Get("metrics") == "true"
+ return opts
+}
+
+// SiteReplicationRemove - PUT /minio/admin/v3/site-replication/remove
+func (a adminAPIHandlers) SiteReplicationRemove(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationRemoveAction)
+ if objectAPI == nil {
+ return
+ }
+ var rreq madmin.SRRemoveReq
+ err := parseJSONBody(ctx, r.Body, &rreq, "")
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ status, err := globalSiteReplicationSys.RemovePeerCluster(ctx, objectAPI, rreq)
+ if err != nil {
+ adminLogIf(ctx, err)
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ body, err := json.Marshal(status)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ writeSuccessResponseJSON(w, body)
+}
+
+// SRPeerRemove - PUT /minio/admin/v3/site-replication/peer/remove
+//
+// used internally to tell current cluster to update endpoint for peer
+func (a adminAPIHandlers) SRPeerRemove(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationRemoveAction)
+ if objectAPI == nil {
+ return
+ }
+
+ var req madmin.SRRemoveReq
+ if err := parseJSONBody(ctx, r.Body, &req, ""); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ if err := globalSiteReplicationSys.InternalRemoveReq(ctx, objectAPI, req); err != nil {
+ adminLogIf(ctx, err)
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+}
+
+// SiteReplicationResyncOp - PUT /minio/admin/v3/site-replication/resync/op
+func (a adminAPIHandlers) SiteReplicationResyncOp(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationResyncAction)
+ if objectAPI == nil {
+ return
+ }
+
+ var peerSite madmin.PeerInfo
+ if err := parseJSONBody(ctx, r.Body, &peerSite, ""); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ vars := mux.Vars(r)
+ op := madmin.SiteResyncOp(vars["operation"])
+ var (
+ status madmin.SRResyncOpStatus
+ err error
+ )
+ switch op {
+ case madmin.SiteResyncStart:
+ status, err = globalSiteReplicationSys.startResync(ctx, objectAPI, peerSite)
+ case madmin.SiteResyncCancel:
+ status, err = globalSiteReplicationSys.cancelResync(ctx, objectAPI, peerSite)
+ default:
+ err = errSRInvalidRequest(errInvalidArgument)
+ }
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ body, err := json.Marshal(status)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ writeSuccessResponseJSON(w, body)
+}
+
+// SiteReplicationDevNull - everything goes to io.Discard
+// [POST] /minio/admin/v3/site-replication/devnull
+func (a adminAPIHandlers) SiteReplicationDevNull(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ globalSiteNetPerfRX.Connect()
+ defer globalSiteNetPerfRX.Disconnect()
+
+ connectTime := time.Now()
+ for {
+ n, err := io.CopyN(xioutil.Discard, r.Body, 128*humanize.KiByte)
+ atomic.AddUint64(&globalSiteNetPerfRX.RX, uint64(n))
+ if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
+ // If there is a disconnection before globalNetPerfMinDuration (we give a margin of error of 1 sec)
+ // would mean the network is not stable. Logging here will help in debugging network issues.
+ if time.Since(connectTime) < (globalNetPerfMinDuration - time.Second) {
+ adminLogIf(ctx, err)
+ }
+ }
+ if err != nil {
+ if errors.Is(err, io.EOF) {
+ w.WriteHeader(http.StatusNoContent)
+ } else {
+ w.WriteHeader(http.StatusBadRequest)
+ }
+ break
+ }
+ }
+}
+
+// SiteReplicationNetPerf - everything goes to io.Discard
+// [POST] /minio/admin/v3/site-replication/netperf
+func (a adminAPIHandlers) SiteReplicationNetPerf(w http.ResponseWriter, r *http.Request) {
+ durationStr := r.Form.Get(peerRESTDuration)
+ duration, _ := time.ParseDuration(durationStr)
+ if duration < globalNetPerfMinDuration {
+ duration = globalNetPerfMinDuration
+ }
+ result := siteNetperf(r.Context(), duration)
+ adminLogIf(r.Context(), gob.NewEncoder(w).Encode(result))
+}
diff --git a/cmd/admin-handlers-users-race_test.go b/cmd/admin-handlers-users-race_test.go
new file mode 100644
index 00000000..79474a01
--- /dev/null
+++ b/cmd/admin-handlers-users-race_test.go
@@ -0,0 +1,152 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+//go:build !race
+// +build !race
+
+// Tests in this file are not run under the `-race` flag as they are too slow
+// and cause context deadline errors.
+
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "runtime"
+ "testing"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ minio "github.com/minio/minio-go/v7"
+ "github.com/minio/pkg/v3/sync/errgroup"
+)
+
+func runAllIAMConcurrencyTests(suite *TestSuiteIAM, c *check) {
+ suite.SetUpSuite(c)
+ suite.TestDeleteUserRace(c)
+ suite.TearDownSuite(c)
+}
+
+func TestIAMInternalIDPConcurrencyServerSuite(t *testing.T) {
+ if runtime.GOOS == globalWindowsOSName {
+ t.Skip("windows is clunky")
+ }
+
+ baseTestCases := []TestSuiteCommon{
+ // Init and run test on ErasureSD backend with signature v4.
+ {serverType: "ErasureSD", signer: signerV4},
+ // Init and run test on ErasureSD backend, with tls enabled.
+ {serverType: "ErasureSD", signer: signerV4, secure: true},
+ // Init and run test on Erasure backend.
+ {serverType: "Erasure", signer: signerV4},
+ // Init and run test on ErasureSet backend.
+ {serverType: "ErasureSet", signer: signerV4},
+ }
+ testCases := []*TestSuiteIAM{}
+ for _, bt := range baseTestCases {
+ testCases = append(testCases,
+ newTestSuiteIAM(bt, false),
+ newTestSuiteIAM(bt, true),
+ )
+ }
+ for i, testCase := range testCases {
+ etcdStr := ""
+ if testCase.withEtcdBackend {
+ etcdStr = " (with etcd backend)"
+ }
+ t.Run(
+ fmt.Sprintf("Test: %d, ServerType: %s%s", i+1, testCase.serverType, etcdStr),
+ func(t *testing.T) {
+ runAllIAMConcurrencyTests(testCase, &check{t, testCase.serverType})
+ },
+ )
+ }
+}
+
+func (s *TestSuiteIAM) TestDeleteUserRace(c *check) {
+ ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
+ defer cancel()
+
+ bucket := getRandomBucketName()
+ err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{})
+ if err != nil {
+ c.Fatalf("bucket creat error: %v", err)
+ }
+
+ // Create a policy policy
+ policy := "mypolicy"
+ policyBytes := fmt.Appendf(nil, `{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:PutObject",
+ "s3:GetObject",
+ "s3:ListBucket"
+ ],
+ "Resource": [
+ "arn:aws:s3:::%s/*"
+ ]
+ }
+ ]
+}`, bucket)
+ err = s.adm.AddCannedPolicy(ctx, policy, policyBytes)
+ if err != nil {
+ c.Fatalf("policy add error: %v", err)
+ }
+
+ userCount := 50
+ accessKeys := make([]string, userCount)
+ secretKeys := make([]string, userCount)
+ for i := range userCount {
+ accessKey, secretKey := mustGenerateCredentials(c)
+ err = s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled)
+ if err != nil {
+ c.Fatalf("Unable to set user: %v", err)
+ }
+
+ userReq := madmin.PolicyAssociationReq{
+ Policies: []string{policy},
+ User: accessKey,
+ }
+ if _, err := s.adm.AttachPolicy(ctx, userReq); err != nil {
+ c.Fatalf("Unable to attach policy: %v", err)
+ }
+
+ accessKeys[i] = accessKey
+ secretKeys[i] = secretKey
+ }
+
+ g := errgroup.Group{}
+ for i := range userCount {
+ g.Go(func(i int) func() error {
+ return func() error {
+ uClient := s.getUserClient(c, accessKeys[i], secretKeys[i], "")
+ err := s.adm.RemoveUser(ctx, accessKeys[i])
+ if err != nil {
+ return err
+ }
+ c.mustNotListObjects(ctx, uClient, bucket)
+ return nil
+ }
+ }(i), i)
+ }
+ if errs := g.Wait(); len(errs) > 0 {
+ c.Fatalf("unable to remove users: %v", errs)
+ }
+}
diff --git a/cmd/admin-handlers-users.go b/cmd/admin-handlers-users.go
new file mode 100644
index 00000000..8530046b
--- /dev/null
+++ b/cmd/admin-handlers-users.go
@@ -0,0 +1,3000 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "maps"
+ "net/http"
+ "os"
+ "slices"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+ "unicode/utf8"
+
+ "github.com/klauspost/compress/zip"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/auth"
+ "github.com/minio/minio/internal/config/dns"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/mux"
+ xldap "github.com/minio/pkg/v3/ldap"
+ "github.com/minio/pkg/v3/policy"
+ "github.com/puzpuzpuz/xsync/v3"
+)
+
+// RemoveUser - DELETE /minio/admin/v3/remove-user?accessKey=
+func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, cred := validateAdminReq(ctx, w, r, policy.DeleteUserAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ vars := mux.Vars(r)
+ accessKey := vars["accessKey"]
+
+ ok, _, err := globalIAMSys.IsTempUser(accessKey)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ if ok {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
+ 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
+ // remove the root user. Also you cannot delete yourself.
+ if accessKey == globalActiveCred.AccessKey || accessKey == cred.AccessKey {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
+ return
+ }
+
+ if err := globalIAMSys.DeleteUser(ctx, accessKey, true); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
+ Type: madmin.SRIAMItemIAMUser,
+ IAMUser: &madmin.SRIAMUser{
+ AccessKey: accessKey,
+ IsDeleteReq: true,
+ },
+ UpdatedAt: UTCNow(),
+ }))
+}
+
+// ListBucketUsers - GET /minio/admin/v3/list-users?bucket={bucket}
+func (a adminAPIHandlers) ListBucketUsers(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, cred := validateAdminReq(ctx, w, r, policy.ListUsersAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ bucket := mux.Vars(r)["bucket"]
+
+ password := cred.SecretKey
+
+ allCredentials, err := globalIAMSys.ListBucketUsers(ctx, bucket)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ data, err := json.Marshal(allCredentials)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ econfigData, err := madmin.EncryptData(password, data)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, econfigData)
+}
+
+// ListUsers - GET /minio/admin/v3/list-users
+func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, cred := validateAdminReq(ctx, w, r, policy.ListUsersAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ password := cred.SecretKey
+
+ allCredentials, err := globalIAMSys.ListUsers(ctx)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Add ldap users which have mapped policies if in LDAP mode
+ // FIXME(vadmeste): move this to policy info in the future
+ ldapUsers, err := globalIAMSys.ListLDAPUsers(ctx)
+ if err != nil && err != errIAMActionNotAllowed {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ maps.Copy(allCredentials, ldapUsers)
+
+ // Marshal the response
+ data, err := json.Marshal(allCredentials)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ econfigData, err := madmin.EncryptData(password, data)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, econfigData)
+}
+
+// GetUserInfo - GET /minio/admin/v3/user-info
+func (a adminAPIHandlers) GetUserInfo(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ vars := mux.Vars(r)
+ name := vars["accessKey"]
+
+ // 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
+ }
+
+ checkDenyOnly := name == cred.AccessKey
+
+ if !globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.GetUserAdminAction,
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ Claims: cred.Claims,
+ DenyOnly: checkDenyOnly,
+ }) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+
+ userInfo, err := globalIAMSys.GetUserInfo(ctx, name)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ data, err := json.Marshal(userInfo)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, data)
+}
+
+// UpdateGroupMembers - PUT /minio/admin/v3/update-group-members
+func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.AddUserToGroupAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ data, err := io.ReadAll(r.Body)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
+ return
+ }
+
+ var updReq madmin.GroupAddRemove
+ err = json.Unmarshal(data, &updReq)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
+ return
+ }
+
+ // Reject if the group add and remove are temporary credentials, or root credential.
+ for _, member := range updReq.Members {
+ ok, _, err := globalIAMSys.IsTempUser(member)
+ if err != nil && err != errNoSuchUser {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ if ok {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
+ return
+ }
+ // When the user is root credential you are not allowed to
+ // add policies for root user.
+ if member == globalActiveCred.AccessKey {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
+ return
+ }
+ }
+
+ var updatedAt time.Time
+ if updReq.IsRemove {
+ updatedAt, err = globalIAMSys.RemoveUsersFromGroup(ctx, updReq.Group, updReq.Members)
+ } else {
+ // Check if group already exists
+ if _, gerr := globalIAMSys.GetGroupDescription(updReq.Group); gerr != nil {
+ // If group does not exist, then check if the group has beginning and end space characters
+ // we will reject such group names.
+ if errors.Is(gerr, errNoSuchGroup) && hasSpaceBE(updReq.Group) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
+ return
+ }
+ }
+
+ if globalIAMSys.LDAPConfig.Enabled() {
+ // We don't allow internal group manipulation in this API when LDAP
+ // is enabled for now.
+ err = errIAMActionNotAllowed
+ } else {
+ updatedAt, err = globalIAMSys.AddUsersToGroup(ctx, updReq.Group, updReq.Members)
+ }
+ }
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
+ Type: madmin.SRIAMItemGroupInfo,
+ GroupInfo: &madmin.SRGroupInfo{
+ UpdateReq: updReq,
+ },
+ UpdatedAt: updatedAt,
+ }))
+}
+
+// GetGroup - /minio/admin/v3/group?group=mygroup1
+func (a adminAPIHandlers) GetGroup(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.GetGroupAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ vars := mux.Vars(r)
+ group := vars["group"]
+
+ gdesc, err := globalIAMSys.GetGroupDescription(group)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ body, err := json.Marshal(gdesc)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, body)
+}
+
+// ListGroups - GET /minio/admin/v3/groups
+func (a adminAPIHandlers) ListGroups(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.ListGroupsAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ groups, err := globalIAMSys.ListGroups(ctx)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ body, err := json.Marshal(groups)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, body)
+}
+
+// SetGroupStatus - PUT /minio/admin/v3/set-group-status?group=mygroup1&status=enabled
+func (a adminAPIHandlers) SetGroupStatus(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.EnableGroupAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ vars := mux.Vars(r)
+ group := vars["group"]
+ status := vars["status"]
+
+ var (
+ err error
+ updatedAt time.Time
+ )
+ switch status {
+ case statusEnabled:
+ updatedAt, err = globalIAMSys.SetGroupStatus(ctx, group, true)
+ case statusDisabled:
+ updatedAt, err = globalIAMSys.SetGroupStatus(ctx, group, false)
+ default:
+ err = errInvalidArgument
+ }
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
+ Type: madmin.SRIAMItemGroupInfo,
+ GroupInfo: &madmin.SRGroupInfo{
+ UpdateReq: madmin.GroupAddRemove{
+ Group: group,
+ Status: madmin.GroupStatus(status),
+ IsRemove: false,
+ },
+ },
+ UpdatedAt: updatedAt,
+ }))
+}
+
+// SetUserStatus - PUT /minio/admin/v3/set-user-status?accessKey=&status=[enabled|disabled]
+func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, creds := validateAdminReq(ctx, w, r, policy.EnableUserAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ vars := mux.Vars(r)
+ accessKey := vars["accessKey"]
+ status := vars["status"]
+
+ // you cannot enable or disable yourself.
+ if accessKey == creds.AccessKey {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL)
+ return
+ }
+
+ updatedAt, err := globalIAMSys.SetUserStatus(ctx, accessKey, madmin.AccountStatus(status))
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
+ Type: madmin.SRIAMItemIAMUser,
+ IAMUser: &madmin.SRIAMUser{
+ AccessKey: accessKey,
+ IsDeleteReq: false,
+ UserReq: &madmin.AddOrUpdateUserReq{
+ Status: madmin.AccountStatus(status),
+ },
+ },
+ UpdatedAt: updatedAt,
+ }))
+}
+
+// AddUser - PUT /minio/admin/v3/add-user?accessKey=
+func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ vars := mux.Vars(r)
+ accessKey := vars["accessKey"]
+
+ // 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
+ }
+
+ // Not allowed to add a user with same access key as root credential
+ if accessKey == globalActiveCred.AccessKey {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL)
+ return
+ }
+
+ user, exists := globalIAMSys.GetUser(ctx, accessKey)
+ if exists && (user.Credentials.IsTemp() || user.Credentials.IsServiceAccount()) {
+ // Updating STS credential is not allowed, and this API does not
+ // support updating service accounts.
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL)
+ 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, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL)
+ return
+ }
+
+ // Check if accessKey has beginning and end space characters, this only applies to new users.
+ if !exists && hasSpaceBE(accessKey) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
+ return
+ }
+
+ if !utf8.ValidString(accessKey) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserValidUTF), r.URL)
+ return
+ }
+
+ checkDenyOnly := accessKey == cred.AccessKey
+
+ 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, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+
+ if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
+ // More than maxConfigSize bytes were available
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
+ return
+ }
+
+ password := cred.SecretKey
+ configBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
+ if err != nil {
+ adminLogIf(ctx, err)
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
+ return
+ }
+
+ var ureq madmin.AddOrUpdateUserReq
+ if err = json.Unmarshal(configBytes, &ureq); err != nil {
+ adminLogIf(ctx, err)
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
+ return
+ }
+
+ // We don't allow internal user creation with LDAP enabled for now.
+ if globalIAMSys.LDAPConfig.Enabled() {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
+ return
+ }
+
+ updatedAt, err := globalIAMSys.CreateUser(ctx, accessKey, ureq)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
+ Type: madmin.SRIAMItemIAMUser,
+ IAMUser: &madmin.SRIAMUser{
+ AccessKey: accessKey,
+ IsDeleteReq: false,
+ UserReq: &ureq,
+ },
+ UpdatedAt: updatedAt,
+ }))
+}
+
+// TemporaryAccountInfo - GET /minio/admin/v3/temporary-account-info
+func (a adminAPIHandlers) TemporaryAccountInfo(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
+ }
+
+ accessKey := mux.Vars(r)["accessKey"]
+ if accessKey == "" {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
+ return
+ }
+
+ args := policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.ListTemporaryAccountsAdminAction,
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ Claims: cred.Claims,
+ }
+
+ if !globalIAMSys.IsAllowed(args) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+
+ stsAccount, sessionPolicy, err := globalIAMSys.GetTemporaryAccount(ctx, accessKey)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ var stsAccountPolicy policy.Policy
+
+ if sessionPolicy != nil {
+ stsAccountPolicy = *sessionPolicy
+ } else {
+ policiesNames, err := globalIAMSys.PolicyDBGet(stsAccount.ParentUser, stsAccount.Groups...)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ if len(policiesNames) == 0 {
+ policySet, _ := args.GetPolicies(iamPolicyClaimNameOpenID())
+ policiesNames = policySet.ToSlice()
+ }
+
+ stsAccountPolicy = globalIAMSys.GetCombinedPolicy(policiesNames...)
+ }
+
+ policyJSON, err := json.MarshalIndent(stsAccountPolicy, "", " ")
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ infoResp := madmin.TemporaryAccountInfoResp{
+ ParentUser: stsAccount.ParentUser,
+ AccountStatus: stsAccount.Status,
+ ImpliedPolicy: sessionPolicy == nil,
+ Policy: string(policyJSON),
+ Expiration: &stsAccount.Expiration,
+ }
+
+ data, err := json.Marshal(infoResp)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, encryptedData)
+}
+
+// AddServiceAccount - PUT /minio/admin/v3/add-service-account
+func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Request) {
+ ctx, cred, opts, createReq, targetUser, APIError := commonAddServiceAccount(r, false)
+ if APIError.Code != "" {
+ writeErrorResponseJSON(ctx, w, APIError, r.URL)
+ return
+ }
+
+ if createReq.AccessKey == globalActiveCred.AccessKey {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL)
+ return
+ }
+
+ var (
+ targetGroups []string
+ err error
+ )
+
+ // Find the user for the request sender (as it may be sent via a service
+ // account or STS account):
+ requestorUser := cred.AccessKey
+ requestorParentUser := cred.AccessKey
+ requestorGroups := cred.Groups
+ requestorIsDerivedCredential := false
+ if cred.IsServiceAccount() || cred.IsTemp() {
+ requestorParentUser = cred.ParentUser
+ requestorIsDerivedCredential = true
+ }
+
+ if globalIAMSys.GetUsersSysType() == MinIOUsersSysType && targetUser != cred.AccessKey {
+ // For internal IDP, ensure that the targetUser's parent account exists.
+ // It could be a regular user account or the root account.
+ _, isRegularUser := globalIAMSys.GetUser(ctx, targetUser)
+ if !isRegularUser && targetUser != globalActiveCred.AccessKey {
+ apiErr := toAdminAPIErr(ctx, errNoSuchUser)
+ apiErr.Description = fmt.Sprintf("Specified target user %s does not exist", targetUser)
+ writeErrorResponseJSON(ctx, w, apiErr, r.URL)
+ return
+ }
+ }
+
+ // Check if we are creating svc account for request sender.
+ isSvcAccForRequestor := targetUser == requestorUser || targetUser == requestorParentUser
+
+ // If we are creating svc account for request sender, ensure
+ // that targetUser is a real user (i.e. not derived
+ // credentials).
+ if isSvcAccForRequestor {
+ if requestorIsDerivedCredential {
+ if requestorParentUser == "" {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx,
+ errors.New("service accounts cannot be generated for temporary credentials without parent")), r.URL)
+ return
+ }
+ targetUser = requestorParentUser
+ }
+ targetGroups = requestorGroups
+
+ // In case of LDAP/OIDC we need to set `opts.claims` to ensure
+ // it is associated with the LDAP/OIDC user properly.
+ for k, v := range cred.Claims {
+ if k == expClaim {
+ continue
+ }
+ opts.claims[k] = v
+ }
+ } else if globalIAMSys.LDAPConfig.Enabled() {
+ // In case of LDAP we need to resolve the targetUser to a DN and
+ // query their groups:
+ opts.claims[ldapUserN] = targetUser // simple username
+ var lookupResult *xldap.DNSearchResult
+ lookupResult, targetGroups, err = globalIAMSys.LDAPConfig.LookupUserDN(targetUser)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ targetUser = lookupResult.NormDN
+ opts.claims[ldapUser] = targetUser // username DN
+ opts.claims[ldapActualUser] = lookupResult.ActualDN
+
+ // Add LDAP attributes that were looked up into the claims.
+ for attribKey, attribValue := range lookupResult.Attributes {
+ opts.claims[ldapAttribPrefix+attribKey] = attribValue
+ }
+
+ // NOTE: if not using LDAP, then internal IDP or open ID is
+ // being used - in the former, group info is enforced when
+ // generated credentials are used to make requests, and in the
+ // latter, a group notion is not supported.
+ }
+
+ newCred, updatedAt, err := globalIAMSys.NewServiceAccount(ctx, targetUser, targetGroups, opts)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ createResp := madmin.AddServiceAccountResp{
+ Credentials: madmin.Credentials{
+ AccessKey: newCred.AccessKey,
+ SecretKey: newCred.SecretKey,
+ Expiration: newCred.Expiration,
+ },
+ }
+
+ data, err := json.Marshal(createResp)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, encryptedData)
+
+ // Call hook for cluster-replication if the service account is not for a
+ // root user.
+ if newCred.ParentUser != globalActiveCred.AccessKey {
+ replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
+ Type: madmin.SRIAMItemSvcAcc,
+ SvcAccChange: &madmin.SRSvcAccChange{
+ Create: &madmin.SRSvcAccCreate{
+ Parent: newCred.ParentUser,
+ AccessKey: newCred.AccessKey,
+ SecretKey: newCred.SecretKey,
+ Groups: newCred.Groups,
+ Name: newCred.Name,
+ Description: newCred.Description,
+ Claims: opts.claims,
+ SessionPolicy: madmin.SRSessionPolicy(createReq.Policy),
+ Status: auth.AccountOn,
+ Expiration: createReq.Expiration,
+ },
+ },
+ UpdatedAt: updatedAt,
+ }))
+ }
+}
+
+// UpdateServiceAccount - POST /minio/admin/v3/update-service-account
+func (a adminAPIHandlers) UpdateServiceAccount(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
+ }
+
+ accessKey := mux.Vars(r)["accessKey"]
+ if accessKey == "" {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
+ return
+ }
+
+ svcAccount, _, err := globalIAMSys.GetServiceAccount(ctx, accessKey)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ password := cred.SecretKey
+ reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
+ return
+ }
+
+ var updateReq madmin.UpdateServiceAccountReq
+ if err = json.Unmarshal(reqBytes, &updateReq); err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
+ return
+ }
+
+ if err := updateReq.Validate(); err != nil {
+ // Since this validation would happen client side as well, we only send
+ // a generic error message here.
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
+ return
+ }
+
+ condValues := getConditionValues(r, "", cred)
+ err = addExpirationToCondValues(updateReq.NewExpiration, condValues)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Permission checks:
+ //
+ // 1. Any type of account (i.e. access keys (previously/still called service
+ // accounts), STS accounts, internal IDP accounts, etc) with the
+ // policy.UpdateServiceAccountAdminAction permission can update any service
+ // account.
+ //
+ // 2. We would like to let a user update their own access keys, however it
+ // is currently blocked pending a re-design. Users are still able to delete
+ // and re-create them.
+ if !globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.UpdateServiceAccountAdminAction,
+ ConditionValues: condValues,
+ IsOwner: owner,
+ Claims: cred.Claims,
+ }) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+
+ var sp *policy.Policy
+ if len(updateReq.NewPolicy) > 0 {
+ sp, err = policy.ParseConfig(bytes.NewReader(updateReq.NewPolicy))
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ if sp.Version == "" && len(sp.Statements) == 0 {
+ sp = nil
+ }
+ }
+ opts := updateServiceAccountOpts{
+ secretKey: updateReq.NewSecretKey,
+ status: updateReq.NewStatus,
+ name: updateReq.NewName,
+ description: updateReq.NewDescription,
+ expiration: updateReq.NewExpiration,
+ sessionPolicy: sp,
+ }
+ updatedAt, err := globalIAMSys.UpdateServiceAccount(ctx, accessKey, opts)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Call site replication hook - non-root user accounts are replicated.
+ if svcAccount.ParentUser != globalActiveCred.AccessKey {
+ replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
+ Type: madmin.SRIAMItemSvcAcc,
+ SvcAccChange: &madmin.SRSvcAccChange{
+ Update: &madmin.SRSvcAccUpdate{
+ AccessKey: accessKey,
+ SecretKey: opts.secretKey,
+ Status: opts.status,
+ Name: opts.name,
+ Description: opts.description,
+ SessionPolicy: madmin.SRSessionPolicy(updateReq.NewPolicy),
+ Expiration: updateReq.NewExpiration,
+ },
+ },
+ UpdatedAt: updatedAt,
+ }))
+ }
+
+ writeSuccessNoContent(w)
+}
+
+// InfoServiceAccount - GET /minio/admin/v3/info-service-account
+func (a adminAPIHandlers) InfoServiceAccount(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
+ }
+
+ accessKey := mux.Vars(r)["accessKey"]
+ if accessKey == "" {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
+ return
+ }
+
+ svcAccount, sessionPolicy, err := globalIAMSys.GetServiceAccount(ctx, accessKey)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ if !globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.ListServiceAccountsAdminAction,
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ Claims: cred.Claims,
+ }) {
+ requestUser := cred.AccessKey
+ if cred.ParentUser != "" {
+ requestUser = cred.ParentUser
+ }
+
+ if requestUser != svcAccount.ParentUser {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+ }
+
+ // if session policy is nil or empty, then it is implied policy
+ impliedPolicy := sessionPolicy == nil || (sessionPolicy.Version == "" && len(sessionPolicy.Statements) == 0)
+
+ var svcAccountPolicy policy.Policy
+
+ if !impliedPolicy {
+ svcAccountPolicy = *sessionPolicy
+ } else {
+ policiesNames, err := globalIAMSys.PolicyDBGet(svcAccount.ParentUser, svcAccount.Groups...)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ svcAccountPolicy = globalIAMSys.GetCombinedPolicy(policiesNames...)
+ }
+
+ policyJSON, err := json.MarshalIndent(svcAccountPolicy, "", " ")
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ var expiration *time.Time
+ if !svcAccount.Expiration.IsZero() && !svcAccount.Expiration.Equal(timeSentinel) {
+ expiration = &svcAccount.Expiration
+ }
+
+ infoResp := madmin.InfoServiceAccountResp{
+ ParentUser: svcAccount.ParentUser,
+ Name: svcAccount.Name,
+ Description: svcAccount.Description,
+ AccountStatus: svcAccount.Status,
+ ImpliedPolicy: impliedPolicy,
+ Policy: string(policyJSON),
+ Expiration: expiration,
+ }
+
+ data, err := json.Marshal(infoResp)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, encryptedData)
+}
+
+// ListServiceAccounts - GET /minio/admin/v3/list-service-accounts
+func (a adminAPIHandlers) ListServiceAccounts(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
+ }
+
+ var targetAccount string
+
+ // If listing is requested for a specific user (who is not the request
+ // sender), check that the user has permissions.
+ user := r.Form.Get("user")
+ if user != "" && user != cred.AccessKey {
+ if !globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.ListServiceAccountsAdminAction,
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ Claims: cred.Claims,
+ }) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+ targetAccount = user
+ } else {
+ targetAccount = cred.AccessKey
+ if cred.ParentUser != "" {
+ targetAccount = cred.ParentUser
+ }
+ }
+
+ serviceAccounts, err := globalIAMSys.ListServiceAccounts(ctx, targetAccount)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ var serviceAccountList []madmin.ServiceAccountInfo
+
+ for _, svc := range serviceAccounts {
+ expiryTime := svc.Expiration
+ serviceAccountList = append(serviceAccountList, madmin.ServiceAccountInfo{
+ Description: svc.Description,
+ ParentUser: svc.ParentUser,
+ Name: svc.Name,
+ AccountStatus: svc.Status,
+ AccessKey: svc.AccessKey,
+ ImpliedPolicy: svc.IsImpliedPolicy(),
+ Expiration: &expiryTime,
+ })
+ }
+
+ listResp := madmin.ListServiceAccountsResp{
+ Accounts: serviceAccountList,
+ }
+
+ data, err := json.Marshal(listResp)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, encryptedData)
+}
+
+// DeleteServiceAccount - DELETE /minio/admin/v3/delete-service-account
+func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Request) {
+ ctx := 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
+ }
+
+ serviceAccount := mux.Vars(r)["accessKey"]
+ if serviceAccount == "" {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminInvalidArgument), r.URL)
+ return
+ }
+
+ if serviceAccount == siteReplicatorSvcAcc && globalSiteReplicationSys.isEnabled() {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidArgument), r.URL)
+ return
+ }
+ // We do not care if service account is readable or not at this point,
+ // since this is a delete call we shall allow it to be deleted if possible.
+ svcAccount, _, err := globalIAMSys.GetServiceAccount(ctx, serviceAccount)
+ if errors.Is(err, errNoSuchServiceAccount) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminServiceAccountNotFound), 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 {
+ parentUser := cred.AccessKey
+ if cred.ParentUser != "" {
+ parentUser = cred.ParentUser
+ }
+ if svcAccount.ParentUser != "" && parentUser != svcAccount.ParentUser {
+ // The service account belongs to another user but return not
+ // found error to mitigate brute force attacks. or the
+ // serviceAccount doesn't exist.
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminServiceAccountNotFound), r.URL)
+ return
+ }
+ }
+
+ if err := globalIAMSys.DeleteServiceAccount(ctx, serviceAccount, true); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Call site replication hook - non-root user accounts are replicated.
+ if svcAccount.ParentUser != "" && svcAccount.ParentUser != globalActiveCred.AccessKey {
+ replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
+ Type: madmin.SRIAMItemSvcAcc,
+ SvcAccChange: &madmin.SRSvcAccChange{
+ Delete: &madmin.SRSvcAccDelete{
+ AccessKey: serviceAccount,
+ },
+ },
+ UpdatedAt: UTCNow(),
+ }))
+ }
+
+ writeSuccessNoContent(w)
+}
+
+// ListAccessKeysBulk - GET /minio/admin/v3/list-access-keys-bulk
+func (a adminAPIHandlers) ListAccessKeysBulk(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
+ }
+
+ users := r.Form["users"]
+ isAll := r.Form.Get("all") == "true"
+ selfOnly := !isAll && len(users) == 0
+
+ if isAll && len(users) > 0 {
+ // This should be checked on client side, so return generic error
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
+ return
+ }
+
+ // Empty user list and not self, list access keys for all users
+ if isAll {
+ if !globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.ListUsersAdminAction,
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ Claims: cred.Claims,
+ }) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+ } else if len(users) == 1 {
+ if users[0] == cred.AccessKey || users[0] == cred.ParentUser {
+ selfOnly = true
+ }
+ }
+
+ if !globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.ListServiceAccountsAdminAction,
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ Claims: cred.Claims,
+ DenyOnly: selfOnly,
+ }) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+
+ if selfOnly && len(users) == 0 {
+ selfUser := cred.AccessKey
+ if cred.ParentUser != "" {
+ selfUser = cred.ParentUser
+ }
+ users = append(users, selfUser)
+ }
+
+ var checkedUserList []string
+ if isAll {
+ users, err := globalIAMSys.ListUsers(ctx)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ for user := range users {
+ checkedUserList = append(checkedUserList, user)
+ }
+ checkedUserList = append(checkedUserList, globalActiveCred.AccessKey)
+ } else {
+ for _, user := range users {
+ // Validate the user
+ _, ok := globalIAMSys.GetUser(ctx, user)
+ if !ok {
+ continue
+ }
+ checkedUserList = append(checkedUserList, user)
+ }
+ }
+
+ listType := r.Form.Get("listType")
+ var listSTSKeys, listServiceAccounts bool
+ switch listType {
+ case madmin.AccessKeyListUsersOnly:
+ listSTSKeys = false
+ listServiceAccounts = false
+ case madmin.AccessKeyListSTSOnly:
+ listSTSKeys = true
+ listServiceAccounts = false
+ case madmin.AccessKeyListSvcaccOnly:
+ listSTSKeys = false
+ listServiceAccounts = true
+ case madmin.AccessKeyListAll:
+ listSTSKeys = true
+ listServiceAccounts = true
+ default:
+ err := errors.New("invalid list type")
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrInvalidRequest, err), r.URL)
+ return
+ }
+
+ accessKeyMap := make(map[string]madmin.ListAccessKeysResp)
+ for _, user := range checkedUserList {
+ accessKeys := madmin.ListAccessKeysResp{}
+ if listSTSKeys {
+ stsKeys, err := globalIAMSys.ListSTSAccounts(ctx, user)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ for _, sts := range stsKeys {
+ accessKeys.STSKeys = append(accessKeys.STSKeys, madmin.ServiceAccountInfo{
+ AccessKey: sts.AccessKey,
+ Expiration: &sts.Expiration,
+ })
+ }
+ // if only STS keys, skip if user has no STS keys
+ if !listServiceAccounts && len(stsKeys) == 0 {
+ continue
+ }
+ }
+
+ if listServiceAccounts {
+ serviceAccounts, err := globalIAMSys.ListServiceAccounts(ctx, user)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ for _, svc := range serviceAccounts {
+ accessKeys.ServiceAccounts = append(accessKeys.ServiceAccounts, madmin.ServiceAccountInfo{
+ AccessKey: svc.AccessKey,
+ Expiration: &svc.Expiration,
+ })
+ }
+ // if only service accounts, skip if user has no service accounts
+ if !listSTSKeys && len(serviceAccounts) == 0 {
+ continue
+ }
+ }
+ accessKeyMap[user] = accessKeys
+ }
+
+ data, err := json.Marshal(accessKeyMap)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, encryptedData)
+}
+
+// AccountInfoHandler returns usage, permissions and other bucket metadata for incoming us
+func (a adminAPIHandlers) AccountInfoHandler(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
+ }
+
+ // Set prefix value for "s3:prefix" policy conditionals.
+ r.Header.Set("prefix", "")
+
+ // Set delimiter value for "s3:delimiter" policy conditionals.
+ r.Header.Set("delimiter", SlashSeparator)
+
+ // Check if we are asked to return prefix usage
+ enablePrefixUsage := r.Form.Get("prefix-usage") == "true"
+
+ isAllowedAccess := func(bucketName string) (rd, wr bool) {
+ if globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.ListBucketAction,
+ BucketName: bucketName,
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ ObjectName: "",
+ Claims: cred.Claims,
+ }) {
+ rd = true
+ }
+
+ if globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.GetBucketLocationAction,
+ BucketName: bucketName,
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ ObjectName: "",
+ Claims: cred.Claims,
+ }) {
+ rd = true
+ }
+
+ if globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.PutObjectAction,
+ BucketName: bucketName,
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ ObjectName: "",
+ Claims: cred.Claims,
+ }) {
+ wr = true
+ }
+
+ return rd, wr
+ }
+
+ // If etcd, dns federation configured list buckets from etcd.
+ var err error
+ var buckets []BucketInfo
+ if globalDNSConfig != nil && globalBucketFederation {
+ dnsBuckets, err := globalDNSConfig.List()
+ if err != nil && !IsErrIgnored(err,
+ dns.ErrNoEntriesFound,
+ dns.ErrDomainMissing) {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ for _, dnsRecords := range dnsBuckets {
+ buckets = append(buckets, BucketInfo{
+ Name: dnsRecords[0].Key,
+ Created: dnsRecords[0].CreationDate,
+ })
+ }
+ sort.Slice(buckets, func(i, j int) bool {
+ return buckets[i].Name < buckets[j].Name
+ })
+ } else {
+ buckets, err = objectAPI.ListBuckets(ctx, BucketOptions{Cached: true})
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ }
+
+ accountName := cred.AccessKey
+ if cred.IsTemp() || cred.IsServiceAccount() {
+ // For derived credentials, check the parent user's permissions.
+ accountName = cred.ParentUser
+ }
+
+ roleArn := policy.Args{Claims: cred.Claims}.GetRoleArn()
+ policySetFromClaims, hasPolicyClaim := policy.GetPoliciesFromClaims(cred.Claims, iamPolicyClaimNameOpenID())
+ var effectivePolicy policy.Policy
+
+ var buf []byte
+ switch {
+ 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 {
+ if policy.Name == "consoleAdmin" {
+ effectivePolicy = policy.Definition
+ break
+ }
+ }
+
+ case roleArn != "":
+ _, policy, err := globalIAMSys.GetRolePolicy(roleArn)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ policySlice := newMappedPolicy(policy).toSlice()
+ effectivePolicy = globalIAMSys.GetCombinedPolicy(policySlice...)
+
+ case hasPolicyClaim:
+ effectivePolicy = globalIAMSys.GetCombinedPolicy(policySetFromClaims.ToSlice()...)
+
+ default:
+ policies, err := globalIAMSys.PolicyDBGet(accountName, cred.Groups...)
+ if err != nil {
+ adminLogIf(ctx, err)
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ effectivePolicy = globalIAMSys.GetCombinedPolicy(policies...)
+ }
+
+ buf, err = json.MarshalIndent(effectivePolicy, "", " ")
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ acctInfo := madmin.AccountInfo{
+ AccountName: accountName,
+ Server: objectAPI.BackendInfo(),
+ Policy: buf,
+ }
+
+ for _, bucket := range buckets {
+ rd, wr := isAllowedAccess(bucket.Name)
+ if rd || wr {
+ // Fetch the data usage of the current bucket
+ bui := globalBucketQuotaSys.GetBucketUsageInfo(ctx, bucket.Name)
+ size := bui.Size
+ objectsCount := bui.ObjectsCount
+ objectsHist := bui.ObjectSizesHistogram
+ versionsHist := bui.ObjectVersionsHistogram
+
+ // Fetch the prefix usage of the current bucket
+ var prefixUsage map[string]uint64
+ if enablePrefixUsage {
+ prefixUsage, _ = loadPrefixUsageFromBackend(ctx, objectAPI, bucket.Name)
+ }
+
+ lcfg, _ := globalBucketObjectLockSys.Get(bucket.Name)
+ quota, _ := globalBucketQuotaSys.Get(ctx, bucket.Name)
+ rcfg, _, _ := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket.Name)
+ tcfg, _, _ := globalBucketMetadataSys.GetTaggingConfig(bucket.Name)
+
+ acctInfo.Buckets = append(acctInfo.Buckets, madmin.BucketAccessInfo{
+ Name: bucket.Name,
+ Created: bucket.Created,
+ Size: size,
+ Objects: objectsCount,
+ ObjectSizesHistogram: objectsHist,
+ ObjectVersionsHistogram: versionsHist,
+ PrefixUsage: prefixUsage,
+ Details: &madmin.BucketDetails{
+ Versioning: globalBucketVersioningSys.Enabled(bucket.Name),
+ VersioningSuspended: globalBucketVersioningSys.Suspended(bucket.Name),
+ Replication: rcfg != nil,
+ Locking: lcfg.LockEnabled,
+ Quota: quota,
+ Tagging: tcfg,
+ },
+ Access: madmin.AccountAccess{
+ Read: rd,
+ Write: wr,
+ },
+ })
+ }
+ }
+
+ usageInfoJSON, err := json.Marshal(acctInfo)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, usageInfoJSON)
+}
+
+// InfoCannedPolicy - GET /minio/admin/v3/info-canned-policy?name={policyName}
+//
+// Newer API response with policy timestamps is returned with query parameter
+// `v=2` like:
+//
+// GET /minio/admin/v3/info-canned-policy?name={policyName}&v=2
+//
+// The newer API will eventually become the default (and only) one. The older
+// response is to return only the policy JSON. The newer response returns
+// timestamps along with the policy JSON. Both versions are supported for now,
+// for smooth transition to new API.
+func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.GetPolicyAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ name := mux.Vars(r)["name"]
+ policies := newMappedPolicy(name).toSlice()
+ if len(policies) != 1 {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errTooManyPolicies), r.URL)
+ return
+ }
+ setReqInfoPolicyName(ctx, name)
+
+ policyDoc, err := globalIAMSys.InfoPolicy(name)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Is the new API version being requested?
+ infoPolicyAPIVersion := r.Form.Get("v")
+ if infoPolicyAPIVersion == "2" {
+ buf, err := json.MarshalIndent(policyDoc, "", " ")
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ w.Write(buf)
+ return
+ } else if infoPolicyAPIVersion != "" {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errors.New("invalid version parameter 'v' supplied")), r.URL)
+ return
+ }
+
+ // Return the older API response value of just the policy json.
+ buf, err := json.MarshalIndent(policyDoc.Policy, "", " ")
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ w.Write(buf)
+}
+
+// ListBucketPolicies - GET /minio/admin/v3/list-canned-policies?bucket={bucket}
+func (a adminAPIHandlers) ListBucketPolicies(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.ListUserPoliciesAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ bucket := mux.Vars(r)["bucket"]
+ policies, err := globalIAMSys.ListPolicies(ctx, bucket)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ newPolicies := make(map[string]policy.Policy)
+ for name, p := range policies {
+ _, err = json.Marshal(p)
+ if err != nil {
+ adminLogIf(ctx, err)
+ continue
+ }
+ newPolicies[name] = p
+ }
+ if err = json.NewEncoder(w).Encode(newPolicies); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+}
+
+// ListCannedPolicies - GET /minio/admin/v3/list-canned-policies
+func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.ListUserPoliciesAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ policies, err := globalIAMSys.ListPolicies(ctx, "")
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ newPolicies := make(map[string]policy.Policy)
+ for name, p := range policies {
+ _, err = json.Marshal(p)
+ if err != nil {
+ adminLogIf(ctx, err)
+ continue
+ }
+ newPolicies[name] = p
+ }
+ if err = json.NewEncoder(w).Encode(newPolicies); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+}
+
+// RemoveCannedPolicy - DELETE /minio/admin/v3/remove-canned-policy?name=
+func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.DeletePolicyAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ vars := mux.Vars(r)
+ policyName := vars["name"]
+ setReqInfoPolicyName(ctx, policyName)
+
+ if err := globalIAMSys.DeletePolicy(ctx, policyName, true); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Call cluster-replication policy creation hook to replicate policy deletion to
+ // other minio clusters.
+ replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
+ Type: madmin.SRIAMItemPolicy,
+ Name: policyName,
+ UpdatedAt: UTCNow(),
+ }))
+}
+
+// AddCannedPolicy - PUT /minio/admin/v3/add-canned-policy?name=
+func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.CreatePolicyAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ vars := mux.Vars(r)
+ policyName := vars["name"]
+
+ // Policy has space characters in begin and end reject such inputs.
+ if hasSpaceBE(policyName) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
+ return
+ }
+ 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.
+ if r.ContentLength <= 0 {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL)
+ return
+ }
+
+ // Error out if Content-Length is beyond allowed size.
+ if r.ContentLength > maxBucketPolicySize {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrEntityTooLarge), r.URL)
+ return
+ }
+
+ iamPolicyBytes, err := io.ReadAll(io.LimitReader(r.Body, r.ContentLength))
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ iamPolicy, err := policy.ParseConfig(bytes.NewReader(iamPolicyBytes))
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Version in policy must not be empty
+ if iamPolicy.Version == "" {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrPolicyInvalidVersion), r.URL)
+ return
+ }
+
+ updatedAt, err := globalIAMSys.SetPolicy(ctx, policyName, *iamPolicy)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Call cluster-replication policy creation hook to replicate policy to
+ // other minio clusters.
+ replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
+ Type: madmin.SRIAMItemPolicy,
+ Name: policyName,
+ Policy: iamPolicyBytes,
+ UpdatedAt: updatedAt,
+ }))
+}
+
+// SetPolicyForUserOrGroup - sets a policy on a user or a group.
+//
+// PUT /minio/admin/v3/set-policy?policy=xxx&user-or-group=?[&is-group]
+//
+// Deprecated: This API is replaced by attach/detach policy APIs for specific
+// type of users (builtin or LDAP).
+func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.AttachPolicyAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ vars := mux.Vars(r)
+ policyName := vars["policyName"]
+ entityName := vars["userOrGroup"]
+ isGroup := vars["isGroup"] == "true"
+ setReqInfoPolicyName(ctx, policyName)
+
+ if !isGroup {
+ ok, _, err := globalIAMSys.IsTempUser(entityName)
+ if err != nil && err != errNoSuchUser {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ if ok {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
+ return
+ }
+ // When the user is root credential you are not allowed to
+ // add policies for root user.
+ if entityName == globalActiveCred.AccessKey {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
+ return
+ }
+ }
+
+ // Validate that user or group exists.
+ if !isGroup {
+ if globalIAMSys.GetUsersSysType() == MinIOUsersSysType {
+ _, ok := globalIAMSys.GetUser(ctx, entityName)
+ if !ok {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errNoSuchUser), r.URL)
+ return
+ }
+ }
+ } else {
+ _, err := globalIAMSys.GetGroupDescription(entityName)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ }
+
+ userType := regUser
+ if globalIAMSys.GetUsersSysType() == LDAPUsersSysType {
+ userType = stsUser
+
+ // Validate that the user or group exists in LDAP and use the normalized
+ // form of the entityName (which will be an LDAP DN).
+ var err error
+ if isGroup {
+ var foundGroupDN *xldap.DNSearchResult
+ var underBaseDN bool
+ if foundGroupDN, underBaseDN, err = globalIAMSys.LDAPConfig.GetValidatedGroupDN(nil, entityName); err != nil {
+ iamLogIf(ctx, err)
+ } else if foundGroupDN == nil || !underBaseDN {
+ err = errNoSuchGroup
+ } else {
+ entityName = foundGroupDN.NormDN
+ }
+ } else {
+ var foundUserDN *xldap.DNSearchResult
+ if foundUserDN, err = globalIAMSys.LDAPConfig.GetValidatedDNForUsername(entityName); err != nil {
+ iamLogIf(ctx, err)
+ } else if foundUserDN == nil {
+ err = errNoSuchUser
+ } else {
+ entityName = foundUserDN.NormDN
+ }
+ }
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ }
+
+ updatedAt, err := globalIAMSys.PolicyDBSet(ctx, entityName, policyName, userType, isGroup)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
+ Type: madmin.SRIAMItemPolicyMapping,
+ PolicyMapping: &madmin.SRPolicyMapping{
+ UserOrGroup: entityName,
+ UserType: int(userType),
+ IsGroup: isGroup,
+ Policy: policyName,
+ },
+ UpdatedAt: updatedAt,
+ }))
+}
+
+// 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) {
+ ctx := r.Context()
+
+ // Check authorization.
+ objectAPI, cred := validateAdminReq(ctx, w, r,
+ policy.ListGroupsAdminAction, policy.ListUsersAdminAction, policy.ListUserPoliciesAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ // Validate API arguments.
+ q := madmin.PolicyEntitiesQuery{
+ Users: r.Form["user"],
+ Groups: r.Form["group"],
+ Policy: r.Form["policy"],
+ }
+
+ // Query IAM
+ res, err := globalIAMSys.QueryPolicyEntities(r.Context(), q)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Encode result and send response.
+ data, err := json.Marshal(res)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ password := cred.SecretKey
+ econfigData, err := madmin.EncryptData(password, data)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ writeSuccessResponseJSON(w, econfigData)
+}
+
+// AttachDetachPolicyBuiltin - POST /minio/admin/v3/idp/builtin/policy/{operation}
+func (a adminAPIHandlers) AttachDetachPolicyBuiltin(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, cred := validateAdminReq(ctx, w, r, policy.UpdatePolicyAssociationAction,
+ policy.AttachPolicyAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
+ // More than maxConfigSize bytes were available
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
+ return
+ }
+
+ // Ensure body content type is opaque to ensure that request body has not
+ // been interpreted as form data.
+ contentType := r.Header.Get("Content-Type")
+ if contentType != "application/octet-stream" {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
+ return
+ }
+
+ operation := mux.Vars(r)["operation"]
+ if operation != "attach" && operation != "detach" {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminInvalidArgument), r.URL)
+ return
+ }
+ isAttach := operation == "attach"
+
+ password := cred.SecretKey
+ reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ var par madmin.PolicyAssociationReq
+ if err = json.Unmarshal(reqBytes, &par); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ if err = par.IsValid(); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ updatedAt, addedOrRemoved, _, err := globalIAMSys.PolicyDBUpdateBuiltin(ctx, isAttach, par)
+ if err != nil {
+ if err == errNoSuchUser || err == errNoSuchGroup {
+ if globalIAMSys.LDAPConfig.Enabled() {
+ // When LDAP is enabled, warn user that they are using the wrong
+ // API. FIXME: error can be no such group as well - fix errNoSuchUserLDAPWarn
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errNoSuchUserLDAPWarn), r.URL)
+ return
+ }
+ }
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ setReqInfoPolicyName(ctx, strings.Join(addedOrRemoved, ","))
+
+ respBody := madmin.PolicyAssociationResp{
+ UpdatedAt: updatedAt,
+ }
+ if isAttach {
+ respBody.PoliciesAttached = addedOrRemoved
+ } else {
+ respBody.PoliciesDetached = addedOrRemoved
+ }
+
+ data, err := json.Marshal(respBody)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ encryptedData, err := madmin.EncryptData(password, data)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ 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)
+}
+
+// InfoAccessKey - GET /minio/admin/v3/info-access-key?access-key=
+func (a adminAPIHandlers) InfoAccessKey(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
+ }
+
+ accessKey := mux.Vars(r)["accessKey"]
+ if accessKey == "" {
+ accessKey = cred.AccessKey
+ }
+
+ u, ok := globalIAMSys.GetUser(ctx, accessKey)
+ targetCred := u.Credentials
+
+ if !globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.ListServiceAccountsAdminAction,
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ Claims: cred.Claims,
+ }) {
+ // If requested user does not exist and requestor is not allowed to list service accounts, return access denied.
+ if !ok {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+
+ requestUser := cred.AccessKey
+ if cred.ParentUser != "" {
+ requestUser = cred.ParentUser
+ }
+
+ if requestUser != targetCred.ParentUser {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+ }
+
+ if !ok {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchAccessKey), r.URL)
+ return
+ }
+
+ var (
+ sessionPolicy *policy.Policy
+ err error
+ userType string
+ )
+ switch {
+ case targetCred.IsTemp():
+ userType = "STS"
+ _, sessionPolicy, err = globalIAMSys.GetTemporaryAccount(ctx, accessKey)
+ if err == errNoSuchTempAccount {
+ err = errNoSuchAccessKey
+ }
+ case targetCred.IsServiceAccount():
+ userType = "Service Account"
+ _, sessionPolicy, err = globalIAMSys.GetServiceAccount(ctx, accessKey)
+ if err == errNoSuchServiceAccount {
+ err = errNoSuchAccessKey
+ }
+ default:
+ err = errNoSuchAccessKey
+ }
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // if session policy is nil or empty, then it is implied policy
+ impliedPolicy := sessionPolicy == nil || (sessionPolicy.Version == "" && len(sessionPolicy.Statements) == 0)
+
+ var svcAccountPolicy policy.Policy
+
+ if !impliedPolicy {
+ svcAccountPolicy = *sessionPolicy
+ } else {
+ policiesNames, err := globalIAMSys.PolicyDBGet(targetCred.ParentUser, targetCred.Groups...)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ svcAccountPolicy = globalIAMSys.GetCombinedPolicy(policiesNames...)
+ }
+
+ policyJSON, err := json.MarshalIndent(svcAccountPolicy, "", " ")
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ var expiration *time.Time
+ if !targetCred.Expiration.IsZero() && !targetCred.Expiration.Equal(timeSentinel) {
+ expiration = &targetCred.Expiration
+ }
+
+ userProvider := guessUserProvider(targetCred)
+
+ infoResp := madmin.InfoAccessKeyResp{
+ AccessKey: accessKey,
+ InfoServiceAccountResp: madmin.InfoServiceAccountResp{
+ ParentUser: targetCred.ParentUser,
+ Name: targetCred.Name,
+ Description: targetCred.Description,
+ AccountStatus: targetCred.Status,
+ ImpliedPolicy: impliedPolicy,
+ Policy: string(policyJSON),
+ Expiration: expiration,
+ },
+
+ UserType: userType,
+ UserProvider: userProvider,
+ }
+
+ populateProviderInfoFromClaims(targetCred.Claims, userProvider, &infoResp)
+
+ data, err := json.Marshal(infoResp)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, encryptedData)
+}
+
+const (
+ allPoliciesFile = "policies.json"
+ allUsersFile = "users.json"
+ allGroupsFile = "groups.json"
+ allSvcAcctsFile = "svcaccts.json"
+ userPolicyMappingsFile = "user_mappings.json"
+ groupPolicyMappingsFile = "group_mappings.json"
+ stsUserPolicyMappingsFile = "stsuser_mappings.json"
+
+ iamAssetsDir = "iam-assets"
+)
+
+var iamExportFiles = []string{
+ allPoliciesFile,
+ allUsersFile,
+ allGroupsFile,
+ allSvcAcctsFile,
+ userPolicyMappingsFile,
+ groupPolicyMappingsFile,
+ stsUserPolicyMappingsFile,
+}
+
+// ExportIAMHandler - exports all iam info as a zipped file
+func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ // Get current object layer instance.
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.ExportIAMAction)
+ if objectAPI == nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+ // Initialize a zip writer which will provide a zipped content
+ // of bucket metadata
+ zipWriter := zip.NewWriter(w)
+ defer zipWriter.Close()
+ rawDataFn := func(r io.Reader, filename string, sz int) error {
+ header, zerr := zip.FileInfoHeader(dummyFileInfo{
+ name: filename,
+ size: int64(sz),
+ mode: 0o600,
+ modTime: time.Now(),
+ isDir: false,
+ sys: nil,
+ })
+ if zerr != nil {
+ adminLogIf(ctx, zerr)
+ return nil
+ }
+ header.Method = zip.Deflate
+ zwriter, zerr := zipWriter.CreateHeader(header)
+ if zerr != nil {
+ adminLogIf(ctx, zerr)
+ return nil
+ }
+ if _, err := io.Copy(zwriter, r); err != nil {
+ adminLogIf(ctx, err)
+ }
+ return nil
+ }
+
+ for _, f := range iamExportFiles {
+ iamFile := pathJoin(iamAssetsDir, f)
+ switch f {
+ case allPoliciesFile:
+ allPolicies, err := globalIAMSys.ListPolicies(ctx, "")
+ if err != nil {
+ adminLogIf(ctx, err)
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+
+ policiesData, err := json.Marshal(allPolicies)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+ if err = rawDataFn(bytes.NewReader(policiesData), iamFile, len(policiesData)); err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+ case allUsersFile:
+ userIdentities := make(map[string]UserIdentity)
+ err := globalIAMSys.store.loadUsers(ctx, regUser, userIdentities)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+ userAccounts := make(map[string]madmin.AddOrUpdateUserReq)
+ for u, uid := range userIdentities {
+ userAccounts[u] = madmin.AddOrUpdateUserReq{
+ SecretKey: uid.Credentials.SecretKey,
+ Status: func() madmin.AccountStatus {
+ // Export current credential status
+ if uid.Credentials.Status == auth.AccountOff {
+ return madmin.AccountDisabled
+ }
+ return madmin.AccountEnabled
+ }(),
+ }
+ }
+ userData, err := json.Marshal(userAccounts)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+
+ if err = rawDataFn(bytes.NewReader(userData), iamFile, len(userData)); err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+ case allGroupsFile:
+ groups := make(map[string]GroupInfo)
+ err := globalIAMSys.store.loadGroups(ctx, groups)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+ groupData, err := json.Marshal(groups)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+
+ if err = rawDataFn(bytes.NewReader(groupData), iamFile, len(groupData)); err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+ case allSvcAcctsFile:
+ serviceAccounts := make(map[string]UserIdentity)
+ err := globalIAMSys.store.loadUsers(ctx, svcUser, serviceAccounts)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+ svcAccts := make(map[string]madmin.SRSvcAccCreate)
+ for user, acc := range serviceAccounts {
+ if user == siteReplicatorSvcAcc {
+ // skip site-replication service account.
+ continue
+ }
+ claims, err := globalIAMSys.GetClaimsForSvcAcc(ctx, acc.Credentials.AccessKey)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+ sa, policy, err := globalIAMSys.GetServiceAccount(ctx, acc.Credentials.AccessKey)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+
+ var policyJSON []byte
+ if policy != nil {
+ policyJSON, err = json.Marshal(policy)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+ }
+ svcAccts[user] = madmin.SRSvcAccCreate{
+ Parent: acc.Credentials.ParentUser,
+ AccessKey: user,
+ SecretKey: acc.Credentials.SecretKey,
+ Groups: acc.Credentials.Groups,
+ Claims: claims,
+ SessionPolicy: policyJSON,
+ Status: acc.Credentials.Status,
+ Name: sa.Name,
+ Description: sa.Description,
+ Expiration: &sa.Expiration,
+ }
+ }
+
+ svcAccData, err := json.Marshal(svcAccts)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+
+ if err = rawDataFn(bytes.NewReader(svcAccData), iamFile, len(svcAccData)); err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+ case userPolicyMappingsFile:
+ userPolicyMap := xsync.NewMapOf[string, MappedPolicy]()
+ err := globalIAMSys.store.loadMappedPolicies(ctx, regUser, false, userPolicyMap)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+ userPolData, err := json.Marshal(mappedPoliciesToMap(userPolicyMap))
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+
+ if err = rawDataFn(bytes.NewReader(userPolData), iamFile, len(userPolData)); err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+ case groupPolicyMappingsFile:
+ groupPolicyMap := xsync.NewMapOf[string, MappedPolicy]()
+ err := globalIAMSys.store.loadMappedPolicies(ctx, regUser, true, groupPolicyMap)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+ grpPolData, err := json.Marshal(mappedPoliciesToMap(groupPolicyMap))
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+
+ if err = rawDataFn(bytes.NewReader(grpPolData), iamFile, len(grpPolData)); err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+ case stsUserPolicyMappingsFile:
+ userPolicyMap := xsync.NewMapOf[string, MappedPolicy]()
+ err := globalIAMSys.store.loadMappedPolicies(ctx, stsUser, false, userPolicyMap)
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+ userPolData, err := json.Marshal(mappedPoliciesToMap(userPolicyMap))
+ if err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+ if err = rawDataFn(bytes.NewReader(userPolData), iamFile, len(userPolData)); err != nil {
+ writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
+ return
+ }
+ }
+ }
+}
+
+// ImportIAM - imports all IAM info into MinIO
+func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
+ a.importIAM(w, r, "")
+}
+
+// ImportIAMV2 - imports all IAM info into MinIO
+func (a adminAPIHandlers) ImportIAMV2(w http.ResponseWriter, r *http.Request) {
+ a.importIAM(w, r, "v2")
+}
+
+// ImportIAM - imports all IAM info into MinIO
+func (a adminAPIHandlers) importIAM(w http.ResponseWriter, r *http.Request, apiVer string) {
+ ctx := r.Context()
+
+ // Validate signature, permissions and get current object layer instance.
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.ImportIAMAction)
+ if objectAPI == nil || globalNotificationSys == nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ data, err := io.ReadAll(r.Body)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
+ return
+ }
+ reader := bytes.NewReader(data)
+ zr, err := zip.NewReader(reader, int64(len(data)))
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
+ return
+ }
+
+ var skipped, removed, added madmin.IAMEntities
+ var failed madmin.IAMErrEntities
+
+ // import policies first
+ {
+ f, err := zr.Open(pathJoin(iamAssetsDir, allPoliciesFile))
+ switch {
+ case errors.Is(err, os.ErrNotExist):
+ case err != nil:
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allPoliciesFile, ""), r.URL)
+ return
+ default:
+ defer f.Close()
+ var allPolicies map[string]policy.Policy
+ data, err = io.ReadAll(f)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allPoliciesFile, ""), r.URL)
+ return
+ }
+ err = json.Unmarshal(data, &allPolicies)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, allPoliciesFile, ""), r.URL)
+ return
+ }
+ for policyName, policy := range allPolicies {
+ if policy.IsEmpty() {
+ err = globalIAMSys.DeletePolicy(ctx, policyName, true)
+ removed.Policies = append(removed.Policies, policyName)
+ } else {
+ _, err = globalIAMSys.SetPolicy(ctx, policyName, policy)
+ added.Policies = append(added.Policies, policyName)
+ }
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, importError(ctx, err, allPoliciesFile, policyName), r.URL)
+ return
+ }
+ }
+ }
+ }
+
+ // import users
+ {
+ f, err := zr.Open(pathJoin(iamAssetsDir, allUsersFile))
+ switch {
+ case errors.Is(err, os.ErrNotExist):
+ case err != nil:
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allUsersFile, ""), r.URL)
+ return
+ default:
+ defer f.Close()
+ var userAccts map[string]madmin.AddOrUpdateUserReq
+ data, err := io.ReadAll(f)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allUsersFile, ""), r.URL)
+ return
+ }
+ err = json.Unmarshal(data, &userAccts)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, allUsersFile, ""), r.URL)
+ return
+ }
+ for accessKey, ureq := range userAccts {
+ // Not allowed to add a user with same access key as root credential
+ if accessKey == globalActiveCred.AccessKey {
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAddUserInvalidArgument, err, allUsersFile, accessKey), r.URL)
+ return
+ }
+
+ user, exists := globalIAMSys.GetUser(ctx, accessKey)
+ if exists && (user.Credentials.IsTemp() || user.Credentials.IsServiceAccount()) {
+ // Updating STS credential is not allowed, and this API does not
+ // support updating service accounts.
+ 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.
+ if !exists && hasSpaceBE(accessKey) {
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminResourceInvalidArgument, err, allUsersFile, accessKey), r.URL)
+ return
+ }
+
+ if _, err = globalIAMSys.CreateUser(ctx, accessKey, ureq); err != nil {
+ failed.Users = append(failed.Users, madmin.IAMErrEntity{Name: accessKey, Error: err})
+ } else {
+ added.Users = append(added.Users, accessKey)
+ }
+ }
+ }
+ }
+
+ // import groups
+ {
+ f, err := zr.Open(pathJoin(iamAssetsDir, allGroupsFile))
+ switch {
+ case errors.Is(err, os.ErrNotExist):
+ case err != nil:
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allGroupsFile, ""), r.URL)
+ return
+ default:
+ defer f.Close()
+ var grpInfos map[string]GroupInfo
+ data, err := io.ReadAll(f)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allGroupsFile, ""), r.URL)
+ return
+ }
+ if err = json.Unmarshal(data, &grpInfos); err != nil {
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, allGroupsFile, ""), r.URL)
+ return
+ }
+ for group, grpInfo := range grpInfos {
+ // Check if group already exists
+ if _, gerr := globalIAMSys.GetGroupDescription(group); gerr != nil {
+ // If group does not exist, then check if the group has beginning and end space characters
+ // we will reject such group names.
+ if errors.Is(gerr, errNoSuchGroup) && hasSpaceBE(group) {
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminResourceInvalidArgument, gerr, allGroupsFile, group), r.URL)
+ return
+ }
+ }
+ if _, gerr := globalIAMSys.AddUsersToGroup(ctx, group, grpInfo.Members); gerr != nil {
+ failed.Groups = append(failed.Groups, madmin.IAMErrEntity{Name: group, Error: err})
+ } else {
+ added.Groups = append(added.Groups, group)
+ }
+ }
+ }
+ }
+
+ // import service accounts
+ {
+ f, err := zr.Open(pathJoin(iamAssetsDir, allSvcAcctsFile))
+ switch {
+ case errors.Is(err, os.ErrNotExist):
+ case err != nil:
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allSvcAcctsFile, ""), r.URL)
+ return
+ default:
+ defer f.Close()
+ var serviceAcctReqs map[string]madmin.SRSvcAccCreate
+ data, err := io.ReadAll(f)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allSvcAcctsFile, ""), r.URL)
+ return
+ }
+ if err = json.Unmarshal(data, &serviceAcctReqs); err != nil {
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, allSvcAcctsFile, ""), r.URL)
+ return
+ }
+
+ // Validations for LDAP enabled deployments.
+ if globalIAMSys.LDAPConfig.Enabled() {
+ skippedAccessKeys, err := globalIAMSys.NormalizeLDAPAccessKeypairs(ctx, serviceAcctReqs)
+ skipped.ServiceAccounts = append(skipped.ServiceAccounts, skippedAccessKeys...)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, ""), r.URL)
+ return
+ }
+ }
+
+ for user, svcAcctReq := range serviceAcctReqs {
+ if slices.Contains(skipped.ServiceAccounts, user) {
+ continue
+ }
+ var sp *policy.Policy
+ var err error
+ if len(svcAcctReq.SessionPolicy) > 0 {
+ sp, err = policy.ParseConfig(bytes.NewReader(svcAcctReq.SessionPolicy))
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, user), r.URL)
+ return
+ }
+ }
+ // service account access key cannot have space characters
+ // beginning and end of the string.
+ if hasSpaceBE(svcAcctReq.AccessKey) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
+ return
+ }
+ updateReq := true
+ _, _, err = globalIAMSys.GetServiceAccount(ctx, svcAcctReq.AccessKey)
+ if err != nil {
+ if !errors.Is(err, errNoSuchServiceAccount) {
+ writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, user), r.URL)
+ return
+ }
+ updateReq = false
+ }
+ if updateReq {
+ // If the service account exists, we remove it to ensure a
+ // clean import.
+ err := globalIAMSys.DeleteServiceAccount(ctx, svcAcctReq.AccessKey, true)
+ if err != nil {
+ delErr := fmt.Errorf("failed to delete existing service account (%s) before importing it: %w", svcAcctReq.AccessKey, err)
+ writeErrorResponseJSON(ctx, w, importError(ctx, delErr, allSvcAcctsFile, user), r.URL)
+ return
+ }
+ }
+ opts := newServiceAccountOpts{
+ accessKey: user,
+ secretKey: svcAcctReq.SecretKey,
+ sessionPolicy: sp,
+ claims: svcAcctReq.Claims,
+ name: svcAcctReq.Name,
+ description: svcAcctReq.Description,
+ expiration: svcAcctReq.Expiration,
+ allowSiteReplicatorAccount: false,
+ }
+
+ if _, _, err = globalIAMSys.NewServiceAccount(ctx, svcAcctReq.Parent, svcAcctReq.Groups, opts); err != nil {
+ failed.ServiceAccounts = append(failed.ServiceAccounts, madmin.IAMErrEntity{Name: user, Error: err})
+ } else {
+ added.ServiceAccounts = append(added.ServiceAccounts, user)
+ }
+ }
+ }
+ }
+
+ // import user policy mappings
+ {
+ f, err := zr.Open(pathJoin(iamAssetsDir, userPolicyMappingsFile))
+ switch {
+ case errors.Is(err, os.ErrNotExist):
+ case err != nil:
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, userPolicyMappingsFile, ""), r.URL)
+ return
+ default:
+ defer f.Close()
+ var userPolicyMap map[string]MappedPolicy
+ data, err := io.ReadAll(f)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, userPolicyMappingsFile, ""), r.URL)
+ return
+ }
+ if err = json.Unmarshal(data, &userPolicyMap); err != nil {
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, userPolicyMappingsFile, ""), r.URL)
+ return
+ }
+ for u, pm := range userPolicyMap {
+ // disallow setting policy mapping if user is a temporary user
+ ok, _, err := globalIAMSys.IsTempUser(u)
+ if err != nil && err != errNoSuchUser {
+ writeErrorResponseJSON(ctx, w, importError(ctx, err, userPolicyMappingsFile, u), r.URL)
+ return
+ }
+ if ok {
+ writeErrorResponseJSON(ctx, w, importError(ctx, errIAMActionNotAllowed, userPolicyMappingsFile, u), r.URL)
+ return
+ }
+ if _, err := globalIAMSys.PolicyDBSet(ctx, u, pm.Policies, regUser, false); err != nil {
+ failed.UserPolicies = append(
+ failed.UserPolicies,
+ madmin.IAMErrPolicyEntity{
+ Name: u,
+ Policies: strings.Split(pm.Policies, ","),
+ Error: err,
+ })
+ } else {
+ added.UserPolicies = append(added.UserPolicies, map[string][]string{u: strings.Split(pm.Policies, ",")})
+ }
+ }
+ }
+ }
+
+ // import group policy mappings
+ {
+ f, err := zr.Open(pathJoin(iamAssetsDir, groupPolicyMappingsFile))
+ switch {
+ case errors.Is(err, os.ErrNotExist):
+ case err != nil:
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, groupPolicyMappingsFile, ""), r.URL)
+ return
+ default:
+ defer f.Close()
+ var grpPolicyMap map[string]MappedPolicy
+ data, err := io.ReadAll(f)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, groupPolicyMappingsFile, ""), r.URL)
+ return
+ }
+ if err = json.Unmarshal(data, &grpPolicyMap); err != nil {
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, groupPolicyMappingsFile, ""), r.URL)
+ return
+ }
+
+ // Validations for LDAP enabled deployments.
+ if globalIAMSys.LDAPConfig.Enabled() {
+ isGroup := true
+ skippedDN, err := globalIAMSys.NormalizeLDAPMappingImport(ctx, isGroup, grpPolicyMap)
+ skipped.Groups = append(skipped.Groups, skippedDN...)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, importError(ctx, err, groupPolicyMappingsFile, ""), r.URL)
+ return
+ }
+ }
+
+ for g, pm := range grpPolicyMap {
+ if slices.Contains(skipped.Groups, g) {
+ continue
+ }
+ if _, err := globalIAMSys.PolicyDBSet(ctx, g, pm.Policies, unknownIAMUserType, true); err != nil {
+ failed.GroupPolicies = append(
+ failed.GroupPolicies,
+ madmin.IAMErrPolicyEntity{
+ Name: g,
+ Policies: strings.Split(pm.Policies, ","),
+ Error: err,
+ })
+ } else {
+ added.GroupPolicies = append(added.GroupPolicies, map[string][]string{g: strings.Split(pm.Policies, ",")})
+ }
+ }
+ }
+ }
+
+ // import sts user policy mappings
+ {
+ f, err := zr.Open(pathJoin(iamAssetsDir, stsUserPolicyMappingsFile))
+ switch {
+ case errors.Is(err, os.ErrNotExist):
+ case err != nil:
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, stsUserPolicyMappingsFile, ""), r.URL)
+ return
+ default:
+ defer f.Close()
+ var userPolicyMap map[string]MappedPolicy
+ data, err := io.ReadAll(f)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, stsUserPolicyMappingsFile, ""), r.URL)
+ return
+ }
+ if err = json.Unmarshal(data, &userPolicyMap); err != nil {
+ writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, stsUserPolicyMappingsFile, ""), r.URL)
+ return
+ }
+
+ // Validations for LDAP enabled deployments.
+ if globalIAMSys.LDAPConfig.Enabled() {
+ isGroup := true
+ skippedDN, err := globalIAMSys.NormalizeLDAPMappingImport(ctx, !isGroup, userPolicyMap)
+ skipped.Users = append(skipped.Users, skippedDN...)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, importError(ctx, err, stsUserPolicyMappingsFile, ""), r.URL)
+ return
+ }
+ }
+ for u, pm := range userPolicyMap {
+ if slices.Contains(skipped.Users, u) {
+ continue
+ }
+ // disallow setting policy mapping if user is a temporary user
+ ok, _, err := globalIAMSys.IsTempUser(u)
+ if err != nil && err != errNoSuchUser {
+ writeErrorResponseJSON(ctx, w, importError(ctx, err, stsUserPolicyMappingsFile, u), r.URL)
+ return
+ }
+ if ok {
+ writeErrorResponseJSON(ctx, w, importError(ctx, errIAMActionNotAllowed, stsUserPolicyMappingsFile, u), r.URL)
+ return
+ }
+
+ if _, err := globalIAMSys.PolicyDBSet(ctx, u, pm.Policies, stsUser, false); err != nil {
+ failed.STSPolicies = append(
+ failed.STSPolicies,
+ madmin.IAMErrPolicyEntity{
+ Name: u,
+ Policies: strings.Split(pm.Policies, ","),
+ Error: err,
+ })
+ } else {
+ added.STSPolicies = append(added.STSPolicies, map[string][]string{u: strings.Split(pm.Policies, ",")})
+ }
+ }
+ }
+ }
+
+ if apiVer == "v2" {
+ iamr := madmin.ImportIAMResult{
+ Skipped: skipped,
+ Removed: removed,
+ Added: added,
+ Failed: failed,
+ }
+
+ b, err := json.Marshal(iamr)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, b)
+ }
+}
+
+func addExpirationToCondValues(exp *time.Time, condValues map[string][]string) error {
+ if exp == nil || exp.IsZero() || exp.Equal(timeSentinel) {
+ return nil
+ }
+ dur := time.Until(*exp)
+ if dur <= 0 {
+ return errors.New("unsupported expiration time")
+ }
+ condValues["DurationSeconds"] = []string{strconv.FormatInt(int64(dur.Seconds()), 10)}
+ return nil
+}
+
+func commonAddServiceAccount(r *http.Request, ldap bool) (context.Context, auth.Credentials, newServiceAccountOpts, madmin.AddServiceAccountReq, string, APIError) {
+ ctx := r.Context()
+
+ // Get current object layer instance.
+ objectAPI := newObjectLayerFn()
+ if objectAPI == nil || globalNotificationSys == nil {
+ return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErr(ErrServerNotInitialized)
+ }
+
+ cred, owner, s3Err := validateAdminSignature(ctx, r, "")
+ if s3Err != ErrNone {
+ return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErr(s3Err)
+ }
+
+ password := cred.SecretKey
+ reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
+ if err != nil {
+ return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err)
+ }
+
+ var createReq madmin.AddServiceAccountReq
+ if err = json.Unmarshal(reqBytes, &createReq); err != nil {
+ return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err)
+ }
+
+ if createReq.Expiration != nil && !createReq.Expiration.IsZero() {
+ // truncate expiration at the second.
+ truncateTime := createReq.Expiration.Truncate(time.Second)
+ createReq.Expiration = &truncateTime
+ }
+
+ // service account access key cannot have space characters beginning and end of the string.
+ if hasSpaceBE(createReq.AccessKey) {
+ return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument)
+ }
+
+ if err := createReq.Validate(); err != nil {
+ // Since this validation would happen client side as well, we only send
+ // a generic error message here.
+ return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument)
+ }
+ // If the request did not set a TargetUser, the service account is
+ // created for the request sender.
+ targetUser := createReq.TargetUser
+ if targetUser == "" {
+ targetUser = cred.AccessKey
+ }
+
+ description := createReq.Description
+ if description == "" {
+ description = createReq.Comment
+ }
+ opts := newServiceAccountOpts{
+ accessKey: createReq.AccessKey,
+ secretKey: createReq.SecretKey,
+ name: createReq.Name,
+ description: description,
+ expiration: createReq.Expiration,
+ claims: make(map[string]any),
+ }
+
+ condValues := getConditionValues(r, "", cred)
+ err = addExpirationToCondValues(createReq.Expiration, condValues)
+ if err != nil {
+ return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", toAdminAPIErr(ctx, err)
+ }
+
+ denyOnly := (targetUser == cred.AccessKey || targetUser == cred.ParentUser)
+ if ldap && !denyOnly {
+ res, _ := globalIAMSys.LDAPConfig.GetValidatedDNForUsername(targetUser)
+ if res != nil && res.NormDN == cred.ParentUser {
+ denyOnly = true
+ }
+ }
+
+ // Check if action is allowed if creating access key for another user
+ // Check if action is explicitly denied if for self
+ if !globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.CreateServiceAccountAdminAction,
+ ConditionValues: condValues,
+ IsOwner: owner,
+ Claims: cred.Claims,
+ DenyOnly: denyOnly,
+ }) {
+ return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErr(ErrAccessDenied)
+ }
+
+ var sp *policy.Policy
+ if len(createReq.Policy) > 0 {
+ sp, err = policy.ParseConfig(bytes.NewReader(createReq.Policy))
+ if err != nil {
+ return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", toAdminAPIErr(ctx, err)
+ }
+ }
+
+ opts.sessionPolicy = sp
+
+ 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)
+}
diff --git a/cmd/admin-handlers-users_test.go b/cmd/admin-handlers-users_test.go
new file mode 100644
index 00000000..82826458
--- /dev/null
+++ b/cmd/admin-handlers-users_test.go
@@ -0,0 +1,1850 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "runtime"
+ "slices"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio-go/v7"
+ "github.com/minio/minio-go/v7/pkg/credentials"
+ "github.com/minio/minio-go/v7/pkg/s3utils"
+ "github.com/minio/minio-go/v7/pkg/set"
+ "github.com/minio/minio-go/v7/pkg/signer"
+ "github.com/minio/minio/internal/auth"
+ "github.com/minio/pkg/v3/env"
+)
+
+const (
+ testDefaultTimeout = 30 * time.Second
+)
+
+// API suite container for IAM
+type TestSuiteIAM struct {
+ TestSuiteCommon
+
+ ServerTypeDescription string
+
+ // Flag to turn on tests for etcd backend IAM
+ withEtcdBackend bool
+
+ endpoint string
+ adm *madmin.AdminClient
+ client *minio.Client
+}
+
+func newTestSuiteIAM(c TestSuiteCommon, withEtcdBackend bool) *TestSuiteIAM {
+ etcdStr := ""
+ if withEtcdBackend {
+ etcdStr = " (with etcd backend)"
+ }
+ return &TestSuiteIAM{
+ TestSuiteCommon: c,
+ ServerTypeDescription: fmt.Sprintf("%s%s", c.serverType, etcdStr),
+ withEtcdBackend: withEtcdBackend,
+ }
+}
+
+func (s *TestSuiteIAM) iamSetup(c *check) {
+ var err error
+ // strip url scheme from endpoint
+ s.endpoint = strings.TrimPrefix(s.endPoint, "http://")
+ if s.secure {
+ s.endpoint = strings.TrimPrefix(s.endPoint, "https://")
+ }
+
+ s.adm, err = madmin.New(s.endpoint, s.accessKey, s.secretKey, s.secure)
+ if err != nil {
+ c.Fatalf("error creating admin client: %v", err)
+ }
+ // Set transport, so that TLS is handled correctly.
+ s.adm.SetCustomTransport(s.TestSuiteCommon.client.Transport)
+
+ s.client, err = minio.New(s.endpoint, &minio.Options{
+ Creds: credentials.NewStaticV4(s.accessKey, s.secretKey, ""),
+ Secure: s.secure,
+ Transport: s.TestSuiteCommon.client.Transport,
+ })
+ if err != nil {
+ c.Fatalf("error creating minio client: %v", err)
+ }
+}
+
+// List of all IAM test suites (i.e. test server configuration combinations)
+// common to tests.
+var iamTestSuites = func() []*TestSuiteIAM {
+ baseTestCases := []TestSuiteCommon{
+ // Init and run test on ErasureSD backend with signature v4.
+ {serverType: "ErasureSD", signer: signerV4},
+ // Init and run test on ErasureSD backend, with tls enabled.
+ {serverType: "ErasureSD", signer: signerV4, secure: true},
+ // Init and run test on Erasure backend.
+ {serverType: "Erasure", signer: signerV4},
+ // Init and run test on ErasureSet backend.
+ {serverType: "ErasureSet", signer: signerV4},
+ }
+ testCases := []*TestSuiteIAM{}
+ for _, bt := range baseTestCases {
+ testCases = append(testCases,
+ newTestSuiteIAM(bt, false),
+ newTestSuiteIAM(bt, true),
+ )
+ }
+ return testCases
+}()
+
+const (
+ EnvTestEtcdBackend = "_MINIO_ETCD_TEST_SERVER"
+)
+
+func (s *TestSuiteIAM) setUpEtcd(c *check, etcdServer string) {
+ ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
+ defer cancel()
+
+ configCmds := []string{
+ "etcd",
+ "endpoints=" + etcdServer,
+ "path_prefix=" + mustGetUUID(),
+ }
+ _, err := s.adm.SetConfigKV(ctx, strings.Join(configCmds, " "))
+ if err != nil {
+ c.Fatalf("unable to setup Etcd for tests: %v", err)
+ }
+
+ s.RestartIAMSuite(c)
+}
+
+func (s *TestSuiteIAM) SetUpSuite(c *check) {
+ // If etcd backend is specified and etcd server is not present, the test
+ // is skipped.
+ etcdServer := env.Get(EnvTestEtcdBackend, "")
+ if s.withEtcdBackend && etcdServer == "" {
+ c.Skip("Skipping etcd backend IAM test as no etcd server is configured.")
+ }
+
+ s.TestSuiteCommon.SetUpSuite(c)
+
+ s.iamSetup(c)
+
+ if s.withEtcdBackend {
+ s.setUpEtcd(c, etcdServer)
+ }
+}
+
+func (s *TestSuiteIAM) RestartIAMSuite(c *check) {
+ s.RestartTestServer(c)
+
+ s.iamSetup(c)
+}
+
+func (s *TestSuiteIAM) getAdminClient(c *check, accessKey, secretKey, sessionToken string) *madmin.AdminClient {
+ madmClnt, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{
+ Creds: credentials.NewStaticV4(accessKey, secretKey, sessionToken),
+ Secure: s.secure,
+ })
+ if err != nil {
+ c.Fatalf("error creating user admin client: %s", err)
+ }
+ madmClnt.SetCustomTransport(s.TestSuiteCommon.client.Transport)
+ return madmClnt
+}
+
+func (s *TestSuiteIAM) getUserClient(c *check, accessKey, secretKey, sessionToken string) *minio.Client {
+ client, err := minio.New(s.endpoint, &minio.Options{
+ Creds: credentials.NewStaticV4(accessKey, secretKey, sessionToken),
+ Secure: s.secure,
+ Transport: s.TestSuiteCommon.client.Transport,
+ })
+ if err != nil {
+ c.Fatalf("error creating user minio client: %s", err)
+ }
+ return client
+}
+
+func TestIAMInternalIDPServerSuite(t *testing.T) {
+ if runtime.GOOS == globalWindowsOSName {
+ t.Skip("windows is clunky disable these tests")
+ }
+ for i, testCase := range iamTestSuites {
+ t.Run(
+ fmt.Sprintf("Test: %d, ServerType: %s", i+1, testCase.ServerTypeDescription),
+ func(t *testing.T) {
+ suite := testCase
+ c := &check{t, testCase.serverType}
+
+ suite.SetUpSuite(c)
+ suite.TestUserCreate(c)
+ suite.TestUserPolicyEscalationBug(c)
+ suite.TestPolicyCreate(c)
+ suite.TestCannedPolicies(c)
+ suite.TestGroupAddRemove(c)
+ suite.TestServiceAccountOpsByAdmin(c)
+ suite.TestServiceAccountPrivilegeEscalationBug(c)
+ suite.TestServiceAccountPrivilegeEscalationBug2_2025_10_15(c, true)
+ suite.TestServiceAccountPrivilegeEscalationBug2_2025_10_15(c, false)
+ suite.TestServiceAccountOpsByUser(c)
+ suite.TestServiceAccountDurationSecondsCondition(c)
+ suite.TestAddServiceAccountPerms(c)
+ suite.TearDownSuite(c)
+ },
+ )
+ }
+}
+
+func (s *TestSuiteIAM) TestUserCreate(c *check) {
+ ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
+ defer cancel()
+
+ // 1. Create a user.
+ accessKey, secretKey := mustGenerateCredentials(c)
+ err := s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled)
+ if err != nil {
+ c.Fatalf("Unable to set user: %v", err)
+ }
+
+ // 2. Check new user appears in listing
+ usersMap, err := s.adm.ListUsers(ctx)
+ if err != nil {
+ c.Fatalf("error listing: %v", err)
+ }
+ v, ok := usersMap[accessKey]
+ if !ok {
+ c.Fatalf("user not listed: %s", accessKey)
+ }
+ c.Assert(v.Status, madmin.AccountEnabled)
+
+ // 3. Associate policy and check that user can access
+ _, err = s.adm.AttachPolicy(ctx, madmin.PolicyAssociationReq{
+ Policies: []string{"readwrite"},
+ User: accessKey,
+ })
+ if err != nil {
+ c.Fatalf("unable to attach policy: %v", err)
+ }
+
+ client := s.getUserClient(c, accessKey, secretKey, "")
+ err = client.MakeBucket(ctx, getRandomBucketName(), minio.MakeBucketOptions{})
+ if err != nil {
+ c.Fatalf("user could not create bucket: %v", err)
+ }
+
+ // 3.10. Check that user's password can be updated.
+ _, newSecretKey := mustGenerateCredentials(c)
+ err = s.adm.SetUser(ctx, accessKey, newSecretKey, madmin.AccountEnabled)
+ if err != nil {
+ c.Fatalf("Unable to update user's secret key: %v", err)
+ }
+ // 3.10.1 Check that old password no longer works.
+ err = client.MakeBucket(ctx, getRandomBucketName(), minio.MakeBucketOptions{})
+ if err == nil {
+ c.Fatalf("user was unexpectedly able to create bucket with bad password!")
+ }
+ // 3.10.2 Check that new password works.
+ client = s.getUserClient(c, accessKey, newSecretKey, "")
+ err = client.MakeBucket(ctx, getRandomBucketName(), minio.MakeBucketOptions{})
+ if err != nil {
+ c.Fatalf("user could not create bucket: %v", err)
+ }
+
+ // 4. Check that user can be disabled and verify it.
+ err = s.adm.SetUserStatus(ctx, accessKey, madmin.AccountDisabled)
+ if err != nil {
+ c.Fatalf("could not set user account to disabled")
+ }
+ usersMap, err = s.adm.ListUsers(ctx)
+ if err != nil {
+ c.Fatalf("error listing: %v", err)
+ }
+ v, ok = usersMap[accessKey]
+ if !ok {
+ c.Fatalf("user was not listed after disabling: %s", accessKey)
+ }
+ c.Assert(v.Status, madmin.AccountDisabled)
+ err = client.MakeBucket(ctx, getRandomBucketName(), minio.MakeBucketOptions{})
+ if err == nil {
+ c.Fatalf("user account was not disabled!")
+ }
+
+ // 5. Check that user can be deleted and verify it.
+ err = s.adm.RemoveUser(ctx, accessKey)
+ if err != nil {
+ c.Fatalf("user could not be deleted: %v", err)
+ }
+ usersMap, err = s.adm.ListUsers(ctx)
+ if err != nil {
+ c.Fatalf("error listing: %v", err)
+ }
+ _, ok = usersMap[accessKey]
+ if ok {
+ c.Fatalf("user not deleted: %s", accessKey)
+ }
+ err = client.MakeBucket(ctx, getRandomBucketName(), minio.MakeBucketOptions{})
+ if err == nil {
+ c.Fatalf("user account was not deleted!")
+ }
+}
+
+func (s *TestSuiteIAM) TestUserPolicyEscalationBug(c *check) {
+ ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
+ defer cancel()
+
+ bucket := getRandomBucketName()
+ err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{})
+ if err != nil {
+ c.Fatalf("bucket creat error: %v", err)
+ }
+
+ // 2. Create a user, associate policy and verify access
+ accessKey, secretKey := mustGenerateCredentials(c)
+ err = s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled)
+ if err != nil {
+ c.Fatalf("Unable to set user: %v", err)
+ }
+ // 2.1 check that user does not have any access to the bucket
+ uClient := s.getUserClient(c, accessKey, secretKey, "")
+ c.mustNotListObjects(ctx, uClient, bucket)
+
+ // 2.2 create and associate policy to user
+ policy := "mypolicy-test-user-update"
+ policyBytes := fmt.Appendf(nil, `{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:ListBucket"
+ ],
+ "Resource": [
+ "arn:aws:s3:::%s"
+ ]
+ },
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:PutObject",
+ "s3:GetObject"
+ ],
+ "Resource": [
+ "arn:aws:s3:::%s/*"
+ ]
+ }
+ ]
+}`, bucket, bucket)
+ err = s.adm.AddCannedPolicy(ctx, policy, policyBytes)
+ if err != nil {
+ c.Fatalf("policy add error: %v", err)
+ }
+ _, err = s.adm.AttachPolicy(ctx, madmin.PolicyAssociationReq{
+ Policies: []string{policy},
+ User: accessKey,
+ })
+ if err != nil {
+ c.Fatalf("unable to attach policy: %v", err)
+ }
+ // 2.3 check user has access to bucket
+ c.mustListObjects(ctx, uClient, bucket)
+ // 2.3 check that user cannot delete the bucket
+ err = uClient.RemoveBucket(ctx, bucket)
+ if err == nil || err.Error() != "Access Denied." {
+ c.Fatalf("bucket was deleted unexpectedly or got unexpected err: %v", err)
+ }
+
+ // 3. Craft a request to update the user's permissions
+ ep := s.adm.GetEndpointURL()
+ urlValue := url.Values{}
+ urlValue.Add("accessKey", accessKey)
+ u, err := url.Parse(fmt.Sprintf("%s://%s/minio/admin/v3/add-user?%s", ep.Scheme, ep.Host, s3utils.QueryEncode(urlValue)))
+ if err != nil {
+ c.Fatalf("unexpected url parse err: %v", err)
+ }
+ req, err := http.NewRequestWithContext(ctx, http.MethodPut, u.String(), nil)
+ if err != nil {
+ c.Fatalf("unexpected new request err: %v", err)
+ }
+ reqBodyArg := madmin.UserInfo{
+ SecretKey: secretKey,
+ PolicyName: "consoleAdmin",
+ Status: madmin.AccountEnabled,
+ }
+ buf, err := json.Marshal(reqBodyArg)
+ if err != nil {
+ c.Fatalf("unexpected json encode err: %v", err)
+ }
+ buf, err = madmin.EncryptData(secretKey, buf)
+ if err != nil {
+ c.Fatalf("unexpected encryption err: %v", err)
+ }
+
+ req.ContentLength = int64(len(buf))
+ sum := sha256.Sum256(buf)
+ req.Header.Set("X-Amz-Content-Sha256", hex.EncodeToString(sum[:]))
+ req.Body = io.NopCloser(bytes.NewReader(buf))
+ req = signer.SignV4(*req, accessKey, secretKey, "", "")
+
+ // 3.1 Execute the request.
+ resp, err := s.TestSuiteCommon.client.Do(req)
+ if err != nil {
+ c.Fatalf("unexpected request err: %v", err)
+ }
+ if resp.StatusCode != 200 {
+ c.Fatalf("got unexpected response: %#v\n", resp)
+ }
+
+ // 3.2 check that user cannot delete the bucket
+ err = uClient.RemoveBucket(ctx, bucket)
+ if err == nil || err.Error() != "Access Denied." {
+ c.Fatalf("User was able to escalate privileges (Err=%v)!", err)
+ }
+}
+
+func (s *TestSuiteIAM) TestAddServiceAccountPerms(c *check) {
+ ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
+ defer cancel()
+
+ // 1. Create a policy
+ policy1 := "deny-svc"
+ policy2 := "allow-svc"
+ policyBytes := []byte(`{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Deny",
+ "Action": [
+ "admin:CreateServiceAccount"
+ ]
+ }
+ ]
+}`)
+
+ newPolicyBytes := []byte(`{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:ListBucket"
+ ],
+ "Resource": [
+ "arn:aws:s3:::testbucket"
+ ]
+ }
+ ]
+}`)
+
+ err := s.adm.AddCannedPolicy(ctx, policy1, policyBytes)
+ if err != nil {
+ c.Fatalf("policy add error: %v", err)
+ }
+
+ err = s.adm.AddCannedPolicy(ctx, policy2, newPolicyBytes)
+ if err != nil {
+ c.Fatalf("policy add error: %v", err)
+ }
+
+ // 2. Verify that policy json is validated by server
+ invalidPolicyBytes := policyBytes[:len(policyBytes)-1]
+ err = s.adm.AddCannedPolicy(ctx, policy1+"invalid", invalidPolicyBytes)
+ if err == nil {
+ c.Fatalf("invalid policy creation success")
+ }
+
+ // 3. Create a user, associate policy and verify access
+ accessKey, secretKey := mustGenerateCredentials(c)
+ err = s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled)
+ if err != nil {
+ c.Fatalf("Unable to set user: %v", err)
+ }
+ // 3.1 check that user does not have any access to the bucket
+ uClient := s.getUserClient(c, accessKey, secretKey, "")
+ c.mustNotListObjects(ctx, uClient, "testbucket")
+
+ // 3.2 associate policy to user
+ _, err = s.adm.AttachPolicy(ctx, madmin.PolicyAssociationReq{
+ Policies: []string{policy1},
+ User: accessKey,
+ })
+ if err != nil {
+ c.Fatalf("unable to attach policy: %v", err)
+ }
+
+ admClnt := s.getAdminClient(c, accessKey, secretKey, "")
+
+ // 3.3 check user does not have explicit permissions to create service account.
+ c.mustNotCreateSvcAccount(ctx, accessKey, admClnt)
+
+ // 4. Verify the policy appears in listing
+ ps, err := s.adm.ListCannedPolicies(ctx)
+ if err != nil {
+ c.Fatalf("policy list err: %v", err)
+ }
+ _, ok := ps[policy1]
+ if !ok {
+ c.Fatalf("policy was missing!")
+ }
+
+ // Detach policy1 to set up for policy2
+ _, err = s.adm.DetachPolicy(ctx, madmin.PolicyAssociationReq{
+ Policies: []string{policy1},
+ User: accessKey,
+ })
+ if err != nil {
+ c.Fatalf("unable to detach policy: %v", err)
+ }
+
+ // 3.2 associate policy to user
+ _, err = s.adm.AttachPolicy(ctx, madmin.PolicyAssociationReq{
+ Policies: []string{policy2},
+ User: accessKey,
+ })
+ if err != nil {
+ c.Fatalf("unable to attach policy: %v", err)
+ }
+
+ // 3.3 check user can create service account implicitly.
+ c.mustCreateSvcAccount(ctx, accessKey, admClnt)
+
+ _, ok = ps[policy2]
+ if !ok {
+ c.Fatalf("policy was missing!")
+ }
+
+ err = s.adm.RemoveUser(ctx, accessKey)
+ if err != nil {
+ c.Fatalf("user could not be deleted: %v", err)
+ }
+
+ err = s.adm.RemoveCannedPolicy(ctx, policy1)
+ if err != nil {
+ c.Fatalf("policy del err: %v", err)
+ }
+
+ err = s.adm.RemoveCannedPolicy(ctx, policy2)
+ if err != nil {
+ c.Fatalf("policy del err: %v", err)
+ }
+}
+
+func (s *TestSuiteIAM) TestPolicyCreate(c *check) {
+ ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
+ defer cancel()
+
+ bucket := getRandomBucketName()
+ err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{})
+ if err != nil {
+ c.Fatalf("bucket creat error: %v", err)
+ }
+
+ // 1. Create a policy
+ policy := "mypolicy"
+ policyBytes := fmt.Appendf(nil, `{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:ListBucket"
+ ],
+ "Resource": [
+ "arn:aws:s3:::%s"
+ ]
+ },
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:PutObject",
+ "s3:GetObject"
+ ],
+ "Resource": [
+ "arn:aws:s3:::%s/*"
+ ]
+ }
+ ]
+}`, bucket, bucket)
+ err = s.adm.AddCannedPolicy(ctx, policy, policyBytes)
+ if err != nil {
+ c.Fatalf("policy add error: %v", err)
+ }
+
+ // 2. Verify that policy json is validated by server
+ invalidPolicyBytes := policyBytes[:len(policyBytes)-1]
+ err = s.adm.AddCannedPolicy(ctx, policy+"invalid", invalidPolicyBytes)
+ if err == nil {
+ c.Fatalf("invalid policy creation success")
+ }
+
+ // 3. Create a user, associate policy and verify access
+ accessKey, secretKey := mustGenerateCredentials(c)
+ err = s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled)
+ if err != nil {
+ c.Fatalf("Unable to set user: %v", err)
+ }
+ // 3.1 check that user does not have any access to the bucket
+ uClient := s.getUserClient(c, accessKey, secretKey, "")
+ c.mustNotListObjects(ctx, uClient, bucket)
+
+ // 3.2 associate policy to user
+ _, err = s.adm.AttachPolicy(ctx, madmin.PolicyAssociationReq{
+ Policies: []string{policy},
+ User: accessKey,
+ })
+ if err != nil {
+ c.Fatalf("unable to attach policy: %v", err)
+ }
+ // 3.3 check user has access to bucket
+ c.mustListObjects(ctx, uClient, bucket)
+ // 3.4 Check that user cannot exceed their permissions
+ err = uClient.RemoveBucket(ctx, bucket)
+ if err == nil {
+ c.Fatalf("bucket was deleted!")
+ }
+
+ // 4. Verify the policy appears in listing
+ ps, err := s.adm.ListCannedPolicies(ctx)
+ if err != nil {
+ c.Fatalf("policy list err: %v", err)
+ }
+ _, ok := ps[policy]
+ if !ok {
+ c.Fatalf("policy was missing!")
+ }
+
+ // 5. Check that policy cannot be deleted when attached to a user.
+ err = s.adm.RemoveCannedPolicy(ctx, policy)
+ if err == nil {
+ c.Fatalf("policy could be unexpectedly deleted!")
+ }
+
+ // 6. Delete the user and then delete the policy.
+ err = s.adm.RemoveUser(ctx, accessKey)
+ if err != nil {
+ c.Fatalf("user could not be deleted: %v", err)
+ }
+ err = s.adm.RemoveCannedPolicy(ctx, policy)
+ if err != nil {
+ c.Fatalf("policy del err: %v", err)
+ }
+}
+
+func (s *TestSuiteIAM) TestCannedPolicies(c *check) {
+ ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
+ defer cancel()
+
+ policies, err := s.adm.ListCannedPolicies(ctx)
+ if err != nil {
+ c.Fatalf("unable to list policies: %v", err)
+ }
+
+ defaultPolicies := []string{
+ "readwrite",
+ "readonly",
+ "writeonly",
+ "diagnostics",
+ "consoleAdmin",
+ }
+
+ for _, v := range defaultPolicies {
+ if _, ok := policies[v]; !ok {
+ c.Fatalf("Failed to find %s in policies list", v)
+ }
+ }
+
+ bucket := getRandomBucketName()
+ err = s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{})
+ if err != nil {
+ c.Fatalf("bucket creat error: %v", err)
+ }
+
+ policyBytes := fmt.Appendf(nil, `{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:ListBucket"
+ ],
+ "Resource": [
+ "arn:aws:s3:::%s"
+ ]
+ },
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:PutObject",
+ "s3:GetObject"
+ ],
+ "Resource": [
+ "arn:aws:s3:::%s/*"
+ ]
+ }
+ ]
+}`, bucket, bucket)
+
+ // Check that default policies can be overwritten.
+ err = s.adm.AddCannedPolicy(ctx, "readwrite", policyBytes)
+ if err != nil {
+ c.Fatalf("policy add error: %v", err)
+ }
+
+ info, err := s.adm.InfoCannedPolicy(ctx, "readwrite")
+ if err != nil {
+ 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)
+ if !strings.Contains(infoStr, `"s3:PutObject"`) || !strings.Contains(infoStr, ":"+bucket+"/") {
+ c.Fatalf("policy contains unexpected content!")
+ }
+}
+
+func (s *TestSuiteIAM) TestGroupAddRemove(c *check) {
+ ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
+ defer cancel()
+
+ bucket := getRandomBucketName()
+ err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{})
+ if err != nil {
+ c.Fatalf("bucket creat error: %v", err)
+ }
+
+ policy := "mypolicy"
+ policyBytes := fmt.Appendf(nil, `{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:ListBucket"
+ ],
+ "Resource": [
+ "arn:aws:s3:::%s"
+ ]
+ },
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:PutObject",
+ "s3:GetObject"
+ ],
+ "Resource": [
+ "arn:aws:s3:::%s/*"
+ ]
+ }
+ ]
+}`, bucket, bucket)
+ err = s.adm.AddCannedPolicy(ctx, policy, policyBytes)
+ if err != nil {
+ c.Fatalf("policy add error: %v", err)
+ }
+
+ accessKey, secretKey := mustGenerateCredentials(c)
+ err = s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled)
+ if err != nil {
+ c.Fatalf("Unable to set user: %v", err)
+ }
+
+ // 1. Add user to a new group
+ group := "mygroup"
+ err = s.adm.UpdateGroupMembers(ctx, madmin.GroupAddRemove{
+ Group: group,
+ Members: []string{accessKey},
+ })
+ if err != nil {
+ c.Fatalf("Unable to add user to group: %v", err)
+ }
+
+ // 2. Check that user has no access
+ uClient := s.getUserClient(c, accessKey, secretKey, "")
+ c.mustNotListObjects(ctx, uClient, bucket)
+
+ // 3. Associate policy to group and check user got access.
+ _, err = s.adm.AttachPolicy(ctx, madmin.PolicyAssociationReq{
+ Policies: []string{policy},
+ Group: group,
+ })
+ if err != nil {
+ c.Fatalf("unable to attach policy: %v", err)
+ }
+ // 3.1 check user has access to bucket
+ c.mustListObjects(ctx, uClient, bucket)
+ // 3.2 Check that user cannot exceed their permissions
+ err = uClient.RemoveBucket(ctx, bucket)
+ if err == nil {
+ c.Fatalf("bucket was deleted!")
+ }
+
+ // 4. List groups and members and verify
+ groups, err := s.adm.ListGroups(ctx)
+ if err != nil {
+ c.Fatalf("group list err: %v", err)
+ }
+ expected := []string{group}
+ if !slices.Equal(groups, expected) {
+ c.Fatalf("expected group listing: %v, got: %v", expected, groups)
+ }
+ groupInfo, err := s.adm.GetGroupDescription(ctx, group)
+ if err != nil {
+ c.Fatalf("group desc err: %v", err)
+ }
+ c.Assert(groupInfo.Name, group)
+ c.Assert(set.CreateStringSet(groupInfo.Members...), set.CreateStringSet(accessKey))
+ c.Assert(groupInfo.Policy, policy)
+ c.Assert(groupInfo.Status, string(madmin.GroupEnabled))
+
+ // 5. Disable/enable the group and verify that user access is revoked/restored.
+ err = s.adm.SetGroupStatus(ctx, group, madmin.GroupDisabled)
+ if err != nil {
+ c.Fatalf("group set status err: %v", err)
+ }
+ groupInfo, err = s.adm.GetGroupDescription(ctx, group)
+ if err != nil {
+ c.Fatalf("group desc err: %v", err)
+ }
+ c.Assert(groupInfo.Status, string(madmin.GroupDisabled))
+ c.mustNotListObjects(ctx, uClient, bucket)
+
+ err = s.adm.SetGroupStatus(ctx, group, madmin.GroupEnabled)
+ if err != nil {
+ c.Fatalf("group set status err: %v", err)
+ }
+ groupInfo, err = s.adm.GetGroupDescription(ctx, group)
+ if err != nil {
+ c.Fatalf("group desc err: %v", err)
+ }
+ c.Assert(groupInfo.Status, string(madmin.GroupEnabled))
+ c.mustListObjects(ctx, uClient, bucket)
+
+ // 6. Verify that group cannot be deleted with users.
+ err = s.adm.UpdateGroupMembers(ctx, madmin.GroupAddRemove{
+ Group: group,
+ IsRemove: true,
+ })
+ if err == nil {
+ c.Fatalf("group was removed!")
+ }
+ groupInfo, err = s.adm.GetGroupDescription(ctx, group)
+ if err != nil {
+ c.Fatalf("group desc err: %v", err)
+ }
+ c.Assert(groupInfo.Name, group)
+
+ // 7. Remove user from group and verify access is revoked.
+ err = s.adm.UpdateGroupMembers(ctx, madmin.GroupAddRemove{
+ Group: group,
+ Members: []string{accessKey},
+ IsRemove: true,
+ })
+ if err != nil {
+ c.Fatalf("group update err: %v", err)
+ }
+ c.mustNotListObjects(ctx, uClient, bucket)
+
+ // 7.1 verify group still exists
+ groupInfo, err = s.adm.GetGroupDescription(ctx, group)
+ if err != nil {
+ c.Fatalf("group desc err: %v", err)
+ }
+ c.Assert(groupInfo.Name, group)
+ c.Assert(len(groupInfo.Members), 0)
+
+ // 8. Delete group and verify
+ err = s.adm.UpdateGroupMembers(ctx, madmin.GroupAddRemove{
+ Group: group,
+ IsRemove: true,
+ })
+ if err != nil {
+ c.Fatalf("group update err: %v", err)
+ }
+ groups, err = s.adm.ListGroups(ctx)
+ if err != nil {
+ c.Fatalf("group list err: %v", err)
+ }
+ if set.CreateStringSet(groups...).Contains(group) {
+ c.Fatalf("created group still present!")
+ }
+ _, err = s.adm.GetGroupDescription(ctx, group)
+ if err == nil {
+ c.Fatalf("group appears to exist")
+ }
+}
+
+func (s *TestSuiteIAM) TestServiceAccountOpsByUser(c *check) {
+ ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
+ defer cancel()
+
+ bucket := getRandomBucketName()
+ err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{})
+ if err != nil {
+ c.Fatalf("bucket creat error: %v", err)
+ }
+
+ // Create policy, user and associate policy
+ policy := "mypolicy"
+ policyBytes := fmt.Appendf(nil, `{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:ListBucket"
+ ],
+ "Resource": [
+ "arn:aws:s3:::%s"
+ ]
+ },
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:PutObject",
+ "s3:GetObject"
+ ],
+ "Resource": [
+ "arn:aws:s3:::%s/*"
+ ]
+ }
+ ]
+}`, bucket, bucket)
+ err = s.adm.AddCannedPolicy(ctx, policy, policyBytes)
+ if err != nil {
+ c.Fatalf("policy add error: %v", err)
+ }
+
+ accessKey, secretKey := mustGenerateCredentials(c)
+ err = s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled)
+ if err != nil {
+ c.Fatalf("Unable to set user: %v", err)
+ }
+
+ _, err = s.adm.AttachPolicy(ctx, madmin.PolicyAssociationReq{
+ Policies: []string{policy},
+ User: accessKey,
+ })
+ if err != nil {
+ c.Fatalf("unable to attach policy: %v", err)
+ }
+
+ // Create an madmin client with user creds
+ userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{
+ Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
+ Secure: s.secure,
+ })
+ if err != nil {
+ c.Fatalf("Err creating user admin client: %v", err)
+ }
+ userAdmClient.SetCustomTransport(s.TestSuiteCommon.client.Transport)
+
+ // Create svc acc
+ cr := c.mustCreateSvcAccount(ctx, accessKey, userAdmClient)
+
+ // 1. Check that svc account appears in listing
+ c.assertSvcAccAppearsInListing(ctx, userAdmClient, accessKey, cr.AccessKey)
+
+ // 2. Check that svc account info can be queried
+ c.assertSvcAccInfoQueryable(ctx, userAdmClient, accessKey, cr.AccessKey, false)
+
+ // 3. Check S3 access
+ c.assertSvcAccS3Access(ctx, s, cr, bucket)
+
+ // 5. Check that service account can be deleted.
+ c.assertSvcAccDeletion(ctx, s, userAdmClient, accessKey, bucket)
+
+ // 6. Check that service account cannot be created for some other user.
+ c.mustNotCreateSvcAccount(ctx, globalActiveCred.AccessKey, userAdmClient)
+}
+
+func (s *TestSuiteIAM) TestServiceAccountDurationSecondsCondition(c *check) {
+ ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
+ defer cancel()
+
+ bucket := getRandomBucketName()
+ err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{})
+ if err != nil {
+ c.Fatalf("bucket creat error: %v", err)
+ }
+
+ // Create policy, user and associate policy
+ policy := "mypolicy"
+ policyBytes := fmt.Appendf(nil, `{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Deny",
+ "Action": [
+ "admin:CreateServiceAccount",
+ "admin:UpdateServiceAccount"
+ ],
+ "Condition": {"NumericGreaterThan": {"svc:DurationSeconds": "3600"}}
+ },
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:ListBucket"
+ ],
+ "Resource": [
+ "arn:aws:s3:::%s"
+ ]
+ },
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:PutObject",
+ "s3:GetObject"
+ ],
+ "Resource": [
+ "arn:aws:s3:::%s/*"
+ ]
+ }
+ ]
+}`, bucket, bucket)
+ err = s.adm.AddCannedPolicy(ctx, policy, policyBytes)
+ if err != nil {
+ c.Fatalf("policy add error: %v", err)
+ }
+
+ accessKey, secretKey := mustGenerateCredentials(c)
+ err = s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled)
+ if err != nil {
+ c.Fatalf("Unable to set user: %v", err)
+ }
+
+ _, err = s.adm.AttachPolicy(ctx, madmin.PolicyAssociationReq{
+ Policies: []string{policy},
+ User: accessKey,
+ })
+ if err != nil {
+ c.Fatalf("unable to attach policy: %v", err)
+ }
+
+ // Create an madmin client with user creds
+ userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{
+ Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
+ Secure: s.secure,
+ })
+ if err != nil {
+ c.Fatalf("Err creating user admin client: %v", err)
+ }
+ userAdmClient.SetCustomTransport(s.TestSuiteCommon.client.Transport)
+
+ distantExpiration := time.Now().Add(30 * time.Minute)
+ cr, err := userAdmClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
+ TargetUser: accessKey,
+ AccessKey: "svc-accesskey",
+ SecretKey: "svc-secretkey",
+ Expiration: &distantExpiration,
+ })
+ if err != nil {
+ c.Fatalf("Unable to create svc acc: %v", err)
+ }
+
+ c.assertSvcAccS3Access(ctx, s, cr, bucket)
+
+ closeExpiration := time.Now().Add(2 * time.Hour)
+ _, err = userAdmClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
+ TargetUser: accessKey,
+ AccessKey: "svc-accesskey",
+ SecretKey: "svc-secretkey",
+ Expiration: &closeExpiration,
+ })
+ if err == nil {
+ c.Fatalf("Creating a svc acc with distant expiration should fail")
+ }
+}
+
+func (s *TestSuiteIAM) TestServiceAccountOpsByAdmin(c *check) {
+ ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
+ defer cancel()
+
+ bucket := getRandomBucketName()
+ err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{})
+ if err != nil {
+ c.Fatalf("bucket creat error: %v", err)
+ }
+
+ // Create policy, user and associate policy
+ policy := "mypolicy"
+ policyBytes := fmt.Appendf(nil, `{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:ListBucket"
+ ],
+ "Resource": [
+ "arn:aws:s3:::%s"
+ ]
+ },
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:PutObject",
+ "s3:GetObject"
+ ],
+ "Resource": [
+ "arn:aws:s3:::%s/*"
+ ]
+ }
+ ]
+}`, bucket, bucket)
+ err = s.adm.AddCannedPolicy(ctx, policy, policyBytes)
+ if err != nil {
+ c.Fatalf("policy add error: %v", err)
+ }
+
+ accessKey, secretKey := mustGenerateCredentials(c)
+ err = s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled)
+ if err != nil {
+ c.Fatalf("Unable to set user: %v", err)
+ }
+
+ _, err = s.adm.AttachPolicy(ctx, madmin.PolicyAssociationReq{
+ Policies: []string{policy},
+ User: accessKey,
+ })
+ if err != nil {
+ c.Fatalf("unable to attach policy: %v", err)
+ }
+
+ // 1. Create a service account for the user
+ cr := c.mustCreateSvcAccount(ctx, accessKey, s.adm)
+
+ // 1.2 Check that svc account appears in listing
+ c.assertSvcAccAppearsInListing(ctx, s.adm, accessKey, cr.AccessKey)
+
+ // 1.3 Check that svc account info can be queried
+ c.assertSvcAccInfoQueryable(ctx, s.adm, accessKey, cr.AccessKey, false)
+
+ // 2. Check that svc account can access the bucket
+ c.assertSvcAccS3Access(ctx, s, cr, bucket)
+
+ // 3. Check that svc account can restrict the policy, and that the
+ // session policy can be updated.
+ c.assertSvcAccSessionPolicyUpdate(ctx, s, s.adm, accessKey, bucket)
+
+ // 4. Check that service account's secret key and account status can be
+ // updated.
+ c.assertSvcAccSecretKeyAndStatusUpdate(ctx, s, s.adm, accessKey, bucket)
+
+ // 5. Check that service account can be deleted.
+ c.assertSvcAccDeletion(ctx, s, s.adm, accessKey, bucket)
+}
+
+func (s *TestSuiteIAM) TestServiceAccountPrivilegeEscalationBug(c *check) {
+ ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
+ defer cancel()
+
+ err := s.client.MakeBucket(ctx, "public", minio.MakeBucketOptions{})
+ if err != nil {
+ c.Fatalf("bucket creat error: %v", err)
+ }
+
+ err = s.client.MakeBucket(ctx, "private", minio.MakeBucketOptions{})
+ if err != nil {
+ c.Fatalf("bucket creat error: %v", err)
+ }
+
+ pubPolicyBytes := []byte(`{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:*"
+ ],
+ "Resource": [
+ "arn:aws:s3:::public",
+ "arn:aws:s3:::public/*"
+ ]
+ }
+ ]
+}`)
+
+ fullS3PolicyBytes := []byte(`{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:*"
+ ],
+ "Resource": [
+ "arn:aws:s3:::*"
+ ]
+ }
+ ]
+}
+`)
+
+ // Create a service account for the root user.
+ cr, err := s.adm.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
+ TargetUser: globalActiveCred.AccessKey,
+ Policy: pubPolicyBytes,
+ })
+ if err != nil {
+ c.Fatalf("admin should be able to create service account for themselves %s", err)
+ }
+
+ svcClient := s.getUserClient(c, cr.AccessKey, cr.SecretKey, "")
+
+ // Check that the service account can access the public bucket.
+ buckets, err := svcClient.ListBuckets(ctx)
+ if err != nil {
+ c.Fatalf("err fetching buckets %s", err)
+ }
+ if len(buckets) != 1 || buckets[0].Name != "public" {
+ c.Fatalf("service account should only have access to public bucket")
+ }
+
+ // Create an madmin client with the service account creds.
+ svcAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{
+ Creds: credentials.NewStaticV4(cr.AccessKey, cr.SecretKey, ""),
+ Secure: s.secure,
+ })
+ if err != nil {
+ c.Fatalf("Err creating svcacct admin client: %v", err)
+ }
+ svcAdmClient.SetCustomTransport(s.TestSuiteCommon.client.Transport)
+
+ // Attempt to update the policy on the service account.
+ err = svcAdmClient.UpdateServiceAccount(ctx, cr.AccessKey,
+ madmin.UpdateServiceAccountReq{
+ NewPolicy: fullS3PolicyBytes,
+ })
+
+ if err == nil {
+ c.Fatalf("service account should not be able to update policy on itself")
+ } else if !strings.Contains(err.Error(), "Access Denied") {
+ c.Fatalf("unexpected error: %v", err)
+ }
+}
+
+func (s *TestSuiteIAM) TestServiceAccountPrivilegeEscalationBug2_2025_10_15(c *check, forRoot bool) {
+ ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
+ defer cancel()
+
+ for i := range 3 {
+ err := s.client.MakeBucket(ctx, fmt.Sprintf("bucket%d", i+1), minio.MakeBucketOptions{})
+ if err != nil {
+ c.Fatalf("bucket create error: %v", err)
+ }
+ defer func(i int) {
+ _ = s.client.RemoveBucket(ctx, fmt.Sprintf("bucket%d", i+1))
+ }(i)
+ }
+
+ allow2BucketsPolicyBytes := []byte(`{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "ListBucket1AndBucket2",
+ "Effect": "Allow",
+ "Action": ["s3:ListBucket"],
+ "Resource": ["arn:aws:s3:::bucket1", "arn:aws:s3:::bucket2"]
+ },
+ {
+ "Sid": "ReadWriteBucket1AndBucket2Objects",
+ "Effect": "Allow",
+ "Action": [
+ "s3:DeleteObject",
+ "s3:DeleteObjectVersion",
+ "s3:GetObject",
+ "s3:GetObjectVersion",
+ "s3:PutObject"
+ ],
+ "Resource": ["arn:aws:s3:::bucket1/*", "arn:aws:s3:::bucket2/*"]
+ }
+ ]
+}`)
+
+ if forRoot {
+ // Create a service account for the root user.
+ _, err := s.adm.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
+ Policy: allow2BucketsPolicyBytes,
+ AccessKey: "restricted",
+ SecretKey: "restricted123",
+ })
+ if err != nil {
+ c.Fatalf("could not create service account")
+ }
+ defer func() {
+ _ = s.adm.DeleteServiceAccount(ctx, "restricted")
+ }()
+ } else {
+ // Create a regular user and attach consoleAdmin policy
+ err := s.adm.AddUser(ctx, "foobar", "foobar123")
+ if err != nil {
+ c.Fatalf("could not create user")
+ }
+
+ _, err = s.adm.AttachPolicy(ctx, madmin.PolicyAssociationReq{
+ Policies: []string{"consoleAdmin"},
+ User: "foobar",
+ })
+ if err != nil {
+ c.Fatalf("could not attach policy")
+ }
+
+ // Create a service account for the regular user.
+ _, err = s.adm.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
+ Policy: allow2BucketsPolicyBytes,
+ TargetUser: "foobar",
+ AccessKey: "restricted",
+ SecretKey: "restricted123",
+ })
+ if err != nil {
+ c.Fatalf("could not create service account: %v", err)
+ }
+ defer func() {
+ _ = s.adm.DeleteServiceAccount(ctx, "restricted")
+ _ = s.adm.RemoveUser(ctx, "foobar")
+ }()
+ }
+ restrictedClient := s.getUserClient(c, "restricted", "restricted123", "")
+
+ buckets, err := restrictedClient.ListBuckets(ctx)
+ if err != nil {
+ c.Fatalf("err fetching buckets %s", err)
+ }
+ if len(buckets) != 2 || buckets[0].Name != "bucket1" || buckets[1].Name != "bucket2" {
+ c.Fatalf("restricted service account should only have access to bucket1 and bucket2")
+ }
+
+ // Try to escalate privileges
+ restrictedAdmClient := s.getAdminClient(c, "restricted", "restricted123", "")
+ _, err = restrictedAdmClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
+ AccessKey: "newroot",
+ SecretKey: "newroot123",
+ })
+ if err == nil {
+ c.Fatalf("restricted service account was able to create service account bypassing sub-policy!")
+ }
+}
+
+func (s *TestSuiteIAM) SetUpAccMgmtPlugin(c *check) {
+ ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
+ defer cancel()
+
+ pluginEndpoint := env.Get("_MINIO_POLICY_PLUGIN_ENDPOINT", "")
+ if pluginEndpoint == "" {
+ c.Skip("_MINIO_POLICY_PLUGIN_ENDPOINT not given - skipping.")
+ }
+
+ configCmds := []string{
+ "policy_plugin",
+ "url=" + pluginEndpoint,
+ }
+
+ _, err := s.adm.SetConfigKV(ctx, strings.Join(configCmds, " "))
+ if err != nil {
+ c.Fatalf("unable to setup access management plugin for tests: %v", err)
+ }
+
+ s.RestartIAMSuite(c)
+}
+
+// TestIAM_AMPInternalIDPServerSuite - tests for access management plugin
+func TestIAM_AMPInternalIDPServerSuite(t *testing.T) {
+ for i, testCase := range iamTestSuites {
+ t.Run(
+ fmt.Sprintf("Test: %d, ServerType: %s", i+1, testCase.ServerTypeDescription),
+ func(t *testing.T) {
+ suite := testCase
+ c := &check{t, testCase.serverType}
+
+ suite.SetUpSuite(c)
+ defer suite.TearDownSuite(c)
+
+ suite.SetUpAccMgmtPlugin(c)
+
+ suite.TestAccMgmtPlugin(c)
+ },
+ )
+ }
+}
+
+// TestAccMgmtPlugin - this test assumes that the access-management-plugin is
+// the same as the example in `docs/iam/access-manager-plugin.go` -
+// specifically, it denies only `s3:Put*` operations on non-root accounts.
+func (s *TestSuiteIAM) TestAccMgmtPlugin(c *check) {
+ ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
+ defer cancel()
+
+ // 0. Check that owner is able to make-bucket.
+ bucket := getRandomBucketName()
+ err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{})
+ if err != nil {
+ c.Fatalf("bucket creat error: %v", err)
+ }
+
+ // 1. Create a user.
+ accessKey, secretKey := mustGenerateCredentials(c)
+ err = s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled)
+ if err != nil {
+ c.Fatalf("Unable to set user: %v", err)
+ }
+
+ // 2. Check new user appears in listing
+ usersMap, err := s.adm.ListUsers(ctx)
+ if err != nil {
+ c.Fatalf("error listing: %v", err)
+ }
+ v, ok := usersMap[accessKey]
+ if !ok {
+ c.Fatalf("user not listed: %s", accessKey)
+ }
+ c.Assert(v.Status, madmin.AccountEnabled)
+
+ // 3. Check that user is able to make a bucket.
+ client := s.getUserClient(c, accessKey, secretKey, "")
+ err = client.MakeBucket(ctx, getRandomBucketName(), minio.MakeBucketOptions{})
+ if err != nil {
+ c.Fatalf("user not create bucket: %v", err)
+ }
+
+ // 3.1 check user has access to bucket
+ c.mustListObjects(ctx, client, bucket)
+
+ // 3.2 check that user cannot upload an object.
+ _, err = client.PutObject(ctx, bucket, "objectName", bytes.NewBuffer([]byte("some content")), 12, minio.PutObjectOptions{})
+ if err == nil {
+ c.Fatalf("user was able to upload unexpectedly")
+ }
+
+ // Create an madmin client with user creds
+ userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{
+ Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
+ Secure: s.secure,
+ })
+ if err != nil {
+ c.Fatalf("Err creating user admin client: %v", err)
+ }
+ userAdmClient.SetCustomTransport(s.TestSuiteCommon.client.Transport)
+
+ // Create svc acc
+ cr := c.mustCreateSvcAccount(ctx, accessKey, userAdmClient)
+
+ // 1. Check that svc account appears in listing
+ c.assertSvcAccAppearsInListing(ctx, userAdmClient, accessKey, cr.AccessKey)
+
+ // 2. Check that svc account info can be queried
+ c.assertSvcAccInfoQueryable(ctx, userAdmClient, accessKey, cr.AccessKey, false)
+
+ // 3. Check S3 access
+ c.assertSvcAccS3Access(ctx, s, cr, bucket)
+
+ // Check that session policies do not apply - as policy enforcement is
+ // delegated to plugin.
+ {
+ svcAK, svcSK := mustGenerateCredentials(c)
+
+ // This policy does not allow listing objects.
+ policyBytes := fmt.Appendf(nil, `{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:PutObject",
+ "s3:GetObject"
+ ],
+ "Resource": [
+ "arn:aws:s3:::%s/*"
+ ]
+ }
+ ]
+}`, bucket)
+ cr, err := userAdmClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
+ Policy: policyBytes,
+ TargetUser: accessKey,
+ AccessKey: svcAK,
+ SecretKey: svcSK,
+ })
+ if err != nil {
+ c.Fatalf("Unable to create svc acc: %v", err)
+ }
+ svcClient := s.getUserClient(c, cr.AccessKey, cr.SecretKey, "")
+ // Though the attached policy does not allow listing, it will be
+ // ignored because the plugin allows it.
+ c.mustListObjects(ctx, svcClient, bucket)
+ }
+
+ // 4. Check that service account's secret key and account status can be
+ // updated.
+ c.assertSvcAccSecretKeyAndStatusUpdate(ctx, s, userAdmClient, accessKey, bucket)
+
+ // 5. Check that service account can be deleted.
+ c.assertSvcAccDeletion(ctx, s, userAdmClient, accessKey, bucket)
+
+ // 6. Check that service account **can** be created for some other user.
+ // This is possible because the policy enforced in the plugin.
+ c.mustCreateSvcAccount(ctx, globalActiveCred.AccessKey, userAdmClient)
+}
+
+func (c *check) mustCreateIAMUser(ctx context.Context, admClnt *madmin.AdminClient) madmin.Credentials {
+ c.Helper()
+ randUser := mustGetUUID()
+ randPass := mustGetUUID()
+ err := admClnt.AddUser(ctx, randUser, randPass)
+ if err != nil {
+ c.Fatalf("should be able to create a user: %v", err)
+ }
+ return madmin.Credentials{
+ AccessKey: randUser,
+ SecretKey: randPass,
+ }
+}
+
+func (c *check) mustGetIAMUserInfo(ctx context.Context, admClnt *madmin.AdminClient, accessKey string) madmin.UserInfo {
+ c.Helper()
+ ui, err := admClnt.GetUserInfo(ctx, accessKey)
+ if err != nil {
+ c.Fatalf("should be able to get user info: %v", err)
+ }
+ return ui
+}
+
+func (c *check) mustNotCreateIAMUser(ctx context.Context, admClnt *madmin.AdminClient) {
+ c.Helper()
+ randUser := mustGetUUID()
+ randPass := mustGetUUID()
+ err := admClnt.AddUser(ctx, randUser, randPass)
+ if err == nil {
+ c.Fatalf("should not be able to create a user")
+ }
+}
+
+func (c *check) mustCreateSvcAccount(ctx context.Context, tgtUser string, admClnt *madmin.AdminClient) madmin.Credentials {
+ c.Helper()
+ cr, err := admClnt.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
+ TargetUser: tgtUser,
+ })
+ if err != nil {
+ c.Fatalf("user should be able to create service accounts %s", err)
+ }
+ return cr
+}
+
+func (c *check) mustNotCreateSvcAccount(ctx context.Context, tgtUser string, admClnt *madmin.AdminClient) {
+ c.Helper()
+ _, err := admClnt.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
+ TargetUser: tgtUser,
+ })
+ if err == nil {
+ c.Fatalf("user was able to add service accounts unexpectedly!")
+ }
+}
+
+func (c *check) mustNotListObjects(ctx context.Context, client *minio.Client, bucket string) {
+ c.Helper()
+ res := client.ListObjects(ctx, bucket, minio.ListObjectsOptions{})
+ v, ok := <-res
+ if !ok || v.Err == nil {
+ c.Fatalf("user was able to list unexpectedly! on %s", bucket)
+ }
+}
+
+func (c *check) mustPutObjectWithTags(ctx context.Context, client *minio.Client, bucket, object string) {
+ c.Helper()
+ _, err := client.PutObject(ctx, bucket, object, bytes.NewBuffer([]byte("stuff")), 5, minio.PutObjectOptions{
+ UserTags: map[string]string{
+ "security": "public",
+ "virus": "true",
+ },
+ })
+ if err != nil {
+ c.Fatalf("user was unable to upload the object: %v", err)
+ }
+}
+
+func (c *check) mustGetObject(ctx context.Context, client *minio.Client, bucket, object string) {
+ c.Helper()
+
+ r, err := client.GetObject(ctx, bucket, object, minio.GetObjectOptions{})
+ if err != nil {
+ c.Fatalf("user was unable to download the object: %v", err)
+ }
+ defer r.Close()
+
+ _, err = io.Copy(io.Discard, r)
+ if err != nil {
+ c.Fatalf("user was unable to download the object: %v", err)
+ }
+}
+
+func (c *check) mustHeadObject(ctx context.Context, client *minio.Client, bucket, object string, tagCount int) {
+ c.Helper()
+
+ oinfo, err := client.StatObject(ctx, bucket, object, minio.StatObjectOptions{})
+ if err != nil {
+ c.Fatalf("user was unable to download the object: %v", err)
+ }
+
+ if oinfo.UserTagCount != tagCount {
+ c.Fatalf("expected tagCount: %d, got %d", tagCount, oinfo.UserTagCount)
+ }
+}
+
+func (c *check) mustListObjects(ctx context.Context, client *minio.Client, bucket string) {
+ c.Helper()
+ res := client.ListObjects(ctx, bucket, minio.ListObjectsOptions{})
+ v, ok := <-res
+ if ok && v.Err != nil {
+ c.Fatalf("user was unable to list: %v", v.Err)
+ }
+}
+
+func (c *check) mustListBuckets(ctx context.Context, client *minio.Client) {
+ c.Helper()
+ _, err := client.ListBuckets(ctx)
+ if err != nil {
+ c.Fatalf("user was unable to list buckets: %v", err)
+ }
+}
+
+func (c *check) mustNotDelete(ctx context.Context, client *minio.Client, bucket string, vid string) {
+ c.Helper()
+
+ err := client.RemoveObject(ctx, bucket, "some-object", minio.RemoveObjectOptions{VersionID: vid})
+ if err == nil {
+ c.Fatalf("user must not be allowed to delete")
+ }
+
+ err = client.RemoveObject(ctx, bucket, "some-object", minio.RemoveObjectOptions{})
+ if err != nil {
+ c.Fatal("user must be able to create delete marker")
+ }
+}
+
+func (c *check) mustDownload(ctx context.Context, client *minio.Client, bucket string) {
+ c.Helper()
+ rd, err := client.GetObject(ctx, bucket, "some-object", minio.GetObjectOptions{})
+ if err != nil {
+ c.Fatalf("download did not succeed got %#v", err)
+ }
+ if _, err = io.Copy(io.Discard, rd); err != nil {
+ c.Fatalf("download did not succeed got %#v", err)
+ }
+}
+
+func (c *check) mustUploadReturnVersions(ctx context.Context, client *minio.Client, bucket string) []string {
+ c.Helper()
+ versions := []string{}
+ for range 5 {
+ ui, err := client.PutObject(ctx, bucket, "some-object", bytes.NewBuffer([]byte("stuff")), 5, minio.PutObjectOptions{})
+ if err != nil {
+ c.Fatalf("upload did not succeed got %#v", err)
+ }
+ versions = append(versions, ui.VersionID)
+ }
+ return versions
+}
+
+func (c *check) mustUpload(ctx context.Context, client *minio.Client, bucket string) {
+ c.Helper()
+ _, err := client.PutObject(ctx, bucket, "some-object", bytes.NewBuffer([]byte("stuff")), 5, minio.PutObjectOptions{})
+ if err != nil {
+ c.Fatalf("upload did not succeed got %#v", err)
+ }
+}
+
+func (c *check) mustNotUpload(ctx context.Context, client *minio.Client, bucket string) {
+ c.Helper()
+ _, err := client.PutObject(ctx, bucket, "some-object", bytes.NewBuffer([]byte("stuff")), 5, minio.PutObjectOptions{})
+ if e, ok := err.(minio.ErrorResponse); ok {
+ if e.Code == "AccessDenied" {
+ return
+ }
+ }
+ c.Fatalf("upload did not get an AccessDenied error - got %#v instead", err)
+}
+
+func (c *check) assertSvcAccS3Access(ctx context.Context, s *TestSuiteIAM, cr madmin.Credentials, bucket string) {
+ svcClient := s.getUserClient(c, cr.AccessKey, cr.SecretKey, "")
+ c.mustListObjects(ctx, svcClient, bucket)
+}
+
+func (c *check) assertSvcAccAppearsInListing(ctx context.Context, madmClient *madmin.AdminClient, parentAK, svcAK string) {
+ c.Helper()
+ listResp, err := madmClient.ListServiceAccounts(ctx, parentAK)
+ if err != nil {
+ c.Fatalf("unable to list svc accounts: %v", err)
+ }
+ var accessKeys []string
+ for _, item := range listResp.Accounts {
+ accessKeys = append(accessKeys, item.AccessKey)
+ }
+ if !set.CreateStringSet(accessKeys...).Contains(svcAK) {
+ c.Fatalf("service account did not appear in listing!")
+ }
+}
+
+func (c *check) assertSvcAccInfoQueryable(ctx context.Context, madmClient *madmin.AdminClient, parentAK, svcAK string, skipParentUserCheck bool) {
+ infoResp, err := madmClient.InfoServiceAccount(ctx, svcAK)
+ if err != nil {
+ c.Fatalf("unable to get svc acc info: %v", err)
+ }
+ if !skipParentUserCheck {
+ c.Assert(infoResp.ParentUser, parentAK)
+ }
+ c.Assert(infoResp.AccountStatus, "on")
+ c.Assert(infoResp.ImpliedPolicy, true)
+}
+
+// This test assumes that the policy for `accessKey` allows listing on the given
+// bucket. It creates a session policy that restricts listing on the bucket and
+// then enables it again in a session policy update call.
+func (c *check) assertSvcAccSessionPolicyUpdate(ctx context.Context, s *TestSuiteIAM, madmClient *madmin.AdminClient, accessKey, bucket string) {
+ c.Helper()
+ svcAK, svcSK := mustGenerateCredentials(c)
+
+ // This policy does not allow listing objects.
+ policyBytes := fmt.Appendf(nil, `{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:PutObject",
+ "s3:GetObject"
+ ],
+ "Resource": [
+ "arn:aws:s3:::%s/*"
+ ]
+ }
+ ]
+}`, bucket)
+ cr, err := madmClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
+ Policy: policyBytes,
+ TargetUser: accessKey,
+ AccessKey: svcAK,
+ SecretKey: svcSK,
+ })
+ if err != nil {
+ c.Fatalf("Unable to create svc acc: %v", err)
+ }
+ svcClient := s.getUserClient(c, cr.AccessKey, cr.SecretKey, "")
+ c.mustNotListObjects(ctx, svcClient, bucket)
+
+ // This policy allows listing objects.
+ newPolicyBytes := fmt.Appendf(nil, `{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:ListBucket"
+ ],
+ "Resource": [
+ "arn:aws:s3:::%s"
+ ]
+ }
+ ]
+}`, bucket)
+ err = madmClient.UpdateServiceAccount(ctx, svcAK, madmin.UpdateServiceAccountReq{
+ NewPolicy: newPolicyBytes,
+ })
+ if err != nil {
+ c.Fatalf("unable to update session policy for svc acc: %v", err)
+ }
+ c.mustListObjects(ctx, svcClient, bucket)
+}
+
+func (c *check) assertSvcAccSecretKeyAndStatusUpdate(ctx context.Context, s *TestSuiteIAM, madmClient *madmin.AdminClient, accessKey, bucket string) {
+ c.Helper()
+ svcAK, svcSK := mustGenerateCredentials(c)
+ cr, err := madmClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
+ TargetUser: accessKey,
+ AccessKey: svcAK,
+ SecretKey: svcSK,
+ })
+ if err != nil {
+ c.Fatalf("Unable to create svc acc: %v", err)
+ }
+ svcClient := s.getUserClient(c, cr.AccessKey, cr.SecretKey, "")
+ c.mustListObjects(ctx, svcClient, bucket)
+
+ _, svcSK2 := mustGenerateCredentials(c)
+ err = madmClient.UpdateServiceAccount(ctx, svcAK, madmin.UpdateServiceAccountReq{
+ NewSecretKey: svcSK2,
+ })
+ if err != nil {
+ c.Fatalf("unable to update secret key for svc acc: %v", err)
+ }
+ // old creds should not work:
+ c.mustNotListObjects(ctx, svcClient, bucket)
+ // new creds work:
+ svcClient2 := s.getUserClient(c, cr.AccessKey, svcSK2, "")
+ c.mustListObjects(ctx, svcClient2, bucket)
+
+ // update status to disabled
+ err = madmClient.UpdateServiceAccount(ctx, svcAK, madmin.UpdateServiceAccountReq{
+ NewStatus: "off",
+ })
+ if err != nil {
+ c.Fatalf("unable to update secret key for svc acc: %v", err)
+ }
+ c.mustNotListObjects(ctx, svcClient2, bucket)
+}
+
+func (c *check) assertSvcAccDeletion(ctx context.Context, s *TestSuiteIAM, madmClient *madmin.AdminClient, accessKey, bucket string) {
+ c.Helper()
+ svcAK, svcSK := mustGenerateCredentials(c)
+ cr, err := madmClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
+ TargetUser: accessKey,
+ AccessKey: svcAK,
+ SecretKey: svcSK,
+ })
+ if err != nil {
+ c.Fatalf("Unable to create svc acc: %v", err)
+ }
+ svcClient := s.getUserClient(c, cr.AccessKey, cr.SecretKey, "")
+ c.mustListObjects(ctx, svcClient, bucket)
+
+ err = madmClient.DeleteServiceAccount(ctx, svcAK)
+ if err != nil {
+ c.Fatalf("unable to delete svc acc: %v", err)
+ }
+ c.mustNotListObjects(ctx, svcClient, bucket)
+}
+
+func mustGenerateCredentials(c *check) (string, string) {
+ c.Helper()
+ ak, sk, err := auth.GenerateCredentials()
+ if err != nil {
+ c.Fatalf("unable to generate credentials: %v", err)
+ }
+ return ak, sk
+}
diff --git a/cmd/admin-handlers.go b/cmd/admin-handlers.go
new file mode 100644
index 00000000..0dcad4cc
--- /dev/null
+++ b/cmd/admin-handlers.go
@@ -0,0 +1,3562 @@
+// Copyright (c) 2015-2022 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ crand "crypto/rand"
+ "crypto/rsa"
+ "crypto/subtle"
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/json"
+ "encoding/pem"
+ "errors"
+ "fmt"
+ "hash/crc32"
+ "io"
+ "math"
+ "net/http"
+ "net/url"
+ "os"
+ "path"
+ "path/filepath"
+ "regexp"
+ "runtime"
+ "sort"
+ "strconv"
+ "strings"
+ "sync/atomic"
+ "time"
+
+ "github.com/dustin/go-humanize"
+ "github.com/klauspost/compress/zip"
+ "github.com/minio/madmin-go/v3"
+ "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/internal/auth"
+ "github.com/minio/minio/internal/dsync"
+ "github.com/minio/minio/internal/grid"
+ "github.com/minio/minio/internal/handlers"
+ xhttp "github.com/minio/minio/internal/http"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/minio/internal/kms"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/mux"
+ xnet "github.com/minio/pkg/v3/net"
+ "github.com/minio/pkg/v3/policy"
+ "github.com/secure-io/sio-go"
+ "github.com/zeebo/xxh3"
+)
+
+const (
+ maxEConfigJSONSize = 262272
+ kubernetesVersionEndpoint = "https://kubernetes.default.svc/version"
+ anonymizeParam = "anonymize"
+ anonymizeStrict = "strict"
+)
+
+// Only valid query params for mgmt admin APIs.
+const (
+ mgmtBucket = "bucket"
+ mgmtPrefix = "prefix"
+ mgmtClientToken = "clientToken"
+ mgmtForceStart = "forceStart"
+ mgmtForceStop = "forceStop"
+)
+
+// ServerUpdateV2Handler - POST /minio/admin/v3/update?updateURL={updateURL}&type=2
+// ----------
+// updates all minio servers and restarts them gracefully.
+func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.ServerUpdateAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ if globalInplaceUpdateDisabled {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
+ 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)
+ updateURL := vars["updateURL"]
+ dryRun := r.Form.Get("dry-run") == "true"
+
+ mode := getMinioMode()
+ if updateURL == "" {
+ updateURL = minioReleaseInfoURL
+ if runtime.GOOS == globalWindowsOSName {
+ updateURL = minioReleaseWindowsInfoURL
+ }
+ }
+
+ local := globalLocalNodeName
+ if local == "" {
+ local = "127.0.0.1"
+ }
+
+ u, err := url.Parse(updateURL)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ content, err := downloadReleaseURL(u, updateTimeout, mode)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ sha256Sum, lrTime, releaseInfo, err := parseReleaseData(content)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ 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
+ // Download Binary Once
+ binC, bin, err := downloadBinary(u, mode)
+ if err != nil {
+ adminLogIf(ctx, fmt.Errorf("server update failed with %w", err))
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ if globalIsDistErasure {
+ // Push binary to other servers
+ for idx, nerr := range globalNotificationSys.VerifyBinary(ctx, u, sha256Sum, releaseInfo, binC) {
+ if nerr.Err != nil {
+ peerResults[nerr.Host.String()] = madmin.ServerPeerUpdateStatus{
+ Host: nerr.Host.String(),
+ Err: nerr.Err.Error(),
+ CurrentVersion: Version,
+ }
+ failedClients[idx] = true
+ } else {
+ peerResults[nerr.Host.String()] = madmin.ServerPeerUpdateStatus{
+ Host: nerr.Host.String(),
+ CurrentVersion: Version,
+ UpdatedVersion: lrTime.Format(MinioReleaseTagTimeLayout),
+ }
+ }
+ }
+ }
+
+ if err = verifyBinary(u, sha256Sum, releaseInfo, mode, bytes.NewReader(bin)); err != nil {
+ peerResults[local] = madmin.ServerPeerUpdateStatus{
+ Host: local,
+ Err: err.Error(),
+ CurrentVersion: Version,
+ }
+ } else {
+ peerResults[local] = madmin.ServerPeerUpdateStatus{
+ Host: local,
+ CurrentVersion: Version,
+ UpdatedVersion: lrTime.Format(MinioReleaseTagTimeLayout),
+ }
+ }
+
+ if !dryRun {
+ if globalIsDistErasure {
+ ng := WithNPeers(len(globalNotificationSys.peerClients))
+ for idx, client := range globalNotificationSys.peerClients {
+ if failedClients[idx] {
+ continue
+ }
+ client := client
+ ng.Go(ctx, func() error {
+ return client.CommitBinary(ctx)
+ }, idx, *client.host)
+ }
+
+ for _, nerr := range ng.Wait() {
+ if nerr.Err != nil {
+ prs, ok := peerResults[nerr.Host.String()]
+ if ok {
+ prs.Err = nerr.Err.Error()
+ peerResults[nerr.Host.String()] = prs
+ } else {
+ peerResults[nerr.Host.String()] = madmin.ServerPeerUpdateStatus{
+ Host: nerr.Host.String(),
+ Err: nerr.Err.Error(),
+ CurrentVersion: Version,
+ UpdatedVersion: lrTime.Format(MinioReleaseTagTimeLayout),
+ }
+ }
+ }
+ }
+ }
+ prs := peerResults[local]
+ if prs.Err == "" {
+ if err = commitBinary(); err != nil {
+ prs.Err = err.Error()
+ }
+ peerResults[local] = prs
+ }
+ }
+
+ prs, ok := peerResults[local]
+ if ok {
+ prs.WaitingDrives = waitingDrivesNode()
+ peerResults[local] = prs
+ }
+
+ if globalIsDistErasure {
+ // Notify all other MinIO peers signal service.
+ startTime := time.Now().Add(restartUpdateDelay)
+ ng := WithNPeers(len(globalNotificationSys.peerClients))
+ for idx, client := range globalNotificationSys.peerClients {
+ if failedClients[idx] {
+ continue
+ }
+ client := client
+ ng.Go(ctx, func() error {
+ prs, ok := peerResults[client.String()]
+ // We restart only on success, not for any failures.
+ if ok && prs.Err == "" {
+ return client.SignalService(serviceRestart, "", dryRun, &startTime)
+ }
+ return nil
+ }, idx, *client.host)
+ }
+
+ for _, nerr := range ng.Wait() {
+ if nerr.Err != nil {
+ waitingDrives := map[string]madmin.DiskMetrics{}
+ jerr := json.Unmarshal([]byte(nerr.Err.Error()), &waitingDrives)
+ if jerr == nil {
+ prs, ok := peerResults[nerr.Host.String()]
+ if ok {
+ prs.WaitingDrives = waitingDrives
+ peerResults[nerr.Host.String()] = prs
+ }
+ continue
+ }
+ }
+ }
+ }
+
+ for _, pr := range peerResults {
+ updateStatus.Results = append(updateStatus.Results, pr)
+ }
+
+ // Marshal API response
+ jsonBytes, err := json.Marshal(updateStatus)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, jsonBytes)
+
+ if !dryRun {
+ prs, ok := peerResults[local]
+ // We restart only on success, not for any failures.
+ if ok && prs.Err == "" {
+ globalServiceSignalCh <- serviceRestart
+ }
+ }
+}
+
+// ServerUpdateHandler - POST /minio/admin/v3/update?updateURL={updateURL}
+// ----------
+// updates all minio servers and restarts them gracefully.
+func (a adminAPIHandlers) ServerUpdateHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.ServerUpdateAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ if globalInplaceUpdateDisabled || currentReleaseTime.IsZero() {
+ // if MINIO_UPDATE=off - inplace update is disabled, mostly in containers.
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ updateURL := vars["updateURL"]
+ mode := getMinioMode()
+ if updateURL == "" {
+ updateURL = minioReleaseInfoURL
+ if runtime.GOOS == globalWindowsOSName {
+ updateURL = minioReleaseWindowsInfoURL
+ }
+ }
+
+ u, err := url.Parse(updateURL)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ content, err := downloadReleaseURL(u, updateTimeout, mode)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ sha256Sum, lrTime, releaseInfo, err := parseReleaseData(content)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ if lrTime.Sub(currentReleaseTime) <= 0 {
+ updateStatus := madmin.ServerUpdateStatus{
+ CurrentVersion: Version,
+ UpdatedVersion: 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
+
+ // Download Binary Once
+ binC, bin, err := downloadBinary(u, mode)
+ if err != nil {
+ adminLogIf(ctx, fmt.Errorf("server update failed with %w", err))
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Push binary to other servers
+ for _, nerr := range globalNotificationSys.VerifyBinary(ctx, u, sha256Sum, releaseInfo, binC) {
+ if nerr.Err != nil {
+ err := AdminError{
+ Code: AdminUpdateApplyFailure,
+ Message: nerr.Err.Error(),
+ StatusCode: http.StatusInternalServerError,
+ }
+ logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
+ adminLogIf(ctx, fmt.Errorf("server update failed with %w", err))
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ }
+
+ err = verifyBinary(u, sha256Sum, releaseInfo, mode, bytes.NewReader(bin))
+ if err != nil {
+ adminLogIf(ctx, fmt.Errorf("server update failed with %w", err))
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ for _, nerr := range globalNotificationSys.CommitBinary(ctx) {
+ if nerr.Err != nil {
+ err := AdminError{
+ Code: AdminUpdateApplyFailure,
+ Message: nerr.Err.Error(),
+ StatusCode: http.StatusInternalServerError,
+ }
+ logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
+ adminLogIf(ctx, fmt.Errorf("server update failed with %w", err))
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ }
+
+ err = commitBinary()
+ if err != nil {
+ adminLogIf(ctx, fmt.Errorf("server update failed with %w", err))
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ updateStatus := madmin.ServerUpdateStatus{
+ CurrentVersion: Version,
+ UpdatedVersion: lrTime.Format(MinioReleaseTagTimeLayout),
+ }
+
+ // Marshal API response
+ jsonBytes, err := json.Marshal(updateStatus)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, jsonBytes)
+
+ // Notify all other MinIO peers signal service.
+ for _, nerr := range globalNotificationSys.SignalService(serviceRestart) {
+ if nerr.Err != nil {
+ logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
+ adminLogIf(ctx, nerr.Err)
+ }
+ }
+
+ globalServiceSignalCh <- serviceRestart
+}
+
+// ServiceHandler - POST /minio/admin/v3/service?action={action}
+// ----------
+// Supports following actions:
+// - restart (restarts all the MinIO instances in a setup)
+// - stop (stops all the MinIO instances in a setup)
+// - freeze (freezes all incoming S3 API calls)
+// - unfreeze (unfreezes previously frozen S3 API calls)
+func (a adminAPIHandlers) ServiceHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ vars := mux.Vars(r)
+ action := vars["action"]
+
+ var serviceSig serviceSignal
+ switch madmin.ServiceAction(action) {
+ case madmin.ServiceActionRestart:
+ serviceSig = serviceRestart
+ case madmin.ServiceActionStop:
+ serviceSig = serviceStop
+ case madmin.ServiceActionFreeze:
+ serviceSig = serviceFreeze
+ case madmin.ServiceActionUnfreeze:
+ serviceSig = serviceUnFreeze
+ default:
+ adminLogIf(ctx, fmt.Errorf("Unrecognized service action %s requested", action), logger.ErrorKind)
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMalformedPOSTRequest), r.URL)
+ return
+ }
+
+ var objectAPI ObjectLayer
+ switch serviceSig {
+ case serviceRestart:
+ objectAPI, _ = validateAdminReq(ctx, w, r, policy.ServiceRestartAdminAction)
+ case serviceStop:
+ objectAPI, _ = validateAdminReq(ctx, w, r, policy.ServiceStopAdminAction)
+ case serviceFreeze, serviceUnFreeze:
+ objectAPI, _ = validateAdminReq(ctx, w, r, policy.ServiceFreezeAdminAction)
+ }
+ if objectAPI == nil {
+ return
+ }
+
+ // Notify all other MinIO peers signal service.
+ for _, nerr := range globalNotificationSys.SignalService(serviceSig) {
+ if nerr.Err != nil {
+ logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
+ adminLogIf(ctx, nerr.Err)
+ }
+ }
+
+ // Reply to the client before restarting, stopping MinIO server.
+ writeSuccessResponseHeadersOnly(w)
+
+ switch serviceSig {
+ case serviceFreeze:
+ freezeServices()
+ case serviceUnFreeze:
+ unfreezeServices()
+ case serviceRestart, serviceStop:
+ globalServiceSignalCh <- serviceSig
+ }
+}
+
+type servicePeerResult struct {
+ Host string `json:"host"`
+ Err string `json:"err,omitempty"`
+ WaitingDrives map[string]madmin.DiskMetrics `json:"waitingDrives,omitempty"`
+}
+
+type serviceResult struct {
+ Action madmin.ServiceAction `json:"action"`
+ DryRun bool `json:"dryRun"`
+ Results []servicePeerResult `json:"results,omitempty"`
+}
+
+// ServiceV2Handler - POST /minio/admin/v3/service?action={action}&type=2
+// ----------
+// Supports following actions:
+// - restart (restarts all the MinIO instances in a setup)
+// - stop (stops all the MinIO instances in a setup)
+// - freeze (freezes all incoming S3 API calls)
+// - unfreeze (unfreezes previously frozen S3 API calls)
+//
+// This newer API now returns back status per remote peer and local regarding
+// if a "restart/stop" was successful or not. Service signal now supports
+// a dry-run that helps skip the nodes that may have hung drives. By default
+// restart/stop will ignore the servers that are hung on drives. You can use
+// 'force' param to force restart even with hung drives if needed.
+func (a adminAPIHandlers) ServiceV2Handler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ vars := mux.Vars(r)
+ action := vars["action"]
+ dryRun := r.Form.Get("dry-run") == "true"
+
+ var serviceSig serviceSignal
+ act := madmin.ServiceAction(action)
+ switch act {
+ case madmin.ServiceActionRestart:
+ serviceSig = serviceRestart
+ case madmin.ServiceActionStop:
+ serviceSig = serviceStop
+ case madmin.ServiceActionFreeze:
+ serviceSig = serviceFreeze
+ case madmin.ServiceActionUnfreeze:
+ serviceSig = serviceUnFreeze
+ default:
+ adminLogIf(ctx, fmt.Errorf("Unrecognized service action %s requested", action), logger.ErrorKind)
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMalformedPOSTRequest), r.URL)
+ return
+ }
+
+ var objectAPI ObjectLayer
+ var execAt *time.Time
+ switch serviceSig {
+ case serviceRestart:
+ objectAPI, _ = validateAdminReq(ctx, w, r, policy.ServiceRestartAdminAction)
+ t := time.Now().Add(restartUpdateDelay)
+ execAt = &t
+ case serviceStop:
+ objectAPI, _ = validateAdminReq(ctx, w, r, policy.ServiceStopAdminAction)
+ case serviceFreeze, serviceUnFreeze:
+ objectAPI, _ = validateAdminReq(ctx, w, r, policy.ServiceFreezeAdminAction)
+ }
+ if objectAPI == nil {
+ return
+ }
+
+ // Notify all other MinIO peers signal service.
+ srvResult := serviceResult{Action: act, Results: []servicePeerResult{}}
+
+ process := act == madmin.ServiceActionRestart || act == madmin.ServiceActionStop
+ if process {
+ localhost := globalLocalNodeName
+ if globalLocalNodeName == "" {
+ localhost = "127.0.0.1"
+ }
+ waitingDrives := waitingDrivesNode()
+ srvResult.Results = append(srvResult.Results, servicePeerResult{
+ Host: localhost,
+ WaitingDrives: waitingDrives,
+ })
+ }
+
+ if globalIsDistErasure {
+ for _, nerr := range globalNotificationSys.SignalServiceV2(serviceSig, dryRun, execAt) {
+ if nerr.Err != nil && process {
+ waitingDrives := map[string]madmin.DiskMetrics{}
+ jerr := json.Unmarshal([]byte(nerr.Err.Error()), &waitingDrives)
+ if jerr == nil {
+ srvResult.Results = append(srvResult.Results, servicePeerResult{
+ Host: nerr.Host.String(),
+ WaitingDrives: waitingDrives,
+ })
+ continue
+ }
+ }
+ errStr := ""
+ if nerr.Err != nil {
+ errStr = nerr.Err.Error()
+ }
+ srvResult.Results = append(srvResult.Results, servicePeerResult{
+ Host: nerr.Host.String(),
+ Err: errStr,
+ })
+ }
+ }
+
+ srvResult.DryRun = dryRun
+
+ buf, err := json.Marshal(srvResult)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Reply to the client before restarting, stopping MinIO server.
+ writeSuccessResponseJSON(w, buf)
+
+ switch serviceSig {
+ case serviceFreeze:
+ freezeServices()
+ case serviceUnFreeze:
+ unfreezeServices()
+ case serviceRestart, serviceStop:
+ if !dryRun {
+ globalServiceSignalCh <- serviceSig
+ }
+ }
+}
+
+// ServerProperties holds some server information such as, version, region
+// uptime, etc..
+type ServerProperties struct {
+ Uptime int64 `json:"uptime"`
+ Version string `json:"version"`
+ CommitID string `json:"commitID"`
+ DeploymentID string `json:"deploymentID"`
+ Region string `json:"region"`
+ SQSARN []string `json:"sqsARN"`
+}
+
+// serverConnStats holds transferred bytes from/to the server
+type serverConnStats struct {
+ internodeInputBytes uint64
+ internodeOutputBytes uint64
+ s3InputBytes uint64
+ s3OutputBytes uint64
+}
+
+// ServerHTTPAPIStats holds total number of HTTP operations from/to the server,
+// including the average duration the call was spent.
+type ServerHTTPAPIStats struct {
+ APIStats map[string]int `json:"apiStats"`
+}
+
+// ServerHTTPStats holds all type of http operations performed to/from the server
+// including their average execution time.
+type ServerHTTPStats struct {
+ S3RequestsInQueue int32 `json:"s3RequestsInQueue"`
+ S3RequestsIncoming uint64 `json:"s3RequestsIncoming"`
+ CurrentS3Requests ServerHTTPAPIStats `json:"currentS3Requests"`
+ TotalS3Requests ServerHTTPAPIStats `json:"totalS3Requests"`
+ TotalS3Errors ServerHTTPAPIStats `json:"totalS3Errors"`
+ TotalS35xxErrors ServerHTTPAPIStats `json:"totalS35xxErrors"`
+ TotalS34xxErrors ServerHTTPAPIStats `json:"totalS34xxErrors"`
+ TotalS3Canceled ServerHTTPAPIStats `json:"totalS3Canceled"`
+ TotalS3RejectedAuth uint64 `json:"totalS3RejectedAuth"`
+ TotalS3RejectedTime uint64 `json:"totalS3RejectedTime"`
+ TotalS3RejectedHeader uint64 `json:"totalS3RejectedHeader"`
+ TotalS3RejectedInvalid uint64 `json:"totalS3RejectedInvalid"`
+}
+
+// StorageInfoHandler - GET /minio/admin/v3/storageinfo
+// ----------
+// Get server information
+func (a adminAPIHandlers) StorageInfoHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.StorageInfoAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ storageInfo := objectAPI.StorageInfo(ctx, true)
+
+ // Collect any disk healing.
+ healing, _ := getAggregatedBackgroundHealState(ctx, nil)
+ healDisks := make(map[string]struct{}, len(healing.HealDisks))
+ for _, disk := range healing.HealDisks {
+ healDisks[disk] = struct{}{}
+ }
+
+ // find all disks which belong to each respective endpoints
+ for i, disk := range storageInfo.Disks {
+ if _, ok := healDisks[disk.Endpoint]; ok {
+ storageInfo.Disks[i].Healing = true
+ }
+ }
+
+ // Marshal API response
+ jsonBytes, err := json.Marshal(storageInfo)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Reply with storage information (across nodes in a
+ // distributed setup) as json.
+ writeSuccessResponseJSON(w, jsonBytes)
+}
+
+// MetricsHandler - GET /minio/admin/v3/metrics
+// ----------
+// Get realtime server metrics
+func (a adminAPIHandlers) MetricsHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.ServerInfoAdminAction)
+ if objectAPI == nil {
+ return
+ }
+ const defaultMetricsInterval = time.Second
+
+ interval, err := time.ParseDuration(r.Form.Get("interval"))
+ if err != nil || interval < time.Second {
+ interval = defaultMetricsInterval
+ }
+
+ n, err := strconv.Atoi(r.Form.Get("n"))
+ if err != nil || n <= 0 {
+ n = math.MaxInt32
+ }
+
+ var types madmin.MetricType
+ if t, _ := strconv.ParseUint(r.Form.Get("types"), 10, 64); t != 0 {
+ types = madmin.MetricType(t)
+ } else {
+ types = madmin.MetricsAll
+ }
+
+ disks := strings.Split(r.Form.Get("disks"), ",")
+ byDisk := strings.EqualFold(r.Form.Get("by-disk"), "true")
+ var diskMap map[string]struct{}
+ if len(disks) > 0 && disks[0] != "" {
+ diskMap = make(map[string]struct{}, len(disks))
+ for _, k := range disks {
+ if k != "" {
+ diskMap[k] = struct{}{}
+ }
+ }
+ }
+ jobID := r.Form.Get("by-jobID")
+
+ hosts := strings.Split(r.Form.Get("hosts"), ",")
+ byHost := strings.EqualFold(r.Form.Get("by-host"), "true")
+ var hostMap map[string]struct{}
+ if len(hosts) > 0 && hosts[0] != "" {
+ hostMap = make(map[string]struct{}, len(hosts))
+ for _, k := range hosts {
+ if k != "" {
+ hostMap[k] = struct{}{}
+ }
+ }
+ }
+ dID := r.Form.Get("by-depID")
+ done := ctx.Done()
+ ticker := time.NewTicker(interval)
+ defer ticker.Stop()
+ w.Header().Set(xhttp.ContentType, string(mimeJSON))
+
+ enc := json.NewEncoder(w)
+ for n > 0 {
+ var m madmin.RealtimeMetrics
+ mLocal := collectLocalMetrics(types, collectMetricsOpts{
+ hosts: hostMap,
+ disks: diskMap,
+ jobID: jobID,
+ depID: dID,
+ })
+ m.Merge(&mLocal)
+ // Allow half the interval for collecting remote...
+ cctx, cancel := context.WithTimeout(ctx, interval/2)
+ mRemote := collectRemoteMetrics(cctx, types, collectMetricsOpts{
+ hosts: hostMap,
+ disks: diskMap,
+ jobID: jobID,
+ depID: dID,
+ })
+ cancel()
+ m.Merge(&mRemote)
+ if !byHost {
+ m.ByHost = nil
+ }
+ if !byDisk {
+ m.ByDisk = nil
+ }
+
+ m.Final = n <= 1
+
+ // Marshal API reesponse
+ if err := enc.Encode(&m); err != nil {
+ n = 0
+ }
+
+ n--
+ if n <= 0 {
+ break
+ }
+
+ // Flush before waiting for next...
+ xhttp.Flush(w)
+
+ select {
+ case <-ticker.C:
+ case <-done:
+ return
+ }
+ }
+}
+
+// DataUsageInfoHandler - GET /minio/admin/v3/datausage?capacity={true}
+// ----------
+// Get server/cluster data usage info
+func (a adminAPIHandlers) DataUsageInfoHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.DataUsageInfoAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ dataUsageInfo, err := loadDataUsageFromBackend(ctx, objectAPI)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ dataUsageInfoJSON, err := json.Marshal(dataUsageInfo)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Get capacity info when asked.
+ if r.Form.Get("capacity") == "true" {
+ sinfo := objectAPI.StorageInfo(ctx, false)
+ dataUsageInfo.TotalCapacity = GetTotalUsableCapacity(sinfo.Disks, sinfo)
+ dataUsageInfo.TotalFreeCapacity = GetTotalUsableCapacityFree(sinfo.Disks, sinfo)
+ if dataUsageInfo.TotalCapacity > dataUsageInfo.TotalFreeCapacity {
+ dataUsageInfo.TotalUsedCapacity = dataUsageInfo.TotalCapacity - dataUsageInfo.TotalFreeCapacity
+ }
+ }
+
+ writeSuccessResponseJSON(w, dataUsageInfoJSON)
+}
+
+func lriToLockEntry(l lockRequesterInfo, now time.Time, resource, server string) *madmin.LockEntry {
+ t := time.Unix(0, l.Timestamp)
+ entry := &madmin.LockEntry{
+ Timestamp: t,
+ Elapsed: now.Sub(t),
+ Resource: resource,
+ ServerList: []string{server},
+ Source: l.Source,
+ Owner: l.Owner,
+ ID: l.UID,
+ Quorum: l.Quorum,
+ }
+ if l.Writer {
+ entry.Type = "WRITE"
+ } else {
+ entry.Type = "READ"
+ }
+ return entry
+}
+
+func topLockEntries(peerLocks []*PeerLocks, stale bool) madmin.LockEntries {
+ now := time.Now().UTC()
+ entryMap := make(map[string]*madmin.LockEntry)
+ toEntry := func(lri lockRequesterInfo) string {
+ return fmt.Sprintf("%s/%s", lri.Name, lri.UID)
+ }
+ for _, peerLock := range peerLocks {
+ if peerLock == nil {
+ continue
+ }
+ for k, v := range peerLock.Locks {
+ for _, lockReqInfo := range v {
+ if val, ok := entryMap[toEntry(lockReqInfo)]; ok {
+ val.ServerList = append(val.ServerList, peerLock.Addr)
+ } else {
+ entryMap[toEntry(lockReqInfo)] = lriToLockEntry(lockReqInfo, now, k, peerLock.Addr)
+ }
+ }
+ }
+ }
+ var lockEntries madmin.LockEntries
+ for _, v := range entryMap {
+ if stale {
+ lockEntries = append(lockEntries, *v)
+ continue
+ }
+ if len(v.ServerList) >= v.Quorum {
+ lockEntries = append(lockEntries, *v)
+ }
+ }
+ sort.Sort(lockEntries)
+ return lockEntries
+}
+
+// PeerLocks holds server information result of one node
+type PeerLocks struct {
+ Addr string
+ Locks map[string][]lockRequesterInfo
+}
+
+// ForceUnlockHandler force unlocks requested resource
+func (a adminAPIHandlers) ForceUnlockHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.ForceUnlockAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ z, ok := objectAPI.(*erasureServerPools)
+ if !ok {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+
+ var args dsync.LockArgs
+ var lockers []dsync.NetLocker
+ for path := range strings.SplitSeq(vars["paths"], ",") {
+ if path == "" {
+ continue
+ }
+ args.Resources = append(args.Resources, path)
+ }
+
+ for _, lks := range z.serverPools[0].erasureLockers {
+ lockers = append(lockers, lks...)
+ }
+
+ for _, locker := range lockers {
+ locker.ForceUnlock(ctx, args)
+ }
+}
+
+// TopLocksHandler Get list of locks in use
+func (a adminAPIHandlers) TopLocksHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.TopLocksAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ count := 10 // by default list only top 10 entries
+ if countStr := r.Form.Get("count"); countStr != "" {
+ var err error
+ count, err = strconv.Atoi(countStr)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ }
+ stale := r.Form.Get("stale") == "true" // list also stale locks
+
+ peerLocks := globalNotificationSys.GetLocks(ctx, r)
+
+ topLocks := topLockEntries(peerLocks, stale)
+
+ // Marshal API response upto requested count.
+ if len(topLocks) > count && count > 0 {
+ topLocks = topLocks[:count]
+ }
+
+ jsonBytes, err := json.Marshal(topLocks)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Reply with storage information (across nodes in a
+ // distributed setup) as json.
+ writeSuccessResponseJSON(w, jsonBytes)
+}
+
+// StartProfilingResult contains the status of the starting
+// profiling action in a given server - deprecated API
+type StartProfilingResult struct {
+ NodeName string `json:"nodeName"`
+ Success bool `json:"success"`
+ Error string `json:"error"`
+}
+
+// StartProfilingHandler - POST /minio/admin/v3/profiling/start?profilerType={profilerType}
+// ----------
+// Enable server profiling
+func (a adminAPIHandlers) StartProfilingHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ // Validate request signature.
+ _, adminAPIErr := checkAdminRequestAuth(ctx, r, policy.ProfilingAdminAction, "")
+ if adminAPIErr != ErrNone {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
+ return
+ }
+
+ if globalNotificationSys == nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ profiles := strings.Split(vars["profilerType"], ",")
+ thisAddr, err := xnet.ParseHost(globalLocalNodeName)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ globalProfilerMu.Lock()
+ defer globalProfilerMu.Unlock()
+
+ if globalProfiler == nil {
+ globalProfiler = make(map[string]minioProfiler, 10)
+ }
+
+ // Stop profiler of all types if already running
+ for k, v := range globalProfiler {
+ for _, p := range profiles {
+ if p == k {
+ v.Stop()
+ delete(globalProfiler, k)
+ }
+ }
+ }
+
+ // Start profiling on remote servers.
+ var hostErrs []NotificationPeerErr
+ for _, profiler := range profiles {
+ hostErrs = append(hostErrs, globalNotificationSys.StartProfiling(ctx, profiler)...)
+
+ // Start profiling locally as well.
+ prof, err := startProfiler(profiler)
+ if err != nil {
+ hostErrs = append(hostErrs, NotificationPeerErr{
+ Host: *thisAddr,
+ Err: err,
+ })
+ } else {
+ globalProfiler[profiler] = prof
+ hostErrs = append(hostErrs, NotificationPeerErr{
+ Host: *thisAddr,
+ })
+ }
+ }
+
+ var startProfilingResult []StartProfilingResult
+
+ for _, nerr := range hostErrs {
+ result := StartProfilingResult{NodeName: nerr.Host.String()}
+ if nerr.Err != nil {
+ result.Error = nerr.Err.Error()
+ } else {
+ result.Success = true
+ }
+ startProfilingResult = append(startProfilingResult, result)
+ }
+
+ // Create JSON result and send it to the client
+ startProfilingResultInBytes, err := json.Marshal(startProfilingResult)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, startProfilingResultInBytes)
+}
+
+// ProfileHandler - POST /minio/admin/v3/profile/?profilerType={profilerType}
+// ----------
+// Enable server profiling
+func (a adminAPIHandlers) ProfileHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ // Validate request signature.
+ _, adminAPIErr := checkAdminRequestAuth(ctx, r, policy.ProfilingAdminAction, "")
+ if adminAPIErr != ErrNone {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
+ return
+ }
+
+ if globalNotificationSys == nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+ profileStr := r.Form.Get("profilerType")
+ profiles := strings.Split(profileStr, ",")
+ duration := time.Minute
+ if dstr := r.Form.Get("duration"); dstr != "" {
+ var err error
+ duration, err = time.ParseDuration(dstr)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
+ return
+ }
+ }
+
+ globalProfilerMu.Lock()
+ if globalProfiler == nil {
+ globalProfiler = make(map[string]minioProfiler, 10)
+ }
+
+ // Stop profiler of all types if already running
+ for k, v := range globalProfiler {
+ v.Stop()
+ delete(globalProfiler, k)
+ }
+
+ // Start profiling on remote servers.
+ for _, profiler := range profiles {
+ // Limit start time to max 10s.
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ globalNotificationSys.StartProfiling(ctx, profiler)
+ // StartProfiling blocks, so we can cancel now.
+ cancel()
+
+ // Start profiling locally as well.
+ prof, err := startProfiler(profiler)
+ if err == nil {
+ globalProfiler[profiler] = prof
+ }
+ }
+ globalProfilerMu.Unlock()
+
+ timer := time.NewTimer(duration)
+ defer timer.Stop()
+ for {
+ select {
+ case <-ctx.Done():
+ // Stop remote profiles
+ go globalNotificationSys.DownloadProfilingData(GlobalContext, io.Discard)
+
+ // Stop local
+ globalProfilerMu.Lock()
+ defer globalProfilerMu.Unlock()
+ for k, v := range globalProfiler {
+ v.Stop()
+ delete(globalProfiler, k)
+ }
+ return
+ case <-timer.C:
+ if !globalNotificationSys.DownloadProfilingData(ctx, w) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminProfilerNotEnabled), r.URL)
+ return
+ }
+ return
+ }
+ }
+}
+
+// dummyFileInfo represents a dummy representation of a profile data file
+// present only in memory, it helps to generate the zip stream.
+type dummyFileInfo struct {
+ name string
+ size int64
+ mode os.FileMode
+ modTime time.Time
+ isDir bool
+ sys any
+}
+
+func (f dummyFileInfo) Name() string { return f.name }
+func (f dummyFileInfo) Size() int64 { return f.size }
+func (f dummyFileInfo) Mode() os.FileMode { return f.mode }
+func (f dummyFileInfo) ModTime() time.Time { return f.modTime }
+func (f dummyFileInfo) IsDir() bool { return f.isDir }
+func (f dummyFileInfo) Sys() any { return f.sys }
+
+// DownloadProfilingHandler - POST /minio/admin/v3/profiling/download
+// ----------
+// Download profiling information of all nodes in a zip format - deprecated API
+func (a adminAPIHandlers) DownloadProfilingHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ // Validate request signature.
+ _, adminAPIErr := checkAdminRequestAuth(ctx, r, policy.ProfilingAdminAction, "")
+ if adminAPIErr != ErrNone {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
+ return
+ }
+
+ if globalNotificationSys == nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ if !globalNotificationSys.DownloadProfilingData(ctx, w) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminProfilerNotEnabled), r.URL)
+ return
+ }
+}
+
+type healInitParams struct {
+ bucket, objPrefix string
+ hs madmin.HealOpts
+ clientToken string
+ forceStart, forceStop bool
+}
+
+// extractHealInitParams - Validates params for heal init API.
+func extractHealInitParams(vars map[string]string, qParams url.Values, r io.Reader) (hip healInitParams, err APIErrorCode) {
+ hip.bucket = vars[mgmtBucket]
+ hip.objPrefix = vars[mgmtPrefix]
+
+ if hip.bucket == "" {
+ if hip.objPrefix != "" {
+ // Bucket is required if object-prefix is given
+ err = ErrHealMissingBucket
+ return hip, err
+ }
+ } else if isReservedOrInvalidBucket(hip.bucket, false) {
+ err = ErrInvalidBucketName
+ return hip, err
+ }
+
+ // empty prefix is valid.
+ if !IsValidObjectPrefix(hip.objPrefix) {
+ err = ErrInvalidObjectName
+ return hip, err
+ }
+
+ if len(qParams[mgmtClientToken]) > 0 {
+ hip.clientToken = qParams[mgmtClientToken][0]
+ }
+ if _, ok := qParams[mgmtForceStart]; ok {
+ hip.forceStart = true
+ }
+ if _, ok := qParams[mgmtForceStop]; ok {
+ hip.forceStop = true
+ }
+
+ // Invalid request conditions:
+ //
+ // Cannot have both forceStart and forceStop in the same
+ // request; If clientToken is provided, request can only be
+ // to continue receiving logs, so it cannot be start or
+ // stop;
+ if (hip.forceStart && hip.forceStop) ||
+ (hip.clientToken != "" && (hip.forceStart || hip.forceStop)) {
+ err = ErrInvalidRequest
+ return hip, err
+ }
+
+ // ignore body if clientToken is provided
+ if hip.clientToken == "" {
+ jerr := json.NewDecoder(r).Decode(&hip.hs)
+ if jerr != nil {
+ adminLogIf(GlobalContext, jerr, logger.ErrorKind)
+ err = ErrRequestBodyParse
+ return hip, err
+ }
+ }
+
+ err = ErrNone
+ return hip, err
+}
+
+// HealHandler - POST /minio/admin/v3/heal/
+// -----------
+// Start heal processing and return heal status items.
+//
+// On a successful heal sequence start, a unique client token is
+// returned. Subsequent requests to this endpoint providing the client
+// token will receive heal status records from the running heal
+// sequence.
+//
+// If no client token is provided, and a heal sequence is in progress
+// an error is returned with information about the running heal
+// sequence. However, if the force-start flag is provided, the server
+// aborts the running heal sequence and starts a new one.
+func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.HealAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ hip, errCode := extractHealInitParams(mux.Vars(r), r.Form, r.Body)
+ if errCode != ErrNone {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(errCode), r.URL)
+ return
+ }
+
+ // Analyze the heal token and route the request accordingly
+ token, _, success := proxyRequestByToken(ctx, w, r, hip.clientToken, false)
+ if success {
+ return
+ }
+ hip.clientToken = token
+ // if request was not successful, try this server locally if token
+ // is not found the call will fail anyways. if token is empty
+ // try this server to generate a new token.
+
+ type healResp struct {
+ respBytes []byte
+ apiErr APIError
+ errBody string
+ }
+
+ // Define a closure to start sending whitespace to client
+ // after 10s unless a response item comes in
+ keepConnLive := func(w http.ResponseWriter, r *http.Request, respCh chan healResp) {
+ ticker := time.NewTicker(time.Second * 10)
+ defer ticker.Stop()
+ started := false
+ forLoop:
+ for {
+ select {
+ case <-r.Context().Done():
+ return
+ case <-ticker.C:
+ if !started {
+ // Start writing response to client
+ started = true
+ setCommonHeaders(w)
+ setEventStreamHeaders(w)
+ // Set 200 OK status
+ w.WriteHeader(200)
+ }
+ // Send whitespace and keep connection open
+ if _, err := w.Write([]byte(" ")); err != nil {
+ return
+ }
+ xhttp.Flush(w)
+ case hr := <-respCh:
+ switch hr.apiErr {
+ case noError:
+ if started {
+ if _, err := w.Write(hr.respBytes); err != nil {
+ return
+ }
+ xhttp.Flush(w)
+ } else {
+ writeSuccessResponseJSON(w, hr.respBytes)
+ }
+ default:
+ var errorRespJSON []byte
+ if hr.errBody == "" {
+ errorRespJSON = encodeResponseJSON(getAPIErrorResponse(ctx, hr.apiErr,
+ r.URL.Path, w.Header().Get(xhttp.AmzRequestID),
+ w.Header().Get(xhttp.AmzRequestHostID)))
+ } else {
+ errorRespJSON = encodeResponseJSON(APIErrorResponse{
+ Code: hr.apiErr.Code,
+ Message: hr.errBody,
+ Resource: r.URL.Path,
+ RequestID: w.Header().Get(xhttp.AmzRequestID),
+ HostID: globalDeploymentID(),
+ })
+ }
+ if !started {
+ setCommonHeaders(w)
+ w.Header().Set(xhttp.ContentType, string(mimeJSON))
+ w.WriteHeader(hr.apiErr.HTTPStatusCode)
+ }
+ if _, err := w.Write(errorRespJSON); err != nil {
+ return
+ }
+ xhttp.Flush(w)
+ }
+ break forLoop
+ }
+ }
+ }
+
+ healPath := pathJoin(hip.bucket, hip.objPrefix)
+ if hip.clientToken == "" && !hip.forceStart && !hip.forceStop {
+ nh, exists := globalAllHealState.getHealSequence(healPath)
+ if exists && !nh.hasEnded() && len(nh.currentStatus.Items) > 0 {
+ clientToken := nh.clientToken
+ if globalIsDistErasure {
+ clientToken = fmt.Sprintf("%s%s%d", nh.clientToken, getKeySeparator(), GetProxyEndpointLocalIndex(globalProxyEndpoints))
+ }
+ b, err := json.Marshal(madmin.HealStartSuccess{
+ ClientToken: clientToken,
+ ClientAddress: nh.clientAddress,
+ StartTime: nh.startTime,
+ })
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ // Client token not specified but a heal sequence exists on a path,
+ // Send the token back to client.
+ writeSuccessResponseJSON(w, b)
+ return
+ }
+ }
+
+ if hip.clientToken != "" && !hip.forceStart && !hip.forceStop {
+ // Since clientToken is given, fetch heal status from running
+ // heal sequence.
+ respBytes, errCode := globalAllHealState.PopHealStatusJSON(
+ healPath, hip.clientToken)
+ if errCode != ErrNone {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(errCode), r.URL)
+ } else {
+ writeSuccessResponseJSON(w, respBytes)
+ }
+ return
+ }
+
+ respCh := make(chan healResp, 1)
+ switch {
+ case hip.forceStop:
+ go func() {
+ respBytes, apiErr := globalAllHealState.stopHealSequence(healPath)
+ hr := healResp{respBytes: respBytes, apiErr: apiErr}
+ respCh <- hr
+ }()
+ case hip.clientToken == "":
+ nh := newHealSequence(GlobalContext, hip.bucket, hip.objPrefix, handlers.GetSourceIP(r), hip.hs, hip.forceStart)
+ go func() {
+ respBytes, apiErr, errMsg := globalAllHealState.LaunchNewHealSequence(nh, objectAPI)
+ hr := healResp{respBytes, apiErr, errMsg}
+ respCh <- hr
+ }()
+ }
+
+ // Due to the force-starting functionality, the Launch
+ // call above can take a long time - to keep the
+ // connection alive, we start sending whitespace
+ keepConnLive(w, r, respCh)
+}
+
+// getAggregatedBackgroundHealState returns the heal state of disks.
+// If no ObjectLayer is provided no set status is returned.
+func getAggregatedBackgroundHealState(ctx context.Context, o ObjectLayer) (madmin.BgHealState, error) {
+ // Get local heal status first
+ bgHealStates, ok := getLocalBackgroundHealStatus(ctx, o)
+ if !ok {
+ return bgHealStates, errServerNotInitialized
+ }
+
+ if globalIsDistErasure {
+ // Get heal status from other peers
+ peersHealStates, nerrs := globalNotificationSys.BackgroundHealStatus(ctx)
+ var errCount int
+ for _, nerr := range nerrs {
+ if nerr.Err != nil {
+ adminLogIf(ctx, nerr.Err)
+ errCount++
+ }
+ }
+ if errCount == len(nerrs) {
+ return madmin.BgHealState{}, fmt.Errorf("all remote servers failed to report heal status, cluster is unhealthy")
+ }
+ bgHealStates.Merge(peersHealStates...)
+ }
+
+ return bgHealStates, nil
+}
+
+func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.HealAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ aggregateHealStateResult, err := getAggregatedBackgroundHealState(r.Context(), objectAPI)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ if err := json.NewEncoder(w).Encode(aggregateHealStateResult); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+}
+
+// SitePerfHandler - measures network throughput between site replicated setups
+func (a adminAPIHandlers) SitePerfHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.HealthInfoAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ if !globalSiteReplicationSys.isEnabled() {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+ return
+ }
+
+ nsLock := objectAPI.NewNSLock(minioMetaBucket, "site-net-perf")
+ lkctx, err := nsLock.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(toAPIErrorCode(ctx, err)), r.URL)
+ return
+ }
+ ctx = lkctx.Context()
+ defer nsLock.Unlock(lkctx)
+
+ durationStr := r.Form.Get(peerRESTDuration)
+ duration, err := time.ParseDuration(durationStr)
+ if err != nil {
+ duration = globalNetPerfMinDuration
+ }
+
+ if duration < globalNetPerfMinDuration {
+ // We need sample size of minimum 10 secs.
+ duration = globalNetPerfMinDuration
+ }
+
+ duration = duration.Round(time.Second)
+
+ results, err := globalSiteReplicationSys.Netperf(ctx, duration)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(toAPIErrorCode(ctx, err)), r.URL)
+ return
+ }
+ enc := json.NewEncoder(w)
+ if err := enc.Encode(results); err != nil {
+ return
+ }
+}
+
+// ClientDevNullExtraTime - return extratime for last devnull
+// [POST] /minio/admin/v3/speedtest/client/devnull/extratime
+func (a adminAPIHandlers) ClientDevNullExtraTime(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.BandwidthMonitorAction)
+ if objectAPI == nil {
+ return
+ }
+
+ enc := json.NewEncoder(w)
+ if err := enc.Encode(madmin.ClientPerfExtraTime{TimeSpent: atomic.LoadInt64(&globalLastClientPerfExtraTime)}); err != nil {
+ return
+ }
+}
+
+// ClientDevNull - everything goes to io.Discard
+// [POST] /minio/admin/v3/speedtest/client/devnull
+func (a adminAPIHandlers) ClientDevNull(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ timeStart := time.Now()
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.BandwidthMonitorAction)
+ if objectAPI == nil {
+ return
+ }
+
+ nsLock := objectAPI.NewNSLock(minioMetaBucket, "client-perf")
+ lkctx, err := nsLock.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(toAPIErrorCode(ctx, err)), r.URL)
+ return
+ }
+ ctx = lkctx.Context()
+ defer nsLock.Unlock(lkctx)
+ timeEnd := time.Now()
+
+ atomic.SwapInt64(&globalLastClientPerfExtraTime, timeEnd.Sub(timeStart).Nanoseconds())
+
+ ctx, cancel := context.WithTimeout(ctx, madmin.MaxClientPerfTimeout)
+ defer cancel()
+ totalRx := int64(0)
+ connectTime := time.Now()
+ for {
+ n, err := io.CopyN(xioutil.Discard, r.Body, 128*humanize.KiByte)
+ if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
+ // would mean the network is not stable. Logging here will help in debugging network issues.
+ if time.Since(connectTime) < (globalNetPerfMinDuration - time.Second) {
+ adminLogIf(ctx, err)
+ }
+ }
+ totalRx += n
+ if err != nil || ctx.Err() != nil || totalRx > 100*humanize.GiByte {
+ break
+ }
+ }
+ w.WriteHeader(http.StatusOK)
+}
+
+// NetperfHandler - perform mesh style network throughput test
+func (a adminAPIHandlers) NetperfHandler(w http.ResponseWriter, r *http.Request) {
+ ctx, cancel := context.WithCancel(r.Context())
+ defer cancel()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.HealthInfoAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ if !globalIsDistErasure {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+ return
+ }
+
+ nsLock := objectAPI.NewNSLock(minioMetaBucket, "netperf")
+ lkctx, err := nsLock.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(toAPIErrorCode(ctx, err)), r.URL)
+ return
+ }
+ ctx = lkctx.Context()
+ defer nsLock.Unlock(lkctx)
+
+ // Freeze all incoming S3 API calls before running speedtest.
+ globalNotificationSys.ServiceFreeze(ctx, true)
+
+ // Unfreeze as soon as request context is canceled or when the function returns.
+ go func() {
+ <-ctx.Done()
+ globalNotificationSys.ServiceFreeze(ctx, false)
+ }()
+
+ durationStr := r.Form.Get(peerRESTDuration)
+ duration, err := time.ParseDuration(durationStr)
+ if err != nil {
+ duration = globalNetPerfMinDuration
+ }
+
+ if duration < globalNetPerfMinDuration {
+ // We need sample size of minimum 10 secs.
+ duration = globalNetPerfMinDuration
+ }
+
+ duration = duration.Round(time.Second)
+
+ results := globalNotificationSys.Netperf(ctx, duration)
+ enc := json.NewEncoder(w)
+ if err := enc.Encode(madmin.NetperfResult{NodeResults: results}); err != nil {
+ return
+ }
+}
+
+func isAllowedRWAccess(r *http.Request, cred auth.Credentials, bucketName string) (rd, wr bool) {
+ owner := cred.AccessKey == globalActiveCred.AccessKey
+
+ // Set prefix value for "s3:prefix" policy conditionals.
+ r.Header.Set("prefix", "")
+
+ // Set delimiter value for "s3:delimiter" policy conditionals.
+ r.Header.Set("delimiter", SlashSeparator)
+
+ isAllowedAccess := func(bucketName string) (rd, wr bool) {
+ if globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.GetObjectAction,
+ BucketName: bucketName,
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ ObjectName: "",
+ Claims: cred.Claims,
+ }) {
+ rd = true
+ }
+
+ if globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.PutObjectAction,
+ BucketName: bucketName,
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ ObjectName: "",
+ Claims: cred.Claims,
+ }) {
+ wr = true
+ }
+
+ return rd, wr
+ }
+ return isAllowedAccess(bucketName)
+}
+
+// ObjectSpeedTestHandler - reports maximum speed of a cluster by performing PUT and
+// GET operations on the server, supports auto tuning by default by automatically
+// increasing concurrency and stopping when we have reached the limits on the
+// system.
+func (a adminAPIHandlers) ObjectSpeedTestHandler(w http.ResponseWriter, r *http.Request) {
+ ctx, cancel := context.WithCancel(r.Context())
+ defer cancel()
+
+ objectAPI, creds := validateAdminReq(ctx, w, r, policy.HealthInfoAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ if !globalAPIConfig.permitRootAccess() {
+ rd, wr := isAllowedRWAccess(r, creds, globalObjectPerfBucket)
+ if !rd || !wr {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, AdminError{
+ Code: "XMinioSpeedtestInsufficientPermissions",
+ Message: fmt.Sprintf("%s does not have read and write access to '%s' bucket", creds.AccessKey,
+ globalObjectPerfBucket),
+ StatusCode: http.StatusForbidden,
+ }), r.URL)
+ return
+ }
+ }
+
+ sizeStr := r.Form.Get(peerRESTSize)
+ durationStr := r.Form.Get(peerRESTDuration)
+ concurrentStr := r.Form.Get(peerRESTConcurrent)
+ storageClass := strings.TrimSpace(r.Form.Get(peerRESTStorageClass))
+ customBucket := strings.TrimSpace(r.Form.Get(peerRESTBucket))
+ autotune := r.Form.Get("autotune") == "true"
+ noClear := r.Form.Get("noclear") == "true"
+ enableSha256 := r.Form.Get("enableSha256") == "true"
+ enableMultipart := r.Form.Get("enableMultipart") == "true"
+
+ size, err := strconv.Atoi(sizeStr)
+ if err != nil {
+ size = 64 * humanize.MiByte
+ }
+
+ concurrent, err := strconv.Atoi(concurrentStr)
+ if err != nil {
+ concurrent = 32
+ }
+
+ duration, err := time.ParseDuration(durationStr)
+ if err != nil {
+ duration = time.Second * 10
+ }
+
+ storageInfo := objectAPI.StorageInfo(ctx, false)
+
+ sufficientCapacity, canAutotune, capacityErrMsg := validateObjPerfOptions(ctx, storageInfo, concurrent, size, autotune)
+ if !sufficientCapacity {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, AdminError{
+ Code: "XMinioSpeedtestInsufficientCapacity",
+ Message: capacityErrMsg,
+ StatusCode: http.StatusInsufficientStorage,
+ }), r.URL)
+ return
+ }
+
+ if autotune && !canAutotune {
+ autotune = false
+ }
+
+ if customBucket == "" {
+ customBucket = globalObjectPerfBucket
+
+ bucketExists, err := makeObjectPerfBucket(ctx, objectAPI, customBucket)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if !noClear && !bucketExists {
+ defer deleteObjectPerfBucket(objectAPI)
+ }
+ }
+
+ if !noClear {
+ defer objectAPI.DeleteObject(ctx, customBucket, speedTest+SlashSeparator, ObjectOptions{
+ DeletePrefix: true,
+ })
+ }
+
+ // Freeze all incoming S3 API calls before running speedtest.
+ globalNotificationSys.ServiceFreeze(ctx, true)
+
+ // Unfreeze as soon as request context is canceled or when the function returns.
+ go func() {
+ <-ctx.Done()
+ globalNotificationSys.ServiceFreeze(ctx, false)
+ }()
+
+ keepAliveTicker := time.NewTicker(500 * time.Millisecond)
+ defer keepAliveTicker.Stop()
+
+ enc := json.NewEncoder(w)
+ ch := objectSpeedTest(ctx, speedTestOpts{
+ objectSize: size,
+ concurrencyStart: concurrent,
+ duration: duration,
+ autotune: autotune,
+ storageClass: storageClass,
+ bucketName: customBucket,
+ enableSha256: enableSha256,
+ enableMultipart: enableMultipart,
+ creds: creds,
+ })
+ var prevResult madmin.SpeedTestResult
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-keepAliveTicker.C:
+ // if previous result is set keep writing the
+ // previous result back to the client
+ if prevResult.Version != "" {
+ if err := enc.Encode(prevResult); err != nil {
+ return
+ }
+ } else {
+ // first result is not yet obtained, keep writing
+ // empty entry to prevent client from disconnecting.
+ if err := enc.Encode(madmin.SpeedTestResult{}); err != nil {
+ return
+ }
+ }
+ xhttp.Flush(w)
+ case result, ok := <-ch:
+ if !ok {
+ return
+ }
+ if err := enc.Encode(result); err != nil {
+ return
+ }
+ prevResult = result
+ xhttp.Flush(w)
+ }
+ }
+}
+
+func makeObjectPerfBucket(ctx context.Context, objectAPI ObjectLayer, bucketName string) (bucketExists bool, err error) {
+ if err = objectAPI.MakeBucket(ctx, bucketName, MakeBucketOptions{VersioningEnabled: globalSiteReplicationSys.isEnabled()}); err != nil {
+ if _, ok := err.(BucketExists); !ok {
+ // Only BucketExists error can be ignored.
+ return false, err
+ }
+ bucketExists = true
+ }
+
+ if globalSiteReplicationSys.isEnabled() {
+ configData := []byte(`Enabledspeedtest/*`)
+ if _, err = globalBucketMetadataSys.Update(ctx, bucketName, bucketVersioningConfig, configData); err != nil {
+ return false, err
+ }
+ }
+
+ return bucketExists, nil
+}
+
+func deleteObjectPerfBucket(objectAPI ObjectLayer) {
+ objectAPI.DeleteBucket(context.Background(), globalObjectPerfBucket, DeleteBucketOptions{
+ Force: true,
+ SRDeleteOp: getSRBucketDeleteOp(globalSiteReplicationSys.isEnabled()),
+ })
+}
+
+func validateObjPerfOptions(ctx context.Context, storageInfo madmin.StorageInfo, concurrent int, size int, autotune bool) (bool, bool, string) {
+ capacityNeeded := uint64(concurrent * size)
+ capacity := GetTotalUsableCapacityFree(storageInfo.Disks, storageInfo)
+
+ if capacity < capacityNeeded {
+ return false, false, fmt.Sprintf("not enough usable space available to perform speedtest - expected %s, got %s",
+ humanize.IBytes(capacityNeeded), humanize.IBytes(capacity))
+ }
+
+ // Verify if we can employ autotune without running out of capacity,
+ // if we do run out of capacity, make sure to turn-off autotuning
+ // in such situations.
+ if autotune {
+ newConcurrent := concurrent + (concurrent+1)/2
+ autoTunedCapacityNeeded := uint64(newConcurrent * size)
+ if capacity < autoTunedCapacityNeeded {
+ // Turn-off auto-tuning if next possible concurrency would reach beyond disk capacity.
+ return true, false, ""
+ }
+ }
+
+ return true, autotune, ""
+}
+
+// DriveSpeedtestHandler - reports throughput of drives available in the cluster
+func (a adminAPIHandlers) DriveSpeedtestHandler(w http.ResponseWriter, r *http.Request) {
+ ctx, cancel := context.WithCancel(r.Context())
+ defer cancel()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.HealthInfoAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ // Freeze all incoming S3 API calls before running speedtest.
+ globalNotificationSys.ServiceFreeze(ctx, true)
+
+ // Unfreeze as soon as request context is canceled or when the function returns.
+ go func() {
+ <-ctx.Done()
+ globalNotificationSys.ServiceFreeze(ctx, false)
+ }()
+
+ serial := r.Form.Get("serial") == "true"
+ blockSizeStr := r.Form.Get("blocksize")
+ fileSizeStr := r.Form.Get("filesize")
+
+ blockSize, err := strconv.ParseUint(blockSizeStr, 10, 64)
+ if err != nil {
+ blockSize = 4 * humanize.MiByte // default value
+ }
+
+ fileSize, err := strconv.ParseUint(fileSizeStr, 10, 64)
+ if err != nil {
+ fileSize = 1 * humanize.GiByte // default value
+ }
+
+ opts := madmin.DriveSpeedTestOpts{
+ Serial: serial,
+ BlockSize: blockSize,
+ FileSize: fileSize,
+ }
+
+ keepAliveTicker := time.NewTicker(500 * time.Millisecond)
+ defer keepAliveTicker.Stop()
+
+ ch := globalNotificationSys.DriveSpeedTest(ctx, opts)
+
+ enc := json.NewEncoder(w)
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-keepAliveTicker.C:
+ // Write a blank entry to prevent client from disconnecting
+ if err := enc.Encode(madmin.DriveSpeedTestResult{}); err != nil {
+ return
+ }
+ xhttp.Flush(w)
+ case result, ok := <-ch:
+ if !ok {
+ return
+ }
+ if err := enc.Encode(result); err != nil {
+ return
+ }
+ xhttp.Flush(w)
+ }
+ }
+}
+
+// Admin API errors
+const (
+ AdminUpdateUnexpectedFailure = "XMinioAdminUpdateUnexpectedFailure"
+ AdminUpdateURLNotReachable = "XMinioAdminUpdateURLNotReachable"
+ AdminUpdateApplyFailure = "XMinioAdminUpdateApplyFailure"
+)
+
+// Returns true if the madmin.TraceInfo should be traced,
+// false if certain conditions are not met.
+// - input entry is not of the type *madmin.TraceInfo*
+// - errOnly entries are to be traced, not status code 2xx, 3xx.
+// - madmin.TraceInfo type is asked by opts
+func shouldTrace(trcInfo madmin.TraceInfo, opts madmin.ServiceTraceOpts) (shouldTrace bool) {
+ // Reject all unwanted types.
+ want := opts.TraceTypes()
+ if !want.Contains(trcInfo.TraceType) {
+ return false
+ }
+
+ isHTTP := trcInfo.TraceType.Overlaps(madmin.TraceInternal|madmin.TraceS3) && trcInfo.HTTP != nil
+
+ // Check latency...
+ if opts.Threshold > 0 && trcInfo.Duration < opts.Threshold {
+ return false
+ }
+
+ // Check internal path
+ isInternal := isHTTP && HasPrefix(trcInfo.HTTP.ReqInfo.Path, minioReservedBucketPath+SlashSeparator)
+ if isInternal && !opts.Internal {
+ return false
+ }
+
+ // Filter non-errors.
+ if isHTTP && opts.OnlyErrors && trcInfo.HTTP.RespInfo.StatusCode < http.StatusBadRequest {
+ return false
+ }
+
+ return true
+}
+
+func extractTraceOptions(r *http.Request) (opts madmin.ServiceTraceOpts, err error) {
+ if err := opts.ParseParams(r); err != nil {
+ return opts, err
+ }
+ // Support deprecated 'all' query
+ if r.Form.Get("all") == "true" {
+ opts.S3 = true
+ opts.Internal = true
+ opts.Storage = true
+ opts.OS = true
+ // Older mc - cannot deal with more types...
+ }
+ return opts, err
+}
+
+// TraceHandler - POST /minio/admin/v3/trace
+// ----------
+// The handler sends http trace to the connected HTTP client.
+func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ // Validate request signature.
+ _, adminAPIErr := checkAdminRequestAuth(ctx, r, policy.TraceAdminAction, "")
+ if adminAPIErr != ErrNone {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
+ return
+ }
+
+ traceOpts, err := extractTraceOptions(r)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
+ return
+ }
+ setEventStreamHeaders(w)
+
+ // Trace Publisher and peer-trace-client uses nonblocking send and hence does not wait for slow receivers.
+ // Keep 100k buffered channel.
+ // If receiver cannot keep up with that we drop events.
+ traceCh := make(chan []byte, 100000)
+ peers, _ := newPeerRestClients(globalEndpoints)
+ err = globalTrace.SubscribeJSON(traceOpts.TraceTypes(), traceCh, ctx.Done(), func(entry madmin.TraceInfo) bool {
+ return shouldTrace(entry, traceOpts)
+ }, nil)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Publish bootstrap events that have already occurred before client could subscribe.
+ if traceOpts.TraceTypes().Contains(madmin.TraceBootstrap) {
+ go globalBootstrapTracer.Publish(ctx, globalTrace)
+ }
+
+ for _, peer := range peers {
+ if peer == nil {
+ continue
+ }
+ peer.Trace(ctx, traceCh, traceOpts)
+ }
+
+ keepAliveTicker := time.NewTicker(time.Second)
+ defer keepAliveTicker.Stop()
+
+ for {
+ select {
+ case entry := <-traceCh:
+ if _, err := w.Write(entry); err != nil {
+ return
+ }
+ grid.PutByteBuffer(entry)
+ if len(traceCh) == 0 {
+ // Flush if nothing is queued
+ xhttp.Flush(w)
+ }
+ case <-keepAliveTicker.C:
+ if len(traceCh) > 0 {
+ continue
+ }
+ if _, err := w.Write([]byte(" ")); err != nil {
+ return
+ }
+ xhttp.Flush(w)
+ case <-ctx.Done():
+ return
+ }
+ }
+}
+
+// The ConsoleLogHandler handler sends console logs to the connected HTTP client.
+func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.ConsoleLogAdminAction)
+ if objectAPI == nil {
+ return
+ }
+ node := r.Form.Get("node")
+ // limit buffered console entries if client requested it.
+ limitStr := r.Form.Get("limit")
+ limitLines, err := strconv.Atoi(limitStr)
+ if err != nil {
+ limitLines = 10
+ }
+
+ logKind := madmin.LogKind(strings.ToUpper(r.Form.Get("logType"))).LogMask()
+ if logKind == 0 {
+ logKind = madmin.LogMaskAll
+ }
+
+ // Avoid reusing tcp connection if read timeout is hit
+ // This is needed to make r.Context().Done() work as
+ // expected in case of read timeout
+ w.Header().Set("Connection", "close")
+
+ setEventStreamHeaders(w)
+
+ logCh := make(chan log.Info, 1000)
+ peers, _ := newPeerRestClients(globalEndpoints)
+ encodedCh := make(chan []byte, 1000+len(peers)*1000)
+ err = globalConsoleSys.Subscribe(logCh, ctx.Done(), node, limitLines, logKind, nil)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ // Convert local entries to JSON
+ go func() {
+ var buf bytes.Buffer
+ enc := json.NewEncoder(&buf)
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case li := <-logCh:
+ if !li.SendLog(node, logKind) {
+ continue
+ }
+ buf.Reset()
+ if err := enc.Encode(li); err != nil {
+ continue
+ }
+ select {
+ case <-ctx.Done():
+ return
+ case encodedCh <- append(grid.GetByteBuffer()[:0], buf.Bytes()...):
+ }
+ }
+ }
+ }()
+
+ // Collect from matching peers
+ for _, peer := range peers {
+ if peer == nil {
+ continue
+ }
+ if node == "" || strings.EqualFold(peer.host.Name, node) {
+ peer.ConsoleLog(ctx, logKind, encodedCh)
+ }
+ }
+
+ keepAliveTicker := time.NewTicker(500 * time.Millisecond)
+ defer keepAliveTicker.Stop()
+ for {
+ select {
+ case log, ok := <-encodedCh:
+ if !ok {
+ return
+ }
+ _, err = w.Write(log)
+ if err != nil {
+ return
+ }
+ grid.PutByteBuffer(log)
+ if len(logCh) == 0 {
+ // Flush if nothing is queued
+ xhttp.Flush(w)
+ }
+ case <-keepAliveTicker.C:
+ if len(logCh) > 0 {
+ continue
+ }
+ if _, err := w.Write([]byte(" ")); err != nil {
+ return
+ }
+ xhttp.Flush(w)
+ case <-ctx.Done():
+ return
+ }
+ }
+}
+
+// KMSCreateKeyHandler - POST /minio/admin/v3/kms/key/create?key-id=
+func (a adminAPIHandlers) KMSCreateKeyHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSCreateKeyAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ if GlobalKMS == nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
+ return
+ }
+
+ if err := GlobalKMS.CreateKey(ctx, &kms.CreateKeyRequest{
+ Name: r.Form.Get("key-id"),
+ }); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ writeSuccessResponseHeadersOnly(w)
+}
+
+// KMSStatusHandler - GET /minio/admin/v3/kms/status
+func (a adminAPIHandlers) KMSStatusHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSKeyStatusAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ if GlobalKMS == nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
+ return
+ }
+
+ stat, err := GlobalKMS.Status(ctx)
+ if err != nil {
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
+ return
+ }
+ resp, err := json.Marshal(stat)
+ if err != nil {
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
+ return
+ }
+ writeSuccessResponseJSON(w, resp)
+}
+
+// KMSKeyStatusHandler - GET /minio/admin/v3/kms/key/status?key-id=
+func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSKeyStatusAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ if GlobalKMS == nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
+ return
+ }
+
+ keyID := r.Form.Get("key-id")
+ if keyID == "" {
+ keyID = GlobalKMS.DefaultKey
+ }
+ response := madmin.KMSKeyStatus{
+ KeyID: keyID,
+ }
+
+ kmsContext := kms.Context{"MinIO admin API": "KMSKeyStatusHandler"} // Context for a test key operation
+ // 1. Generate a new key using the KMS.
+ key, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{
+ Name: keyID,
+ AssociatedData: kmsContext,
+ })
+ if err != nil {
+ response.EncryptionErr = err.Error()
+ resp, err := json.Marshal(response)
+ if err != nil {
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
+ return
+ }
+ writeSuccessResponseJSON(w, resp)
+ return
+ }
+
+ // 2. Verify that we can indeed decrypt the (encrypted) key
+ decryptedKey, err := GlobalKMS.Decrypt(ctx, &kms.DecryptRequest{
+ Name: key.KeyID,
+ Ciphertext: key.Ciphertext,
+ AssociatedData: kmsContext,
+ })
+ if err != nil {
+ response.DecryptionErr = err.Error()
+ resp, err := json.Marshal(response)
+ if err != nil {
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
+ return
+ }
+ writeSuccessResponseJSON(w, resp)
+ return
+ }
+
+ // 3. Compare generated key with decrypted key
+ if subtle.ConstantTimeCompare(key.Plaintext, decryptedKey) != 1 {
+ response.DecryptionErr = "The generated and the decrypted data key do not match"
+ resp, err := json.Marshal(response)
+ if err != nil {
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
+ return
+ }
+ writeSuccessResponseJSON(w, resp)
+ return
+ }
+
+ resp, err := json.Marshal(response)
+ if err != nil {
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
+ return
+ }
+ writeSuccessResponseJSON(w, resp)
+}
+
+func getPoolsInfo(ctx context.Context, allDisks []madmin.Disk) (map[int]map[int]madmin.ErasureSetInfo, error) {
+ objectAPI := newObjectLayerFn()
+ if objectAPI == nil {
+ return nil, errServerNotInitialized
+ }
+
+ z, ok := objectAPI.(*erasureServerPools)
+ if !ok {
+ return nil, errServerNotInitialized
+ }
+
+ poolsInfo := make(map[int]map[int]madmin.ErasureSetInfo)
+ for _, d := range allDisks {
+ poolInfo, ok := poolsInfo[d.PoolIndex]
+ if !ok {
+ poolInfo = make(map[int]madmin.ErasureSetInfo)
+ }
+ erasureSet, ok := poolInfo[d.SetIndex]
+ if !ok {
+ erasureSet.ID = d.SetIndex
+ cache := dataUsageCache{}
+ if err := cache.load(ctx, z.serverPools[d.PoolIndex].sets[d.SetIndex], dataUsageCacheName); err == nil {
+ dataUsageInfo := cache.dui(dataUsageRoot, nil)
+ erasureSet.ObjectsCount = dataUsageInfo.ObjectsTotalCount
+ erasureSet.VersionsCount = dataUsageInfo.VersionsTotalCount
+ erasureSet.DeleteMarkersCount = dataUsageInfo.DeleteMarkersTotalCount
+ erasureSet.Usage = dataUsageInfo.ObjectsTotalSize
+ }
+ }
+ erasureSet.RawCapacity += d.TotalSpace
+ erasureSet.RawUsage += d.UsedSpace
+ if d.Healing {
+ erasureSet.HealDisks = 1
+ }
+ poolInfo[d.SetIndex] = erasureSet
+ poolsInfo[d.PoolIndex] = poolInfo
+ }
+ return poolsInfo, nil
+}
+
+func getServerInfo(ctx context.Context, pools, metrics bool, r *http.Request) madmin.InfoMessage {
+ const operationTimeout = 10 * time.Second
+ ldap := madmin.LDAP{}
+ if globalIAMSys.LDAPConfig.Enabled() {
+ ldapConn, err := globalIAMSys.LDAPConfig.LDAP.Connect()
+ //nolint:gocritic
+ if err != nil {
+ ldap.Status = string(madmin.ItemOffline)
+ } else if ldapConn == nil {
+ ldap.Status = "Not Configured"
+ } else {
+ // Close ldap connection to avoid leaks.
+ ldapConn.Close()
+ ldap.Status = string(madmin.ItemOnline)
+ }
+ }
+
+ log, audit := fetchLoggerInfo(ctx)
+
+ // Get the notification target info
+ notifyTarget := fetchLambdaInfo()
+
+ local := getLocalServerProperty(globalEndpoints, r, metrics)
+ servers := globalNotificationSys.ServerInfo(ctx, metrics)
+ servers = append(servers, local)
+
+ var poolsInfo map[int]map[int]madmin.ErasureSetInfo
+ var backend madmin.ErasureBackend
+
+ mode := madmin.ItemInitializing
+
+ buckets := madmin.Buckets{}
+ objects := madmin.Objects{}
+ versions := madmin.Versions{}
+ deleteMarkers := madmin.DeleteMarkers{}
+ usage := madmin.Usage{}
+
+ objectAPI := newObjectLayerFn()
+ if objectAPI != nil {
+ mode = madmin.ItemOnline
+
+ // Load data usage
+ ctx2, cancel := context.WithTimeout(ctx, operationTimeout)
+ dataUsageInfo, err := loadDataUsageFromBackend(ctx2, objectAPI)
+ cancel()
+ if err == nil {
+ buckets = madmin.Buckets{Count: dataUsageInfo.BucketsCount}
+ objects = madmin.Objects{Count: dataUsageInfo.ObjectsTotalCount}
+ versions = madmin.Versions{Count: dataUsageInfo.VersionsTotalCount}
+ deleteMarkers = madmin.DeleteMarkers{Count: dataUsageInfo.DeleteMarkersTotalCount}
+ usage = madmin.Usage{Size: dataUsageInfo.ObjectsTotalSize}
+ } else {
+ buckets = madmin.Buckets{Error: err.Error()}
+ objects = madmin.Objects{Error: err.Error()}
+ deleteMarkers = madmin.DeleteMarkers{Error: err.Error()}
+ usage = madmin.Usage{Error: err.Error()}
+ }
+
+ // Fetching the backend information
+ backendInfo := objectAPI.BackendInfo()
+ // Calculate the number of online/offline disks of all nodes
+ var allDisks []madmin.Disk
+ for _, s := range servers {
+ allDisks = append(allDisks, s.Disks...)
+ }
+ onlineDisks, offlineDisks := getOnlineOfflineDisksStats(allDisks)
+
+ backend = madmin.ErasureBackend{
+ Type: madmin.ErasureType,
+ OnlineDisks: onlineDisks.Sum(),
+ OfflineDisks: offlineDisks.Sum(),
+ StandardSCParity: backendInfo.StandardSCParity,
+ RRSCParity: backendInfo.RRSCParity,
+ TotalSets: backendInfo.TotalSets,
+ DrivesPerSet: backendInfo.DrivesPerSet,
+ }
+
+ if pools {
+ ctx2, cancel := context.WithTimeout(ctx, operationTimeout)
+ poolsInfo, _ = getPoolsInfo(ctx2, allDisks)
+ cancel()
+ }
+ }
+
+ domain := globalDomainNames
+ services := madmin.Services{
+ LDAP: ldap,
+ Logger: log,
+ Audit: audit,
+ Notifications: notifyTarget,
+ }
+ {
+ ctx2, cancel := context.WithTimeout(ctx, operationTimeout)
+ services.KMSStatus = fetchKMSStatus(ctx2)
+ cancel()
+ }
+
+ return madmin.InfoMessage{
+ Mode: string(mode),
+ Domain: domain,
+ Region: globalSite.Region(),
+ SQSARN: globalEventNotifier.GetARNList(),
+ DeploymentID: globalDeploymentID(),
+ Buckets: buckets,
+ Objects: objects,
+ Versions: versions,
+ DeleteMarkers: deleteMarkers,
+ Usage: usage,
+ Services: services,
+ Backend: backend,
+ Servers: servers,
+ Pools: poolsInfo,
+ }
+}
+
+func getKubernetesInfo(dctx context.Context) madmin.KubernetesInfo {
+ ctx, cancel := context.WithCancel(dctx)
+ defer cancel()
+
+ ki := madmin.KubernetesInfo{}
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, kubernetesVersionEndpoint, nil)
+ if err != nil {
+ ki.Error = err.Error()
+ return ki
+ }
+
+ client := &http.Client{
+ Transport: globalRemoteTargetTransport,
+ Timeout: 10 * time.Second,
+ }
+
+ resp, err := client.Do(req)
+ if err != nil {
+ ki.Error = err.Error()
+ return ki
+ }
+ defer resp.Body.Close()
+ decoder := json.NewDecoder(resp.Body)
+ if err := decoder.Decode(&ki); err != nil {
+ ki.Error = err.Error()
+ }
+ return ki
+}
+
+func fetchHealthInfo(healthCtx context.Context, objectAPI ObjectLayer, query *url.Values, healthInfoCh chan madmin.HealthInfo, healthInfo madmin.HealthInfo) {
+ hostAnonymizer := createHostAnonymizer()
+
+ anonParam := query.Get(anonymizeParam)
+ // anonAddr - Anonymizes hosts in given input string
+ // (only if the anonymize param is set to srict).
+ anonAddr := func(addr string) string {
+ if anonParam != anonymizeStrict {
+ return addr
+ }
+ newAddr, found := hostAnonymizer[addr]
+ if found {
+ return newAddr
+ }
+
+ // If we reach here, it means that the given addr doesn't contain any of the hosts.
+ // Return it as is. Can happen for drive paths in non-distributed mode
+ return addr
+ }
+
+ // anonymizedAddr - Updated the addr of the node info with anonymized one
+ anonymizeAddr := func(info madmin.NodeInfo) {
+ info.SetAddr(anonAddr(info.GetAddr()))
+ }
+
+ partialWrite := func(oinfo madmin.HealthInfo) {
+ select {
+ case healthInfoCh <- oinfo:
+ case <-healthCtx.Done():
+ }
+ }
+
+ getAndWritePlatformInfo := func() {
+ if IsKubernetes() {
+ healthInfo.Sys.KubernetesInfo = getKubernetesInfo(healthCtx)
+ partialWrite(healthInfo)
+ }
+ }
+
+ getAndWriteCPUs := func() {
+ if query.Get("syscpu") == "true" {
+ localCPUInfo := madmin.GetCPUs(healthCtx, globalLocalNodeName)
+ anonymizeAddr(&localCPUInfo)
+ healthInfo.Sys.CPUInfo = append(healthInfo.Sys.CPUInfo, localCPUInfo)
+
+ peerCPUInfo := globalNotificationSys.GetCPUs(healthCtx)
+ for _, cpuInfo := range peerCPUInfo {
+ anonymizeAddr(&cpuInfo)
+ healthInfo.Sys.CPUInfo = append(healthInfo.Sys.CPUInfo, cpuInfo)
+ }
+
+ partialWrite(healthInfo)
+ }
+ }
+
+ getAndWritePartitions := func() {
+ if query.Get("sysdrivehw") == "true" {
+ localPartitions := madmin.GetPartitions(healthCtx, globalLocalNodeName)
+ anonymizeAddr(&localPartitions)
+ healthInfo.Sys.Partitions = append(healthInfo.Sys.Partitions, localPartitions)
+
+ peerPartitions := globalNotificationSys.GetPartitions(healthCtx)
+ for _, p := range peerPartitions {
+ anonymizeAddr(&p)
+ healthInfo.Sys.Partitions = append(healthInfo.Sys.Partitions, p)
+ }
+ partialWrite(healthInfo)
+ }
+ }
+
+ getAndWriteNetInfo := func() {
+ if query.Get(string(madmin.HealthDataTypeSysNet)) == "true" {
+ localNetInfo := madmin.GetNetInfo(globalLocalNodeName, globalInternodeInterface)
+ healthInfo.Sys.NetInfo = append(healthInfo.Sys.NetInfo, localNetInfo)
+
+ peerNetInfos := globalNotificationSys.GetNetInfo(healthCtx)
+ for _, n := range peerNetInfos {
+ anonymizeAddr(&n)
+ healthInfo.Sys.NetInfo = append(healthInfo.Sys.NetInfo, n)
+ }
+ partialWrite(healthInfo)
+ }
+ }
+
+ getAndWriteOSInfo := func() {
+ if query.Get("sysosinfo") == "true" {
+ localOSInfo := madmin.GetOSInfo(healthCtx, globalLocalNodeName)
+ anonymizeAddr(&localOSInfo)
+ healthInfo.Sys.OSInfo = append(healthInfo.Sys.OSInfo, localOSInfo)
+
+ peerOSInfos := globalNotificationSys.GetOSInfo(healthCtx)
+ for _, o := range peerOSInfos {
+ anonymizeAddr(&o)
+ healthInfo.Sys.OSInfo = append(healthInfo.Sys.OSInfo, o)
+ }
+ partialWrite(healthInfo)
+ }
+ }
+
+ getAndWriteMemInfo := func() {
+ if query.Get("sysmem") == "true" {
+ localMemInfo := madmin.GetMemInfo(healthCtx, globalLocalNodeName)
+ anonymizeAddr(&localMemInfo)
+ healthInfo.Sys.MemInfo = append(healthInfo.Sys.MemInfo, localMemInfo)
+
+ peerMemInfos := globalNotificationSys.GetMemInfo(healthCtx)
+ for _, m := range peerMemInfos {
+ anonymizeAddr(&m)
+ healthInfo.Sys.MemInfo = append(healthInfo.Sys.MemInfo, m)
+ }
+ partialWrite(healthInfo)
+ }
+ }
+
+ getAndWriteSysErrors := func() {
+ if query.Get(string(madmin.HealthDataTypeSysErrors)) == "true" {
+ localSysErrors := madmin.GetSysErrors(healthCtx, globalLocalNodeName)
+ anonymizeAddr(&localSysErrors)
+ healthInfo.Sys.SysErrs = append(healthInfo.Sys.SysErrs, localSysErrors)
+ partialWrite(healthInfo)
+
+ peerSysErrs := globalNotificationSys.GetSysErrors(healthCtx)
+ for _, se := range peerSysErrs {
+ anonymizeAddr(&se)
+ healthInfo.Sys.SysErrs = append(healthInfo.Sys.SysErrs, se)
+ }
+ partialWrite(healthInfo)
+ }
+ }
+
+ getAndWriteSysConfig := func() {
+ if query.Get(string(madmin.HealthDataTypeSysConfig)) == "true" {
+ localSysConfig := madmin.GetSysConfig(healthCtx, globalLocalNodeName)
+ anonymizeAddr(&localSysConfig)
+ healthInfo.Sys.SysConfig = append(healthInfo.Sys.SysConfig, localSysConfig)
+ partialWrite(healthInfo)
+
+ peerSysConfig := globalNotificationSys.GetSysConfig(healthCtx)
+ for _, sc := range peerSysConfig {
+ anonymizeAddr(&sc)
+ healthInfo.Sys.SysConfig = append(healthInfo.Sys.SysConfig, sc)
+ }
+ partialWrite(healthInfo)
+ }
+ }
+
+ getAndWriteSysServices := func() {
+ if query.Get(string(madmin.HealthDataTypeSysServices)) == "true" {
+ localSysServices := madmin.GetSysServices(healthCtx, globalLocalNodeName)
+ anonymizeAddr(&localSysServices)
+ healthInfo.Sys.SysServices = append(healthInfo.Sys.SysServices, localSysServices)
+ partialWrite(healthInfo)
+
+ peerSysServices := globalNotificationSys.GetSysServices(healthCtx)
+ for _, ss := range peerSysServices {
+ anonymizeAddr(&ss)
+ healthInfo.Sys.SysServices = append(healthInfo.Sys.SysServices, ss)
+ }
+ partialWrite(healthInfo)
+ }
+ }
+
+ // collect all realtime metrics except disk
+ // disk metrics are already included under drive info of each server
+ getRealtimeMetrics := func() *madmin.RealtimeMetrics {
+ var m madmin.RealtimeMetrics
+ types := madmin.MetricsAll &^ madmin.MetricsDisk
+ mLocal := collectLocalMetrics(types, collectMetricsOpts{})
+ m.Merge(&mLocal)
+ cctx, cancel := context.WithTimeout(healthCtx, time.Second/2)
+ mRemote := collectRemoteMetrics(cctx, types, collectMetricsOpts{})
+ cancel()
+ m.Merge(&mRemote)
+ for idx, host := range m.Hosts {
+ m.Hosts[idx] = anonAddr(host)
+ }
+ for host, metrics := range m.ByHost {
+ m.ByHost[anonAddr(host)] = metrics
+ delete(m.ByHost, host)
+ }
+ return &m
+ }
+
+ anonymizeCmdLine := func(cmdLine string) string {
+ if anonParam != anonymizeStrict {
+ return cmdLine
+ }
+
+ if !globalIsDistErasure {
+ // FS mode - single server - hard code to `server1`
+ anonCmdLine := strings.ReplaceAll(cmdLine, globalLocalNodeName, "server1")
+ if len(globalMinioConsoleHost) > 0 {
+ anonCmdLine = strings.ReplaceAll(anonCmdLine, globalMinioConsoleHost, "server1")
+ }
+ return anonCmdLine
+ }
+
+ // Server start command regex groups:
+ // 1 - minio server
+ // 2 - flags e.g. `--address :9000 --certs-dir /etc/minio/certs`
+ // 3 - pool args e.g. `https://node{01...16}.domain/data/disk{001...204} https://node{17...32}.domain/data/disk{001...204}`
+ re := regexp.MustCompile(`^(.*minio\s+server\s+)(--[^\s]+\s+[^\s]+\s+)*(.*)`)
+
+ // stays unchanged in the anonymized version
+ cmdLineWithoutPools := re.ReplaceAllString(cmdLine, `$1$2`)
+
+ // to be anonymized
+ poolsArgs := re.ReplaceAllString(cmdLine, `$3`)
+ var anonPools []string
+
+ if !strings.Contains(poolsArgs, "{") || !strings.Contains(poolsArgs, "}") {
+ // No ellipses pattern. Anonymize host name from every pool arg
+ pools := strings.Fields(poolsArgs)
+ anonPools = make([]string, len(pools))
+ for index, arg := range pools {
+ anonPools[index] = anonAddr(arg)
+ }
+ return cmdLineWithoutPools + strings.Join(anonPools, " ")
+ }
+
+ // Ellipses pattern in pool args. Regex groups:
+ // 1 - server prefix
+ // 2 - number sequence for servers
+ // 3 - server suffix
+ // 4 - drive prefix (starting with /)
+ // 5 - number sequence for drives
+ // 6 - drive suffix
+ re = regexp.MustCompile(`([^\s^{]*)({\d+...\d+})?([^\s^{^/]*)(/[^\s^{]*)({\d+...\d+})?([^\s]*)`)
+ poolsMatches := re.FindAllStringSubmatch(poolsArgs, -1)
+
+ anonPools = make([]string, len(poolsMatches))
+ idxMap := map[int]string{
+ 1: "spfx",
+ 3: "ssfx",
+ }
+ for pi, poolsMatch := range poolsMatches {
+ // Replace the server prefix/suffix with anonymized ones
+ for idx, lbl := range idxMap {
+ if len(poolsMatch[idx]) > 0 {
+ poolsMatch[idx] = fmt.Sprintf("%s%d", lbl, crc32.ChecksumIEEE([]byte(poolsMatch[idx])))
+ }
+ }
+
+ // Remove the original pools args present at index 0
+ anonPools[pi] = strings.Join(poolsMatch[1:], "")
+ }
+ return cmdLineWithoutPools + strings.Join(anonPools, " ")
+ }
+
+ anonymizeProcInfo := func(p *madmin.ProcInfo) {
+ p.CmdLine = anonymizeCmdLine(p.CmdLine)
+ anonymizeAddr(p)
+ }
+
+ getAndWriteProcInfo := func() {
+ if query.Get("sysprocess") == "true" {
+ localProcInfo := madmin.GetProcInfo(healthCtx, globalLocalNodeName)
+ anonymizeProcInfo(&localProcInfo)
+ healthInfo.Sys.ProcInfo = append(healthInfo.Sys.ProcInfo, localProcInfo)
+ peerProcInfos := globalNotificationSys.GetProcInfo(healthCtx)
+ for _, p := range peerProcInfos {
+ anonymizeProcInfo(&p)
+ healthInfo.Sys.ProcInfo = append(healthInfo.Sys.ProcInfo, p)
+ }
+ partialWrite(healthInfo)
+ }
+ }
+
+ getAndWriteMinioConfig := func() {
+ if query.Get("minioconfig") == "true" {
+ config, err := readServerConfig(healthCtx, objectAPI, nil)
+ if err != nil {
+ healthInfo.Minio.Config = madmin.MinioConfig{
+ Error: err.Error(),
+ }
+ } else {
+ healthInfo.Minio.Config = madmin.MinioConfig{
+ Config: config.RedactSensitiveInfo(),
+ }
+ }
+ partialWrite(healthInfo)
+ }
+ }
+
+ anonymizeNetwork := func(network map[string]string) map[string]string {
+ anonNetwork := map[string]string{}
+ for endpoint, status := range network {
+ anonEndpoint := anonAddr(endpoint)
+ anonNetwork[anonEndpoint] = status
+ }
+ return anonNetwork
+ }
+
+ anonymizeDrives := func(drives []madmin.Disk) []madmin.Disk {
+ anonDrives := []madmin.Disk{}
+ for _, drive := range drives {
+ drive.Endpoint = anonAddr(drive.Endpoint)
+ anonDrives = append(anonDrives, drive)
+ }
+ return anonDrives
+ }
+
+ go func() {
+ defer xioutil.SafeClose(healthInfoCh)
+
+ partialWrite(healthInfo) // Write first message with only version and deployment id populated
+ getAndWritePlatformInfo()
+ getAndWriteCPUs()
+ getAndWritePartitions()
+ getAndWriteNetInfo()
+ getAndWriteOSInfo()
+ getAndWriteMemInfo()
+ getAndWriteProcInfo()
+ getAndWriteMinioConfig()
+ getAndWriteSysErrors()
+ getAndWriteSysServices()
+ getAndWriteSysConfig()
+
+ if query.Get("minioinfo") == "true" {
+ infoMessage := getServerInfo(healthCtx, false, true, nil)
+ servers := make([]madmin.ServerInfo, 0, len(infoMessage.Servers))
+ for _, server := range infoMessage.Servers {
+ anonEndpoint := anonAddr(server.Endpoint)
+ servers = append(servers, madmin.ServerInfo{
+ State: server.State,
+ Endpoint: anonEndpoint,
+ Uptime: server.Uptime,
+ Version: server.Version,
+ CommitID: server.CommitID,
+ Network: anonymizeNetwork(server.Network),
+ Drives: anonymizeDrives(server.Disks),
+ PoolNumber: func() int {
+ if len(server.PoolNumbers) == 1 {
+ return server.PoolNumbers[0]
+ }
+ return math.MaxInt // this indicates that its unset.
+ }(),
+ PoolNumbers: server.PoolNumbers,
+ MemStats: madmin.MemStats{
+ Alloc: server.MemStats.Alloc,
+ TotalAlloc: server.MemStats.TotalAlloc,
+ Mallocs: server.MemStats.Mallocs,
+ Frees: server.MemStats.Frees,
+ HeapAlloc: server.MemStats.HeapAlloc,
+ },
+ GoMaxProcs: server.GoMaxProcs,
+ NumCPU: server.NumCPU,
+ RuntimeVersion: server.RuntimeVersion,
+ GCStats: server.GCStats,
+ MinioEnvVars: server.MinioEnvVars,
+ })
+ }
+
+ tls := getTLSInfo()
+ isK8s := IsKubernetes()
+ isDocker := IsDocker()
+ healthInfo.Minio.Info = madmin.MinioInfo{
+ Mode: infoMessage.Mode,
+ Domain: infoMessage.Domain,
+ Region: infoMessage.Region,
+ SQSARN: infoMessage.SQSARN,
+ DeploymentID: infoMessage.DeploymentID,
+ Buckets: infoMessage.Buckets,
+ Objects: infoMessage.Objects,
+ Usage: infoMessage.Usage,
+ Services: infoMessage.Services,
+ Backend: infoMessage.Backend,
+ Servers: servers,
+ TLS: &tls,
+ IsKubernetes: &isK8s,
+ IsDocker: &isDocker,
+ Metrics: getRealtimeMetrics(),
+ }
+ partialWrite(healthInfo)
+ }
+ }()
+}
+
+// HealthInfoHandler - GET /minio/admin/v3/healthinfo
+// ----------
+// Get server health info
+func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.HealthInfoAdminAction)
+ if objectAPI == nil {
+ return
+ }
+
+ query := r.Form
+ healthInfoCh := make(chan madmin.HealthInfo)
+ enc := json.NewEncoder(w)
+
+ healthInfo := madmin.HealthInfo{
+ TimeStamp: time.Now().UTC(),
+ Version: madmin.HealthInfoVersion,
+ Minio: madmin.MinioHealthInfo{
+ Info: madmin.MinioInfo{
+ DeploymentID: globalDeploymentID(),
+ },
+ },
+ }
+
+ errResp := func(err error) {
+ errorResponse := getAPIErrorResponse(ctx, toAdminAPIErr(ctx, err), r.URL.String(),
+ w.Header().Get(xhttp.AmzRequestID), w.Header().Get(xhttp.AmzRequestHostID))
+ encodedErrorResponse := encodeResponse(errorResponse)
+ healthInfo.Error = string(encodedErrorResponse)
+ adminLogIf(ctx, enc.Encode(healthInfo))
+ }
+
+ deadline := 10 * time.Second // Default deadline is 10secs for health diagnostics.
+ if dstr := query.Get("deadline"); dstr != "" {
+ var err error
+ deadline, err = time.ParseDuration(dstr)
+ if err != nil {
+ errResp(err)
+ return
+ }
+ }
+
+ nsLock := objectAPI.NewNSLock(minioMetaBucket, "health-check-in-progress")
+ lkctx, err := nsLock.GetLock(ctx, newDynamicTimeout(deadline, deadline))
+ if err != nil { // returns a locked lock
+ errResp(err)
+ return
+ }
+
+ defer nsLock.Unlock(lkctx)
+ healthCtx, healthCancel := context.WithTimeout(lkctx.Context(), deadline)
+ defer healthCancel()
+
+ go fetchHealthInfo(healthCtx, objectAPI, &query, healthInfoCh, healthInfo)
+
+ setCommonHeaders(w)
+ setEventStreamHeaders(w)
+ w.WriteHeader(http.StatusOK)
+
+ ticker := time.NewTicker(5 * time.Second)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case oinfo, ok := <-healthInfoCh:
+ if !ok {
+ return
+ }
+ if err := enc.Encode(oinfo); err != nil {
+ return
+ }
+ if len(healthInfoCh) == 0 {
+ // Flush if nothing is queued
+ xhttp.Flush(w)
+ }
+ case <-ticker.C:
+ if _, err := w.Write([]byte(" ")); err != nil {
+ return
+ }
+ xhttp.Flush(w)
+ case <-healthCtx.Done():
+ return
+ }
+ }
+}
+
+func getTLSInfo() madmin.TLSInfo {
+ tlsInfo := madmin.TLSInfo{
+ TLSEnabled: globalIsTLS,
+ Certs: []madmin.TLSCert{},
+ }
+
+ if globalIsTLS {
+ for _, c := range globalPublicCerts {
+ check := xxh3.Hash(c.RawIssuer)
+ check ^= xxh3.Hash(c.RawSubjectPublicKeyInfo)
+ // We XOR, so order doesn't matter.
+ for _, v := range c.DNSNames {
+ check ^= xxh3.HashString(v)
+ }
+ for _, v := range c.EmailAddresses {
+ check ^= xxh3.HashString(v)
+ }
+ for _, v := range c.IPAddresses {
+ check ^= xxh3.HashString(v.String())
+ }
+ for _, v := range c.URIs {
+ check ^= xxh3.HashString(v.String())
+ }
+ tlsInfo.Certs = append(tlsInfo.Certs, madmin.TLSCert{
+ PubKeyAlgo: c.PublicKeyAlgorithm.String(),
+ SignatureAlgo: c.SignatureAlgorithm.String(),
+ NotBefore: c.NotBefore,
+ NotAfter: c.NotAfter,
+ Checksum: strconv.FormatUint(check, 16),
+ })
+ }
+ }
+ return tlsInfo
+}
+
+// ServerInfoHandler - GET /minio/admin/v3/info
+// ----------
+// Get server information
+func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ // Validate request signature.
+ _, adminAPIErr := checkAdminRequestAuth(ctx, r, policy.ServerInfoAdminAction, "")
+ if adminAPIErr != ErrNone {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
+ return
+ }
+
+ metrics := r.Form.Get(peerRESTMetrics) == "true"
+
+ // Marshal API response
+ jsonBytes, err := json.Marshal(getServerInfo(ctx, true, metrics, r))
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Reply with storage information (across nodes in a
+ // distributed setup) as json.
+ writeSuccessResponseJSON(w, jsonBytes)
+}
+
+func fetchLambdaInfo() []map[string][]madmin.TargetIDStatus {
+ lambdaMap := make(map[string][]madmin.TargetIDStatus)
+
+ for _, tgt := range globalEventNotifier.Targets() {
+ targetIDStatus := make(map[string]madmin.Status)
+ active, _ := tgt.IsActive()
+ targetID := tgt.ID()
+ if active {
+ targetIDStatus[targetID.ID] = madmin.Status{Status: string(madmin.ItemOnline)}
+ } else {
+ targetIDStatus[targetID.ID] = madmin.Status{Status: string(madmin.ItemOffline)}
+ }
+ list := lambdaMap[targetID.Name]
+ list = append(list, targetIDStatus)
+ lambdaMap[targetID.Name] = list
+ }
+
+ notify := make([]map[string][]madmin.TargetIDStatus, len(lambdaMap))
+ counter := 0
+ for key, value := range lambdaMap {
+ v := make(map[string][]madmin.TargetIDStatus)
+ v[key] = value
+ notify[counter] = v
+ counter++
+ }
+ return notify
+}
+
+// fetchKMSStatus fetches KMS-related status information for all instances
+func fetchKMSStatus(ctx context.Context) []madmin.KMS {
+ if GlobalKMS == nil {
+ return []madmin.KMS{}
+ }
+
+ stat, err := GlobalKMS.Status(ctx)
+ if err != nil {
+ kmsLogIf(ctx, err, "failed to fetch KMS status information")
+ return []madmin.KMS{}
+ }
+
+ stats := make([]madmin.KMS, 0, len(stat.Endpoints))
+ for endpoint, state := range stat.Endpoints {
+ stats = append(stats, madmin.KMS{
+ Status: string(state),
+ Endpoint: endpoint,
+ })
+ }
+ return stats
+}
+
+func targetStatus(ctx context.Context, h logger.Target) madmin.Status {
+ if h.IsOnline(ctx) {
+ return madmin.Status{Status: string(madmin.ItemOnline)}
+ }
+ return madmin.Status{Status: string(madmin.ItemOffline)}
+}
+
+// fetchLoggerInfo return log info
+func fetchLoggerInfo(ctx context.Context) ([]madmin.Logger, []madmin.Audit) {
+ var loggerInfo []madmin.Logger
+ var auditloggerInfo []madmin.Audit
+ for _, tgt := range logger.SystemTargets() {
+ if tgt.Endpoint() != "" {
+ loggerInfo = append(loggerInfo, madmin.Logger{tgt.String(): targetStatus(ctx, tgt)})
+ }
+ }
+
+ for _, tgt := range logger.AuditTargets() {
+ if tgt.Endpoint() != "" {
+ auditloggerInfo = append(auditloggerInfo, madmin.Audit{tgt.String(): targetStatus(ctx, tgt)})
+ }
+ }
+
+ return loggerInfo, auditloggerInfo
+}
+
+func embedFileInZip(zipWriter *zip.Writer, name string, data []byte, fileMode os.FileMode) error {
+ // Send profiling data to zip as file
+ header, zerr := zip.FileInfoHeader(dummyFileInfo{
+ name: name,
+ size: int64(len(data)),
+ mode: fileMode,
+ modTime: UTCNow(),
+ isDir: false,
+ sys: nil,
+ })
+ if zerr != nil {
+ return zerr
+ }
+ header.Method = zip.Deflate
+ zwriter, zerr := zipWriter.CreateHeader(header)
+ if zerr != nil {
+ return zerr
+ }
+ _, err := io.Copy(zwriter, bytes.NewReader(data))
+ return err
+}
+
+// getClusterMetaInfo gets information of the current cluster and
+// returns it.
+// This is not a critical function, and it is allowed
+// to fail with a ten seconds timeout, returning nil.
+func getClusterMetaInfo(ctx context.Context) []byte {
+ objectAPI := newObjectLayerFn()
+ if objectAPI == nil {
+ return nil
+ }
+
+ // Add a ten seconds timeout because getting profiling data
+ // is critical for debugging, in contrary to getting cluster info
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+
+ resultCh := make(chan madmin.ClusterRegistrationInfo)
+
+ go func() {
+ defer xioutil.SafeClose(resultCh)
+
+ ci := madmin.ClusterRegistrationInfo{}
+ ci.Info.NoOfServerPools = len(globalEndpoints)
+ ci.Info.NoOfServers = totalNodeCount()
+ ci.Info.MinioVersion = Version
+
+ si := objectAPI.StorageInfo(ctx, false)
+
+ ci.Info.NoOfDrives = len(si.Disks)
+ for _, disk := range si.Disks {
+ ci.Info.TotalDriveSpace += disk.TotalSpace
+ ci.Info.UsedDriveSpace += disk.UsedSpace
+ }
+
+ dataUsageInfo, _ := loadDataUsageFromBackend(ctx, objectAPI)
+
+ ci.UsedCapacity = dataUsageInfo.ObjectsTotalSize
+ ci.Info.NoOfBuckets = dataUsageInfo.BucketsCount
+ ci.Info.NoOfObjects = dataUsageInfo.ObjectsTotalCount
+
+ ci.DeploymentID = globalDeploymentID()
+ ci.ClusterName = fmt.Sprintf("%d-servers-%d-disks-%s", ci.Info.NoOfServers, ci.Info.NoOfDrives, ci.Info.MinioVersion)
+
+ select {
+ case resultCh <- ci:
+ case <-ctx.Done():
+ return
+ }
+ }()
+
+ select {
+ case <-ctx.Done():
+ return nil
+ case ci := <-resultCh:
+ out, err := json.MarshalIndent(ci, "", " ")
+ if err != nil {
+ bugLogIf(ctx, err)
+ return nil
+ }
+ return out
+ }
+}
+
+func bytesToPublicKey(pub []byte) (*rsa.PublicKey, error) {
+ block, _ := pem.Decode(pub)
+ if block != nil {
+ pub = block.Bytes
+ }
+ key, err := x509.ParsePKCS1PublicKey(pub)
+ if err != nil {
+ return nil, err
+ }
+ return key, nil
+}
+
+// getRawDataer provides an interface for getting raw FS files.
+type getRawDataer interface {
+ GetRawData(ctx context.Context, volume, file string, fn func(r io.Reader, host string, disk string, filename string, info StatInfo) error) error
+}
+
+// InspectDataHandler - GET /minio/admin/v3/inspect-data
+// ----------
+// Download file from all nodes in a zip format
+func (a adminAPIHandlers) InspectDataHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ // Validate request signature.
+ _, adminAPIErr := checkAdminRequestAuth(ctx, r, policy.InspectDataAction, "")
+ if adminAPIErr != ErrNone {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
+ return
+ }
+
+ objLayer := newObjectLayerFn()
+ o, ok := objLayer.(getRawDataer)
+ if !ok {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+ return
+ }
+
+ if err := parseForm(r); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ volume := r.Form.Get("volume")
+ if len(volume) == 0 {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidBucketName), r.URL)
+ return
+ }
+ file := r.Form.Get("file")
+ if len(file) == 0 {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
+ return
+ }
+
+ file = filepath.ToSlash(file)
+ // Reject attempts to traverse parent or absolute paths.
+ if hasBadPathComponent(volume) || hasBadPathComponent(file) {
+ writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInvalidResourceName), r.URL)
+ return
+ }
+
+ var publicKey *rsa.PublicKey
+
+ publicKeyB64 := r.Form.Get("public-key")
+ if publicKeyB64 != "" {
+ publicKeyBytes, err := base64.StdEncoding.DecodeString(publicKeyB64)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ publicKey, err = bytesToPublicKey(publicKeyBytes)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ }
+ addErr := func(msg string) {}
+
+ // Write a version for making *incompatible* changes.
+ // The AdminClient will reject any version it does not know.
+ var inspectZipW *zip.Writer
+ if publicKey != nil {
+ w.WriteHeader(200)
+ stream := estream.NewWriter(w)
+ defer stream.Close()
+
+ clusterKey, err := bytesToPublicKey(getSubnetAdminPublicKey())
+ if err != nil {
+ bugLogIf(ctx, stream.AddError(err.Error()))
+ return
+ }
+ err = stream.AddKeyEncrypted(clusterKey)
+ if err != nil {
+ bugLogIf(ctx, stream.AddError(err.Error()))
+ return
+ }
+ if b := getClusterMetaInfo(ctx); len(b) > 0 {
+ w, err := stream.AddEncryptedStream("cluster.info", nil)
+ if err != nil {
+ bugLogIf(ctx, err)
+ return
+ }
+ w.Write(b)
+ w.Close()
+ }
+
+ // Add new key for inspect data.
+ if err := stream.AddKeyEncrypted(publicKey); err != nil {
+ bugLogIf(ctx, stream.AddError(err.Error()))
+ return
+ }
+ encStream, err := stream.AddEncryptedStream("inspect.zip", nil)
+ if err != nil {
+ bugLogIf(ctx, stream.AddError(err.Error()))
+ return
+ }
+ addErr = func(msg string) {
+ inspectZipW.Close()
+ encStream.Close()
+ stream.AddError(msg)
+ }
+ defer encStream.Close()
+
+ inspectZipW = zip.NewWriter(encStream)
+ defer inspectZipW.Close()
+ } else {
+ // Legacy: Remove if we stop supporting inspection without public key.
+ var key [32]byte
+ // MUST use crypto/rand
+ n, err := crand.Read(key[:])
+ if err != nil || n != len(key) {
+ bugLogIf(ctx, err)
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Write a version for making *incompatible* changes.
+ // The AdminClient will reject any version it does not know.
+ if publicKey == nil {
+ w.Write([]byte{1})
+ w.Write(key[:])
+ }
+
+ stream, err := sio.AES_256_GCM.Stream(key[:])
+ if err != nil {
+ bugLogIf(ctx, err)
+ return
+ }
+ // Zero nonce, we only use each key once, and 32 bytes is plenty.
+ nonce := make([]byte, stream.NonceSize())
+ encw := stream.EncryptWriter(w, nonce, nil)
+ defer encw.Close()
+
+ // Initialize a zip writer which will provide a zipped content
+ // of profiling data of all nodes
+ inspectZipW = zip.NewWriter(encw)
+ defer inspectZipW.Close()
+
+ if b := getClusterMetaInfo(ctx); len(b) > 0 {
+ adminLogIf(ctx, embedFileInZip(inspectZipW, "cluster.info", b, 0o600))
+ }
+ }
+
+ rawDataFn := func(r io.Reader, host, disk, filename string, si StatInfo) error {
+ // Prefix host+disk
+ filename = path.Join(host, disk, filename)
+ if si.Dir {
+ filename += "/"
+ si.Size = 0
+ }
+ if si.Mode == 0 {
+ // Not, set it to default.
+ si.Mode = 0o600
+ }
+ if si.ModTime.IsZero() {
+ // Set time to now.
+ si.ModTime = time.Now()
+ }
+ header, zerr := zip.FileInfoHeader(dummyFileInfo{
+ name: filename,
+ size: si.Size,
+ mode: os.FileMode(si.Mode),
+ modTime: si.ModTime,
+ isDir: si.Dir,
+ sys: nil,
+ })
+ if zerr != nil {
+ bugLogIf(ctx, zerr)
+ return nil
+ }
+ header.Method = zip.Deflate
+ zwriter, zerr := inspectZipW.CreateHeader(header)
+ if zerr != nil {
+ bugLogIf(ctx, zerr)
+ return nil
+ }
+ if _, err := io.Copy(zwriter, r); err != nil {
+ adminLogIf(ctx, err)
+ }
+ return nil
+ }
+
+ // save args passed to inspect command
+ var sb bytes.Buffer
+ fmt.Fprintf(&sb, "Inspect path: %s%s%s\n", volume, slashSeparator, file)
+ sb.WriteString("Server command line args:")
+ for _, pool := range globalEndpoints {
+ sb.WriteString(" ")
+ sb.WriteString(pool.CmdLine)
+ }
+ sb.WriteString("\n")
+ adminLogIf(ctx, embedFileInZip(inspectZipW, "inspect-input.txt", sb.Bytes(), 0o600))
+
+ err := o.GetRawData(ctx, volume, file, rawDataFn)
+ if err != nil {
+ if errors.Is(err, errFileNotFound) {
+ addErr("GetRawData: No files matched the given pattern")
+ return
+ }
+ embedFileInZip(inspectZipW, "GetRawData-err.txt", []byte(err.Error()), 0o600)
+ adminLogIf(ctx, err)
+ }
+
+ // save the format.json as part of inspect by default
+ if volume != minioMetaBucket || file != formatConfigFile {
+ err = o.GetRawData(ctx, minioMetaBucket, formatConfigFile, rawDataFn)
+ }
+ if !errors.Is(err, errFileNotFound) {
+ adminLogIf(ctx, err)
+ }
+
+ scheme := "https"
+ if !globalIsTLS {
+ scheme = "http"
+ }
+
+ // save MinIO start script to inspect command
+ var scrb bytes.Buffer
+ fmt.Fprintf(&scrb, `#!/usr/bin/env bash
+
+function main() {
+ for file in $(ls -1); do
+ dest_file=$(echo "$file" | cut -d ":" -f1)
+ mv "$file" "$dest_file"
+ done
+
+ # Read content of inspect-input.txt
+ MINIO_OPTS=$(grep "Server command line args" <./inspect-input.txt | sed "s/Server command line args: //g" | sed -r "s#%s:\/\/#\.\/#g")
+
+ # Start MinIO instance using the options
+ START_CMD="CI=on _MINIO_AUTO_DRIVE_HEALING=off minio server ${MINIO_OPTS} &"
+ echo
+ echo "Starting MinIO instance: ${START_CMD}"
+ echo
+ eval "$START_CMD"
+ MINIO_SRVR_PID="$!"
+ echo "MinIO Server PID: ${MINIO_SRVR_PID}"
+ echo
+ echo "Waiting for MinIO instance to get ready!"
+ sleep 10
+}
+
+main "$@"`, scheme)
+ adminLogIf(ctx, embedFileInZip(inspectZipW, "start-minio.sh", scrb.Bytes(), 0o755))
+}
+
+func getSubnetAdminPublicKey() []byte {
+ if globalIsCICD {
+ return subnetAdminPublicKeyDev
+ }
+ return subnetAdminPublicKey
+}
+
+func createHostAnonymizerForFSMode() map[string]string {
+ hostAnonymizer := map[string]string{
+ globalLocalNodeName: "server1",
+ }
+
+ apiEndpoints := getAPIEndpoints()
+ for _, ep := range apiEndpoints {
+ if len(ep) == 0 {
+ continue
+ }
+ if url, err := xnet.ParseHTTPURL(ep); err == nil {
+ // In FS mode the drive names don't include the host.
+ // So mapping just the host should be sufficient.
+ hostAnonymizer[url.Host] = "server1"
+ }
+ }
+ return hostAnonymizer
+}
+
+// anonymizeHost - Add entries related to given endpoint in the host anonymizer map
+// The health report data can contain the hostname in various forms e.g. host, host:port,
+// host:port/drivepath, full url (http://host:port/drivepath)
+// The anonymizer map will have mappings for all these variants for efficiently replacing
+// any of these strings to the anonymized versions at the time of health report generation.
+func anonymizeHost(hostAnonymizer map[string]string, endpoint Endpoint, poolNum int, srvrNum int) {
+ if len(endpoint.Host) == 0 {
+ return
+ }
+
+ currentURL := endpoint.String()
+
+ // mapIfNotPresent - Maps the given key to the value only if the key is not present in the map
+ mapIfNotPresent := func(m map[string]string, key string, val string) {
+ _, found := m[key]
+ if !found {
+ m[key] = val
+ }
+ }
+
+ _, found := hostAnonymizer[currentURL]
+ if !found {
+ // In distributed setup, anonymized addr = 'poolNum.serverNum'
+ newHost := fmt.Sprintf("pool%d.server%d", poolNum, srvrNum)
+ schemePfx := endpoint.Scheme + "://"
+
+ // Hostname
+ mapIfNotPresent(hostAnonymizer, endpoint.Hostname(), newHost)
+
+ newHostPort := newHost
+ if len(endpoint.Port()) > 0 {
+ // Host + port
+ newHostPort = newHost + ":" + endpoint.Port()
+ mapIfNotPresent(hostAnonymizer, endpoint.Host, newHostPort)
+ mapIfNotPresent(hostAnonymizer, schemePfx+endpoint.Host, newHostPort)
+ }
+
+ newHostPortPath := newHostPort
+ if len(endpoint.Path) > 0 {
+ // Host + port + path
+ currentHostPortPath := endpoint.Host + endpoint.Path
+ newHostPortPath = newHostPort + endpoint.Path
+ mapIfNotPresent(hostAnonymizer, currentHostPortPath, newHostPortPath)
+ mapIfNotPresent(hostAnonymizer, schemePfx+currentHostPortPath, newHostPortPath)
+ }
+
+ // Full url
+ hostAnonymizer[currentURL] = schemePfx + newHostPortPath
+ }
+}
+
+// createHostAnonymizer - Creates a map of various strings to corresponding anonymized names
+func createHostAnonymizer() map[string]string {
+ if !globalIsDistErasure {
+ return createHostAnonymizerForFSMode()
+ }
+
+ hostAnonymizer := map[string]string{}
+ hosts := set.NewStringSet()
+ srvrIdx := 0
+
+ for poolIdx, pool := range globalEndpoints {
+ for _, endpoint := range pool.Endpoints {
+ if !hosts.Contains(endpoint.Host) {
+ hosts.Add(endpoint.Host)
+ srvrIdx++
+ }
+ anonymizeHost(hostAnonymizer, endpoint, poolIdx+1, srvrIdx)
+ }
+ }
+ return hostAnonymizer
+}
diff --git a/cmd/admin-handlers_test.go b/cmd/admin-handlers_test.go
new file mode 100644
index 00000000..3f8b3482
--- /dev/null
+++ b/cmd/admin-handlers_test.go
@@ -0,0 +1,535 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "sort"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/auth"
+ "github.com/minio/mux"
+)
+
+// adminErasureTestBed - encapsulates subsystems that need to be setup for
+// admin-handler unit tests.
+type adminErasureTestBed struct {
+ erasureDirs []string
+ objLayer ObjectLayer
+ router *mux.Router
+ done context.CancelFunc
+}
+
+// prepareAdminErasureTestBed - helper function that setups a single-node
+// Erasure backend for admin-handler tests.
+func prepareAdminErasureTestBed(ctx context.Context) (*adminErasureTestBed, error) {
+ ctx, cancel := context.WithCancel(ctx)
+
+ // reset global variables to start afresh.
+ resetTestGlobals()
+
+ // Set globalIsErasure to indicate that the setup uses an erasure
+ // code backend.
+ globalIsErasure = true
+
+ // Initializing objectLayer for HealFormatHandler.
+ objLayer, erasureDirs, xlErr := initTestErasureObjLayer(ctx)
+ if xlErr != nil {
+ cancel()
+ return nil, xlErr
+ }
+
+ // Initialize minio server config.
+ if err := newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
+ cancel()
+ return nil, err
+ }
+
+ // Initialize boot time
+ globalBootTime = UTCNow()
+
+ globalEndpoints = mustGetPoolEndpoints(0, erasureDirs...)
+
+ initAllSubsystems(ctx)
+
+ initConfigSubsystem(ctx, objLayer)
+
+ globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
+
+ // Setup admin mgmt REST API handlers.
+ adminRouter := mux.NewRouter()
+ registerAdminRouter(adminRouter, true)
+
+ return &adminErasureTestBed{
+ erasureDirs: erasureDirs,
+ objLayer: objLayer,
+ router: adminRouter,
+ done: cancel,
+ }, nil
+}
+
+// TearDown - method that resets the test bed for subsequent unit
+// tests to start afresh.
+func (atb *adminErasureTestBed) TearDown() {
+ atb.done()
+ removeRoots(atb.erasureDirs)
+ resetTestGlobals()
+}
+
+// initTestObjLayer - Helper function to initialize an Erasure-based object
+// layer and set globalObjectAPI.
+func initTestErasureObjLayer(ctx context.Context) (ObjectLayer, []string, error) {
+ erasureDirs, err := getRandomDisks(16)
+ if err != nil {
+ return nil, nil, err
+ }
+ endpoints := mustGetPoolEndpoints(0, erasureDirs...)
+ globalPolicySys = NewPolicySys()
+ objLayer, err := newErasureServerPools(ctx, endpoints)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // Make objLayer available to all internal services via globalObjectAPI.
+ globalObjLayerMutex.Lock()
+ globalObjectAPI = objLayer
+ globalObjLayerMutex.Unlock()
+ return objLayer, erasureDirs, nil
+}
+
+// cmdType - Represents different service subcomands like status, stop
+// and restart.
+type cmdType int
+
+const (
+ restartCmd cmdType = iota
+ stopCmd
+)
+
+// toServiceSignal - Helper function that translates a given cmdType
+// value to its corresponding serviceSignal value.
+func (c cmdType) toServiceSignal() serviceSignal {
+ switch c {
+ case restartCmd:
+ return serviceRestart
+ case stopCmd:
+ return serviceStop
+ }
+ return serviceRestart
+}
+
+func (c cmdType) toServiceAction() madmin.ServiceAction {
+ switch c {
+ case restartCmd:
+ return madmin.ServiceActionRestart
+ case stopCmd:
+ return madmin.ServiceActionStop
+ }
+ return madmin.ServiceActionRestart
+}
+
+// testServiceSignalReceiver - Helper function that simulates a
+// go-routine waiting on service signal.
+func testServiceSignalReceiver(cmd cmdType, t *testing.T) {
+ expectedCmd := cmd.toServiceSignal()
+ serviceCmd := <-globalServiceSignalCh
+ if serviceCmd != expectedCmd {
+ t.Errorf("Expected service command %v but received %v", expectedCmd, serviceCmd)
+ }
+}
+
+// getServiceCmdRequest - Constructs a management REST API request for service
+// subcommands for a given cmdType value.
+func getServiceCmdRequest(cmd cmdType, cred auth.Credentials) (*http.Request, error) {
+ queryVal := url.Values{}
+ queryVal.Set("action", string(cmd.toServiceAction()))
+ queryVal.Set("type", "2")
+ resource := adminPathPrefix + adminAPIVersionPrefix + "/service?" + queryVal.Encode()
+ req, err := newTestRequest(http.MethodPost, resource, 0, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ // management REST API uses signature V4 for authentication.
+ err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
+ if err != nil {
+ return nil, err
+ }
+ return req, nil
+}
+
+// testServicesCmdHandler - parametrizes service subcommand tests on
+// cmdType value.
+func testServicesCmdHandler(cmd cmdType, t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ adminTestBed, err := prepareAdminErasureTestBed(ctx)
+ if err != nil {
+ t.Fatal("Failed to initialize a single node Erasure backend for admin handler tests.", err)
+ }
+ defer adminTestBed.TearDown()
+
+ // Initialize admin peers to make admin RPC calls. Note: In a
+ // single node setup, this degenerates to a simple function
+ // call under the hood.
+ globalMinioAddr = "127.0.0.1:9000"
+
+ var wg sync.WaitGroup
+
+ // Setting up a go routine to simulate ServerRouter's
+ // handleServiceSignals for stop and restart commands.
+ if cmd == restartCmd {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ testServiceSignalReceiver(cmd, t)
+ }()
+ }
+ credentials := globalActiveCred
+
+ req, err := getServiceCmdRequest(cmd, credentials)
+ if err != nil {
+ t.Fatalf("Failed to build service status request %v", err)
+ }
+
+ rec := httptest.NewRecorder()
+ adminTestBed.router.ServeHTTP(rec, req)
+
+ resp, _ := io.ReadAll(rec.Body)
+ if rec.Code != http.StatusOK {
+ t.Errorf("Expected to receive %d status code but received %d. Body (%s)",
+ http.StatusOK, rec.Code, string(resp))
+ }
+
+ result := &serviceResult{}
+ if err := json.Unmarshal(resp, result); err != nil {
+ t.Error(err)
+ }
+ _ = result
+
+ // Wait until testServiceSignalReceiver() called in a goroutine quits.
+ wg.Wait()
+}
+
+// Test for service restart management REST API.
+func TestServiceRestartHandler(t *testing.T) {
+ testServicesCmdHandler(restartCmd, t)
+}
+
+// buildAdminRequest - helper function to build an admin API request.
+func buildAdminRequest(queryVal url.Values, method, path string,
+ contentLength int64, bodySeeker io.ReadSeeker) (*http.Request, error,
+) {
+ req, err := newTestRequest(method,
+ adminPathPrefix+adminAPIVersionPrefix+path+"?"+queryVal.Encode(),
+ contentLength, bodySeeker)
+ if err != nil {
+ return nil, err
+ }
+
+ cred := globalActiveCred
+ err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+func TestAdminServerInfo(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ adminTestBed, err := prepareAdminErasureTestBed(ctx)
+ if err != nil {
+ t.Fatal("Failed to initialize a single node Erasure backend for admin handler tests.", err)
+ }
+
+ defer adminTestBed.TearDown()
+
+ // Initialize admin peers to make admin RPC calls.
+ globalMinioAddr = "127.0.0.1:9000"
+
+ // Prepare query params for set-config mgmt REST API.
+ queryVal := url.Values{}
+ queryVal.Set("info", "")
+
+ req, err := buildAdminRequest(queryVal, http.MethodGet, "/info", 0, nil)
+ if err != nil {
+ t.Fatalf("Failed to construct get-config object request - %v", err)
+ }
+
+ rec := httptest.NewRecorder()
+ adminTestBed.router.ServeHTTP(rec, req)
+ if rec.Code != http.StatusOK {
+ t.Errorf("Expected to succeed but failed with %d", rec.Code)
+ }
+
+ results := madmin.InfoMessage{}
+ err = json.NewDecoder(rec.Body).Decode(&results)
+ if err != nil {
+ t.Fatalf("Failed to decode set config result json %v", err)
+ }
+
+ if results.Region != globalMinioDefaultRegion {
+ t.Errorf("Expected %s, got %s", globalMinioDefaultRegion, results.Region)
+ }
+}
+
+// TestToAdminAPIErrCode - test for toAdminAPIErrCode helper function.
+func TestToAdminAPIErrCode(t *testing.T) {
+ testCases := []struct {
+ err error
+ expectedAPIErr APIErrorCode
+ }{
+ // 1. Server not in quorum.
+ {
+ err: errErasureWriteQuorum,
+ expectedAPIErr: ErrAdminConfigNoQuorum,
+ },
+ // 2. No error.
+ {
+ err: nil,
+ expectedAPIErr: ErrNone,
+ },
+ // 3. Non-admin API specific error.
+ {
+ err: errDiskNotFound,
+ expectedAPIErr: toAPIErrorCode(GlobalContext, errDiskNotFound),
+ },
+ }
+
+ for i, test := range testCases {
+ actualErr := toAdminAPIErrCode(GlobalContext, test.err)
+ if actualErr != test.expectedAPIErr {
+ t.Errorf("Test %d: Expected %v but received %v",
+ i+1, test.expectedAPIErr, actualErr)
+ }
+ }
+}
+
+func TestExtractHealInitParams(t *testing.T) {
+ mkParams := func(clientToken string, forceStart, forceStop bool) url.Values {
+ v := url.Values{}
+ if clientToken != "" {
+ v.Add(mgmtClientToken, clientToken)
+ }
+ if forceStart {
+ v.Add(mgmtForceStart, "")
+ }
+ if forceStop {
+ v.Add(mgmtForceStop, "")
+ }
+ return v
+ }
+ qParamsArr := []url.Values{
+ // Invalid cases
+ mkParams("", true, true),
+ mkParams("111", true, true),
+ mkParams("111", true, false),
+ mkParams("111", false, true),
+ // Valid cases follow
+ mkParams("", true, false),
+ mkParams("", false, true),
+ mkParams("", false, false),
+ mkParams("111", false, false),
+ }
+ varsArr := []map[string]string{
+ // Invalid cases
+ {mgmtPrefix: "objprefix"},
+ // Valid cases
+ {},
+ {mgmtBucket: "bucket"},
+ {mgmtBucket: "bucket", mgmtPrefix: "objprefix"},
+ }
+
+ // Body is always valid - we do not test JSON decoding.
+ body := `{"recursive": false, "dryRun": true, "remove": false, "scanMode": 0}`
+
+ // Test all combinations!
+ for pIdx, params := range qParamsArr {
+ for vIdx, vars := range varsArr {
+ _, err := extractHealInitParams(vars, params, bytes.NewReader([]byte(body)))
+ isErrCase := false
+ if pIdx < 4 || vIdx < 1 {
+ isErrCase = true
+ }
+
+ if err != ErrNone && !isErrCase {
+ t.Errorf("Got unexpected error: %v %v %v", pIdx, vIdx, err)
+ } else if err == ErrNone && isErrCase {
+ t.Errorf("Got no error but expected one: %v %v", pIdx, vIdx)
+ }
+ }
+ }
+}
+
+type byResourceUID struct{ madmin.LockEntries }
+
+func (b byResourceUID) Less(i, j int) bool {
+ toUniqLock := func(entry madmin.LockEntry) string {
+ return fmt.Sprintf("%s/%s", entry.Resource, entry.ID)
+ }
+ return toUniqLock(b.LockEntries[i]) < toUniqLock(b.LockEntries[j])
+}
+
+func TestTopLockEntries(t *testing.T) {
+ locksHeld := make(map[string][]lockRequesterInfo)
+ var owners []string
+ for i := range 4 {
+ owners = append(owners, fmt.Sprintf("node-%d", i))
+ }
+
+ // Simulate DeleteObjects of 10 objects in a single request. i.e same lock
+ // request UID, but 10 different resource names associated with it.
+ var lris []lockRequesterInfo
+ uuid := mustGetUUID()
+ for i := range 10 {
+ resource := fmt.Sprintf("bucket/delete-object-%d", i)
+ lri := lockRequesterInfo{
+ Name: resource,
+ Writer: true,
+ UID: uuid,
+ Owner: owners[i%len(owners)],
+ Group: true,
+ Quorum: 3,
+ }
+ lris = append(lris, lri)
+ locksHeld[resource] = []lockRequesterInfo{lri}
+ }
+
+ // Add a few concurrent read locks to the mix
+ for i := range 50 {
+ resource := fmt.Sprintf("bucket/get-object-%d", i)
+ lri := lockRequesterInfo{
+ Name: resource,
+ UID: mustGetUUID(),
+ Owner: owners[i%len(owners)],
+ Quorum: 2,
+ }
+ lris = append(lris, lri)
+ locksHeld[resource] = append(locksHeld[resource], lri)
+ // concurrent read lock, same resource different uid
+ lri.UID = mustGetUUID()
+ lris = append(lris, lri)
+ locksHeld[resource] = append(locksHeld[resource], lri)
+ }
+
+ var peerLocks []*PeerLocks
+ for _, owner := range owners {
+ peerLocks = append(peerLocks, &PeerLocks{
+ Addr: owner,
+ Locks: locksHeld,
+ })
+ }
+ var exp madmin.LockEntries
+ for _, lri := range lris {
+ lockType := func(lri lockRequesterInfo) string {
+ if lri.Writer {
+ return "WRITE"
+ }
+ return "READ"
+ }
+ exp = append(exp, madmin.LockEntry{
+ Resource: lri.Name,
+ Type: lockType(lri),
+ ServerList: owners,
+ Owner: lri.Owner,
+ ID: lri.UID,
+ Quorum: lri.Quorum,
+ Timestamp: time.Unix(0, lri.Timestamp),
+ })
+ }
+
+ testCases := []struct {
+ peerLocks []*PeerLocks
+ expected madmin.LockEntries
+ }{
+ {
+ peerLocks: peerLocks,
+ expected: exp,
+ },
+ }
+
+ // printEntries := func(entries madmin.LockEntries) {
+ // for i, entry := range entries {
+ // fmt.Printf("%d: %s %s %s %s %v %d\n", i, entry.Resource, entry.ID, entry.Owner, entry.Type, entry.ServerList, entry.Elapsed)
+ // }
+ // }
+
+ check := func(exp, got madmin.LockEntries) (int, bool) {
+ if len(exp) != len(got) {
+ return 0, false
+ }
+ sort.Slice(exp, byResourceUID{exp}.Less)
+ sort.Slice(got, byResourceUID{got}.Less)
+ // printEntries(exp)
+ // printEntries(got)
+ for i, e := range exp {
+ if !e.Timestamp.Equal(got[i].Timestamp) {
+ return i, false
+ }
+ // Skip checking elapsed since it's time sensitive.
+ // if e.Elapsed != got[i].Elapsed {
+ // return false
+ // }
+ if e.Resource != got[i].Resource {
+ return i, false
+ }
+ if e.Type != got[i].Type {
+ return i, false
+ }
+ if e.Source != got[i].Source {
+ return i, false
+ }
+ if e.Owner != got[i].Owner {
+ return i, false
+ }
+ if e.ID != got[i].ID {
+ return i, false
+ }
+ if len(e.ServerList) != len(got[i].ServerList) {
+ return i, false
+ }
+ for j := range e.ServerList {
+ if e.ServerList[j] != got[i].ServerList[j] {
+ return i, false
+ }
+ }
+ }
+ return 0, true
+ }
+
+ for i, tc := range testCases {
+ got := topLockEntries(tc.peerLocks, false)
+ if idx, ok := check(tc.expected, got); !ok {
+ t.Fatalf("%d: mismatch at %d \n expected %#v but got %#v", i, idx, tc.expected[idx], got[idx])
+ }
+ }
+}
diff --git a/cmd/admin-heal-ops.go b/cmd/admin-heal-ops.go
new file mode 100644
index 00000000..0b297634
--- /dev/null
+++ b/cmd/admin-heal-ops.go
@@ -0,0 +1,918 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "maps"
+ "net/http"
+ "sort"
+ "sync"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/minio/internal/logger"
+)
+
+// healStatusSummary - overall short summary of a healing sequence
+type healStatusSummary string
+
+// healStatusSummary constants
+const (
+ healNotStartedStatus healStatusSummary = "not started"
+ healRunningStatus = "running"
+ healStoppedStatus = "stopped"
+ healFinishedStatus = "finished"
+)
+
+const (
+ // a heal sequence with this many un-consumed heal result
+ // items blocks until heal-status consumption resumes or is
+ // aborted due to timeout.
+ maxUnconsumedHealResultItems = 1000
+
+ // if no heal-results are consumed (via the heal-status API)
+ // for this timeout duration, the heal sequence is aborted.
+ healUnconsumedTimeout = 24 * time.Hour
+
+ // time-duration to keep heal sequence state after it
+ // completes.
+ keepHealSeqStateDuration = time.Minute * 10
+
+ // nopHeal is a no operating healing action to
+ // wait for the current healing operation to finish
+ nopHeal = ""
+)
+
+var (
+ errHealIdleTimeout = errors.New("healing results were not consumed for too long")
+ errHealStopSignalled = errors.New("heal stop signaled")
+
+ errFnHealFromAPIErr = func(ctx context.Context, err error) error {
+ apiErr := toAdminAPIErr(ctx, err)
+ return fmt.Errorf("Heal internal error: %s: %s",
+ apiErr.Code, apiErr.Description)
+ }
+)
+
+// healSequenceStatus - accumulated status of the heal sequence
+type healSequenceStatus struct {
+ // summary and detail for failures
+ Summary healStatusSummary `json:"Summary"`
+ FailureDetail string `json:"Detail,omitempty"`
+ StartTime time.Time `json:"StartTime"`
+
+ // settings for the heal sequence
+ HealSettings madmin.HealOpts `json:"Settings"`
+
+ // slice of available heal result records
+ Items []madmin.HealResultItem `json:"Items"`
+}
+
+// structure to hold state of all heal sequences in server memory
+type allHealState struct {
+ sync.RWMutex
+
+ // map of heal path to heal sequence
+ healSeqMap map[string]*healSequence // Indexed by endpoint
+ // keep track of the healing status of disks in the memory
+ // false: the disk needs to be healed but no healing routine is started
+ // true: the disk is currently healing
+ healLocalDisks map[Endpoint]bool
+ healStatus map[string]healingTracker // Indexed by disk ID
+}
+
+// newHealState - initialize global heal state management
+func newHealState(ctx context.Context, cleanup bool) *allHealState {
+ hstate := &allHealState{
+ healSeqMap: make(map[string]*healSequence),
+ healLocalDisks: make(map[Endpoint]bool),
+ healStatus: make(map[string]healingTracker),
+ }
+ if cleanup {
+ go hstate.periodicHealSeqsClean(ctx)
+ }
+ return hstate
+}
+
+func (ahs *allHealState) popHealLocalDisks(healLocalDisks ...Endpoint) {
+ ahs.Lock()
+ defer ahs.Unlock()
+
+ for _, ep := range healLocalDisks {
+ delete(ahs.healLocalDisks, ep)
+ }
+ for id, disk := range ahs.healStatus {
+ for _, ep := range healLocalDisks {
+ if disk.Endpoint == ep.String() {
+ delete(ahs.healStatus, id)
+ }
+ }
+ }
+}
+
+// updateHealStatus will update the heal status.
+func (ahs *allHealState) updateHealStatus(tracker *healingTracker) {
+ ahs.Lock()
+ defer ahs.Unlock()
+
+ tracker.mu.RLock()
+ t := *tracker
+ t.QueuedBuckets = append(make([]string, 0, len(tracker.QueuedBuckets)), tracker.QueuedBuckets...)
+ t.HealedBuckets = append(make([]string, 0, len(tracker.HealedBuckets)), tracker.HealedBuckets...)
+ ahs.healStatus[tracker.ID] = t
+ tracker.mu.RUnlock()
+}
+
+// Sort by zone, set and disk index
+func sortDisks(disks []madmin.Disk) {
+ sort.Slice(disks, func(i, j int) bool {
+ a, b := &disks[i], &disks[j]
+ if a.PoolIndex != b.PoolIndex {
+ return a.PoolIndex < b.PoolIndex
+ }
+ if a.SetIndex != b.SetIndex {
+ return a.SetIndex < b.SetIndex
+ }
+ return a.DiskIndex < b.DiskIndex
+ })
+}
+
+// getLocalHealingDisks returns local healing disks indexed by endpoint.
+func (ahs *allHealState) getLocalHealingDisks() map[string]madmin.HealingDisk {
+ ahs.RLock()
+ defer ahs.RUnlock()
+ dst := make(map[string]madmin.HealingDisk, len(ahs.healStatus))
+ for _, v := range ahs.healStatus {
+ dst[v.Endpoint] = v.toHealingDisk()
+ }
+
+ return dst
+}
+
+// getHealLocalDiskEndpoints() returns the list of disks that need
+// to be healed but there is no healing routine in progress on them.
+func (ahs *allHealState) getHealLocalDiskEndpoints() Endpoints {
+ ahs.RLock()
+ defer ahs.RUnlock()
+
+ var endpoints Endpoints
+ for ep, healing := range ahs.healLocalDisks {
+ if !healing {
+ endpoints = append(endpoints, ep)
+ }
+ }
+ return endpoints
+}
+
+// Set, in the memory, the state of the disk as currently healing or not
+func (ahs *allHealState) setDiskHealingStatus(ep Endpoint, healing bool) {
+ ahs.Lock()
+ defer ahs.Unlock()
+
+ ahs.healLocalDisks[ep] = healing
+}
+
+func (ahs *allHealState) pushHealLocalDisks(healLocalDisks ...Endpoint) {
+ ahs.Lock()
+ defer ahs.Unlock()
+
+ for _, ep := range healLocalDisks {
+ ahs.healLocalDisks[ep] = false
+ }
+}
+
+func (ahs *allHealState) periodicHealSeqsClean(ctx context.Context) {
+ // Launch clean-up routine to remove this heal sequence (after
+ // it ends) from the global state after timeout has elapsed.
+ periodicTimer := time.NewTimer(time.Minute * 5)
+ defer periodicTimer.Stop()
+
+ for {
+ select {
+ case <-periodicTimer.C:
+ now := UTCNow()
+ ahs.Lock()
+ for path, h := range ahs.healSeqMap {
+ if h.hasEnded() && h.endTime.Add(keepHealSeqStateDuration).Before(now) {
+ delete(ahs.healSeqMap, path)
+ }
+ }
+ ahs.Unlock()
+
+ periodicTimer.Reset(time.Minute * 5)
+ case <-ctx.Done():
+ // server could be restarting - need
+ // to exit immediately
+ return
+ }
+ }
+}
+
+// getHealSequenceByToken - Retrieve a heal sequence by token. The second
+// argument returns if a heal sequence actually exists.
+func (ahs *allHealState) getHealSequenceByToken(token string) (h *healSequence, exists bool) {
+ ahs.RLock()
+ defer ahs.RUnlock()
+ for _, healSeq := range ahs.healSeqMap {
+ if healSeq.clientToken == token {
+ return healSeq, true
+ }
+ }
+ return nil, false
+}
+
+// getHealSequence - Retrieve a heal sequence by path. The second
+// argument returns if a heal sequence actually exists.
+func (ahs *allHealState) getHealSequence(path string) (h *healSequence, exists bool) {
+ ahs.RLock()
+ defer ahs.RUnlock()
+ h, exists = ahs.healSeqMap[path]
+ return h, exists
+}
+
+func (ahs *allHealState) stopHealSequence(path string) ([]byte, APIError) {
+ var hsp madmin.HealStopSuccess
+ he, exists := ahs.getHealSequence(path)
+ if !exists {
+ hsp = madmin.HealStopSuccess{
+ ClientToken: "unknown",
+ StartTime: UTCNow(),
+ }
+ } else {
+ clientToken := he.clientToken
+ if globalIsDistErasure {
+ clientToken = fmt.Sprintf("%s%s%d", he.clientToken, getKeySeparator(), GetProxyEndpointLocalIndex(globalProxyEndpoints))
+ }
+
+ hsp = madmin.HealStopSuccess{
+ ClientToken: clientToken,
+ ClientAddress: he.clientAddress,
+ StartTime: he.startTime,
+ }
+
+ he.stop()
+ for !he.hasEnded() {
+ time.Sleep(1 * time.Second)
+ }
+ ahs.Lock()
+ defer ahs.Unlock()
+ // Heal sequence explicitly stopped, remove it.
+ delete(ahs.healSeqMap, path)
+ }
+
+ b, err := json.Marshal(&hsp)
+ return b, toAdminAPIErr(GlobalContext, err)
+}
+
+// LaunchNewHealSequence - launches a background routine that performs
+// healing according to the healSequence argument. For each heal
+// sequence, state is stored in the `globalAllHealState`, which is a
+// map of the heal path to `healSequence` which holds state about the
+// heal sequence.
+//
+// Heal results are persisted in server memory for
+// `keepHealSeqStateDuration`. This function also launches a
+// background routine to clean up heal results after the
+// aforementioned duration.
+func (ahs *allHealState) LaunchNewHealSequence(h *healSequence, objAPI ObjectLayer) (
+ respBytes []byte, apiErr APIError, errMsg string,
+) {
+ if h.forceStarted {
+ _, apiErr = ahs.stopHealSequence(pathJoin(h.bucket, h.object))
+ if apiErr.Code != "" {
+ return respBytes, apiErr, ""
+ }
+ } else {
+ oh, exists := ahs.getHealSequence(pathJoin(h.bucket, h.object))
+ if exists && !oh.hasEnded() {
+ errMsg = "Heal is already running on the given path " +
+ "(use force-start option to stop and start afresh). " +
+ fmt.Sprintf("The heal was started by IP %s at %s, token is %s",
+ h.clientAddress, h.startTime.Format(http.TimeFormat), h.clientToken)
+ return nil, errorCodes.ToAPIErr(ErrHealAlreadyRunning), errMsg
+ }
+ }
+
+ ahs.Lock()
+ defer ahs.Unlock()
+
+ // Check if new heal sequence to be started overlaps with any
+ // existing, running sequence
+ hpath := pathJoin(h.bucket, h.object)
+ for k, hSeq := range ahs.healSeqMap {
+ if !hSeq.hasEnded() && (HasPrefix(k, hpath) || HasPrefix(hpath, k)) {
+ errMsg = "The provided heal sequence path overlaps with an existing " +
+ fmt.Sprintf("heal path: %s", k)
+ return nil, errorCodes.ToAPIErr(ErrHealOverlappingPaths), errMsg
+ }
+ }
+
+ // Add heal state and start sequence
+ ahs.healSeqMap[hpath] = h
+
+ clientToken := h.clientToken
+ if globalIsDistErasure {
+ clientToken = fmt.Sprintf("%s%s%d", h.clientToken, getKeySeparator(), GetProxyEndpointLocalIndex(globalProxyEndpoints))
+ }
+
+ if h.clientToken == bgHealingUUID {
+ // For background heal do nothing, do not spawn an unnecessary goroutine.
+ } else {
+ // Launch top-level background heal go-routine
+ go h.healSequenceStart(objAPI)
+ }
+
+ b, err := json.Marshal(madmin.HealStartSuccess{
+ ClientToken: clientToken,
+ ClientAddress: h.clientAddress,
+ StartTime: h.startTime,
+ })
+ if err != nil {
+ bugLogIf(h.ctx, err)
+ return nil, toAdminAPIErr(h.ctx, err), ""
+ }
+ return b, noError, ""
+}
+
+// PopHealStatusJSON - Called by heal-status API. It fetches the heal
+// status results from global state and returns its JSON
+// representation. The clientToken helps ensure there aren't
+// conflicting clients fetching status.
+func (ahs *allHealState) PopHealStatusJSON(hpath string,
+ clientToken string) ([]byte, APIErrorCode,
+) {
+ // fetch heal state for given path
+ h, exists := ahs.getHealSequence(hpath)
+ if !exists {
+ // heal sequence doesn't exist, must have finished.
+ jbytes, err := json.Marshal(healSequenceStatus{
+ Summary: healFinishedStatus,
+ })
+ return jbytes, toAdminAPIErrCode(GlobalContext, err)
+ }
+
+ // Check if client-token is valid
+ if clientToken != h.clientToken {
+ return nil, ErrHealInvalidClientToken
+ }
+
+ // Take lock to access and update the heal-sequence
+ h.mutex.Lock()
+ defer h.mutex.Unlock()
+
+ numItems := len(h.currentStatus.Items)
+
+ // calculate index of most recently available heal result
+ // record.
+ lastResultIndex := h.lastSentResultIndex
+ if numItems > 0 {
+ lastResultIndex = h.currentStatus.Items[numItems-1].ResultIndex
+ }
+
+ h.lastSentResultIndex = lastResultIndex
+
+ jbytes, err := json.Marshal(h.currentStatus)
+ if err != nil {
+ h.currentStatus.Items = nil
+
+ bugLogIf(h.ctx, err)
+ return nil, ErrInternalError
+ }
+
+ h.currentStatus.Items = nil
+
+ return jbytes, ErrNone
+}
+
+// healSource denotes single entity and heal option.
+type healSource struct {
+ bucket string
+ object string
+ versionID string
+ noWait bool // a non blocking call, if task queue is full return right away.
+ opts *madmin.HealOpts // optional heal option overrides default setting
+}
+
+// healSequence - state for each heal sequence initiated on the
+// server.
+type healSequence struct {
+ // bucket, and object on which heal seq. was initiated
+ bucket, object string
+
+ // Report healing progress
+ reportProgress bool
+
+ // time at which heal sequence was started
+ startTime time.Time
+
+ // time at which heal sequence has ended
+ endTime time.Time
+
+ // Heal client info
+ clientToken, clientAddress string
+
+ // was this heal sequence force started?
+ forceStarted bool
+
+ // heal settings applied to this heal sequence
+ settings madmin.HealOpts
+
+ // current accumulated status of the heal sequence
+ currentStatus healSequenceStatus
+
+ // channel signaled by background routine when traversal has
+ // completed
+ traverseAndHealDoneCh chan error
+
+ // canceler to cancel heal sequence.
+ cancelCtx context.CancelFunc
+
+ // the last result index sent to client
+ lastSentResultIndex int64
+
+ // Number of total items scanned against item type
+ scannedItemsMap map[madmin.HealItemType]int64
+
+ // Number of total items healed against item type
+ healedItemsMap map[madmin.HealItemType]int64
+
+ // Number of total items where healing failed against item type
+ healFailedItemsMap map[madmin.HealItemType]int64
+
+ // The time of the last scan/heal activity
+ lastHealActivity time.Time
+
+ // Holds the request-info for logging
+ ctx context.Context
+
+ // used to lock this structure as it is concurrently accessed
+ mutex sync.RWMutex
+}
+
+// NewHealSequence - creates healSettings, assumes bucket and
+// objPrefix are already validated.
+func newHealSequence(ctx context.Context, bucket, objPrefix, clientAddr string,
+ hs madmin.HealOpts, forceStart bool,
+) *healSequence {
+ reqInfo := &logger.ReqInfo{RemoteHost: clientAddr, API: "Heal", BucketName: bucket}
+ reqInfo.AppendTags("prefix", objPrefix)
+ ctx, cancel := context.WithCancel(logger.SetReqInfo(ctx, reqInfo))
+
+ clientToken := mustGetUUID()
+
+ return &healSequence{
+ bucket: bucket,
+ object: objPrefix,
+ reportProgress: true,
+ startTime: UTCNow(),
+ clientToken: clientToken,
+ clientAddress: clientAddr,
+ forceStarted: forceStart,
+ settings: hs,
+ currentStatus: healSequenceStatus{
+ Summary: healNotStartedStatus,
+ HealSettings: hs,
+ },
+ traverseAndHealDoneCh: make(chan error),
+ cancelCtx: cancel,
+ ctx: ctx,
+ scannedItemsMap: make(map[madmin.HealItemType]int64),
+ healedItemsMap: make(map[madmin.HealItemType]int64),
+ healFailedItemsMap: make(map[madmin.HealItemType]int64),
+ }
+}
+
+// getScannedItemsCount - returns a count of all scanned items
+func (h *healSequence) getScannedItemsCount() int64 {
+ var count int64
+ h.mutex.RLock()
+ defer h.mutex.RUnlock()
+
+ for _, v := range h.scannedItemsMap {
+ count += v
+ }
+ return count
+}
+
+// getScannedItemsMap - returns map of all scanned items against type
+func (h *healSequence) getScannedItemsMap() map[madmin.HealItemType]int64 {
+ h.mutex.RLock()
+ defer h.mutex.RUnlock()
+
+ // Make a copy before returning the value
+ retMap := make(map[madmin.HealItemType]int64, len(h.scannedItemsMap))
+ maps.Copy(retMap, h.scannedItemsMap)
+
+ return retMap
+}
+
+// getHealedItemsMap - returns the map of all healed items against type
+func (h *healSequence) getHealedItemsMap() map[madmin.HealItemType]int64 {
+ h.mutex.RLock()
+ defer h.mutex.RUnlock()
+
+ // Make a copy before returning the value
+ retMap := make(map[madmin.HealItemType]int64, len(h.healedItemsMap))
+ maps.Copy(retMap, h.healedItemsMap)
+
+ return retMap
+}
+
+// getHealFailedItemsMap - returns map of all items where heal failed against
+// drive endpoint and status
+func (h *healSequence) getHealFailedItemsMap() map[madmin.HealItemType]int64 {
+ h.mutex.RLock()
+ defer h.mutex.RUnlock()
+
+ // Make a copy before returning the value
+ retMap := make(map[madmin.HealItemType]int64, len(h.healFailedItemsMap))
+ maps.Copy(retMap, h.healFailedItemsMap)
+
+ return retMap
+}
+
+func (h *healSequence) countFailed(healType madmin.HealItemType) {
+ h.mutex.Lock()
+ defer h.mutex.Unlock()
+
+ h.healFailedItemsMap[healType]++
+ h.lastHealActivity = UTCNow()
+}
+
+func (h *healSequence) countScanned(healType madmin.HealItemType) {
+ h.mutex.Lock()
+ defer h.mutex.Unlock()
+
+ h.scannedItemsMap[healType]++
+ h.lastHealActivity = UTCNow()
+}
+
+func (h *healSequence) countHealed(healType madmin.HealItemType) {
+ h.mutex.Lock()
+ defer h.mutex.Unlock()
+
+ h.healedItemsMap[healType]++
+ h.lastHealActivity = UTCNow()
+}
+
+// isQuitting - determines if the heal sequence is quitting (due to an
+// external signal)
+func (h *healSequence) isQuitting() bool {
+ select {
+ case <-h.ctx.Done():
+ return true
+ default:
+ return false
+ }
+}
+
+// check if the heal sequence has ended
+func (h *healSequence) hasEnded() bool {
+ h.mutex.RLock()
+ defer h.mutex.RUnlock()
+ // background heal never ends
+ if h.clientToken == bgHealingUUID {
+ return false
+ }
+ return !h.endTime.IsZero()
+}
+
+// stops the heal sequence - safe to call multiple times.
+func (h *healSequence) stop() {
+ h.cancelCtx()
+}
+
+// pushHealResultItem - pushes a heal result item for consumption in
+// the heal-status API. It blocks if there are
+// maxUnconsumedHealResultItems. When it blocks, the heal sequence
+// routine is effectively paused - this happens when the server has
+// accumulated the maximum number of heal records per heal
+// sequence. When the client consumes further records, the heal
+// sequence automatically resumes. The return value indicates if the
+// operation succeeded.
+func (h *healSequence) pushHealResultItem(r madmin.HealResultItem) error {
+ // start a timer to keep an upper time limit to find an empty
+ // slot to add the given heal result - if no slot is found it
+ // means that the server is holding the maximum amount of
+ // heal-results in memory and the client has not consumed it
+ // for too long.
+ unconsumedTimer := time.NewTimer(healUnconsumedTimeout)
+ defer unconsumedTimer.Stop()
+
+ var itemsLen int
+ for {
+ h.mutex.Lock()
+ itemsLen = len(h.currentStatus.Items)
+ if itemsLen == maxUnconsumedHealResultItems {
+ // wait for a second, or quit if an external
+ // stop signal is received or the
+ // unconsumedTimer fires.
+ select {
+ // Check after a second
+ case <-time.After(time.Second):
+ h.mutex.Unlock()
+ continue
+
+ case <-h.ctx.Done():
+ h.mutex.Unlock()
+ // discard result and return.
+ return errHealStopSignalled
+
+ // Timeout if no results consumed for too long.
+ case <-unconsumedTimer.C:
+ h.mutex.Unlock()
+ return errHealIdleTimeout
+ }
+ }
+ break
+ }
+
+ // Set the correct result index for the new result item
+ if itemsLen > 0 {
+ r.ResultIndex = 1 + h.currentStatus.Items[itemsLen-1].ResultIndex
+ } else {
+ r.ResultIndex = 1 + h.lastSentResultIndex
+ }
+
+ // append to results
+ h.currentStatus.Items = append(h.currentStatus.Items, r)
+
+ // release lock
+ h.mutex.Unlock()
+
+ return nil
+}
+
+// healSequenceStart - this is the top-level background heal
+// routine. It launches another go-routine that actually traverses
+// on-disk data, checks and heals according to the selected
+// settings. This go-routine itself, (1) monitors the traversal
+// routine for completion, and (2) listens for external stop
+// signals. When either event happens, it sets the finish status for
+// the heal-sequence.
+func (h *healSequence) healSequenceStart(objAPI ObjectLayer) {
+ // Set status as running
+ h.mutex.Lock()
+ h.currentStatus.Summary = healRunningStatus
+ h.currentStatus.StartTime = UTCNow()
+ h.mutex.Unlock()
+
+ go h.traverseAndHeal(objAPI)
+
+ select {
+ case err, ok := <-h.traverseAndHealDoneCh:
+ if !ok {
+ return
+ }
+ h.mutex.Lock()
+ h.endTime = UTCNow()
+ // Heal traversal is complete.
+ if err == nil {
+ // heal traversal succeeded.
+ h.currentStatus.Summary = healFinishedStatus
+ } else {
+ // heal traversal had an error.
+ h.currentStatus.Summary = healStoppedStatus
+ h.currentStatus.FailureDetail = err.Error()
+ }
+ h.mutex.Unlock()
+ case <-h.ctx.Done():
+ h.mutex.Lock()
+ h.endTime = UTCNow()
+ h.currentStatus.Summary = healFinishedStatus
+ h.mutex.Unlock()
+
+ // drain traverse channel so the traversal
+ // go-routine does not leak.
+ go func() {
+ // Eventually the traversal go-routine closes
+ // the channel and returns, so this go-routine
+ // itself will not leak.
+ <-h.traverseAndHealDoneCh
+ }()
+ }
+}
+
+func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItemType) error {
+ // Send heal request
+ task := healTask{
+ bucket: source.bucket,
+ object: source.object,
+ versionID: source.versionID,
+ opts: h.settings,
+ }
+ if source.opts != nil {
+ task.opts = *source.opts
+ } else {
+ task.opts.ScanMode = madmin.HealNormalScan
+ }
+
+ h.countScanned(healType)
+
+ if source.noWait {
+ select {
+ case globalBackgroundHealRoutine.tasks <- task:
+ if serverDebugLog {
+ fmt.Printf("Task in the queue: %#v\n", task)
+ }
+ default:
+ // task queue is full, no more workers, we shall move on and heal later.
+ return nil
+ }
+ // Don't wait for result
+ return nil
+ }
+
+ // respCh must be set to wait for result.
+ // We make it size 1, so a result can always be written
+ // even if we aren't listening.
+ task.respCh = make(chan healResult, 1)
+ select {
+ case globalBackgroundHealRoutine.tasks <- task:
+ if serverDebugLog {
+ fmt.Printf("Task in the queue: %#v\n", task)
+ }
+ case <-h.ctx.Done():
+ return nil
+ }
+
+ countOKDrives := func(drives []madmin.HealDriveInfo) (count int) {
+ for _, drive := range drives {
+ if drive.State == madmin.DriveStateOk {
+ count++
+ }
+ }
+ return count
+ }
+
+ // task queued, now wait for the response.
+ select {
+ case res := <-task.respCh:
+ if res.err == nil {
+ h.countHealed(healType)
+ } else {
+ h.countFailed(healType)
+ }
+ if !h.reportProgress {
+ if errors.Is(res.err, errSkipFile) { // this is only sent usually by nopHeal
+ return nil
+ }
+
+ // Report caller of any failure
+ return res.err
+ }
+ res.result.Type = healType
+ if res.err != nil {
+ res.result.Detail = res.err.Error()
+ }
+ if res.result.ParityBlocks > 0 && res.result.DataBlocks > 0 && res.result.DataBlocks > res.result.ParityBlocks {
+ if got := countOKDrives(res.result.After.Drives); got < res.result.ParityBlocks {
+ res.result.Detail = fmt.Sprintf("quorum loss - expected %d minimum, got drive states in OK %d", res.result.ParityBlocks, got)
+ }
+ }
+ return h.pushHealResultItem(res.result)
+ case <-h.ctx.Done():
+ return nil
+ }
+}
+
+func (h *healSequence) healDiskMeta(objAPI ObjectLayer) error {
+ // Start healing the config prefix.
+ return h.healMinioSysMeta(objAPI, minioConfigPrefix)()
+}
+
+func (h *healSequence) healItems(objAPI ObjectLayer) error {
+ if h.clientToken == bgHealingUUID {
+ // For background heal do nothing.
+ return nil
+ }
+
+ if h.bucket == "" { // heal internal meta only during a site-wide heal
+ if err := h.healDiskMeta(objAPI); err != nil {
+ return err
+ }
+ }
+
+ // Heal buckets and objects
+ return h.healBuckets(objAPI)
+}
+
+// traverseAndHeal - traverses on-disk data and performs healing
+// according to settings. At each "safe" point it also checks if an
+// external quit signal has been received and quits if so. Since the
+// healing traversal may be mutating on-disk data when an external
+// quit signal is received, this routine cannot quit immediately and
+// has to wait until a safe point is reached, such as between scanning
+// two objects.
+func (h *healSequence) traverseAndHeal(objAPI ObjectLayer) {
+ h.traverseAndHealDoneCh <- h.healItems(objAPI)
+ xioutil.SafeClose(h.traverseAndHealDoneCh)
+}
+
+// healMinioSysMeta - heals all files under a given meta prefix, returns a function
+// which in-turn heals the respective meta directory path and any files in int.
+func (h *healSequence) healMinioSysMeta(objAPI ObjectLayer, metaPrefix string) func() error {
+ return func() error {
+ // NOTE: Healing on meta is run regardless
+ // of any bucket being selected, this is to ensure that
+ // meta are always upto date and correct.
+ h.settings.Recursive = true
+ return objAPI.HealObjects(h.ctx, minioMetaBucket, metaPrefix, h.settings, func(bucket, object, versionID string, scanMode madmin.HealScanMode) error {
+ if h.isQuitting() {
+ return errHealStopSignalled
+ }
+
+ err := h.queueHealTask(healSource{
+ bucket: bucket,
+ object: object,
+ versionID: versionID,
+ }, madmin.HealItemBucketMetadata)
+ return err
+ })
+ }
+}
+
+// healBuckets - check for all buckets heal or just particular bucket.
+func (h *healSequence) healBuckets(objAPI ObjectLayer) error {
+ if h.isQuitting() {
+ return errHealStopSignalled
+ }
+
+ // 1. If a bucket was specified, heal only the bucket.
+ if h.bucket != "" {
+ return h.healBucket(objAPI, h.bucket, false)
+ }
+
+ buckets, err := objAPI.ListBuckets(h.ctx, BucketOptions{})
+ if err != nil {
+ return errFnHealFromAPIErr(h.ctx, err)
+ }
+
+ // Heal latest buckets first.
+ sort.Slice(buckets, func(i, j int) bool {
+ return buckets[i].Created.After(buckets[j].Created)
+ })
+
+ for _, bucket := range buckets {
+ if err = h.healBucket(objAPI, bucket.Name, false); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// healBucket - traverses and heals given bucket
+func (h *healSequence) healBucket(objAPI ObjectLayer, bucket string, bucketsOnly bool) error {
+ if err := h.queueHealTask(healSource{bucket: bucket}, madmin.HealItemBucket); err != nil {
+ return err
+ }
+
+ if bucketsOnly {
+ return nil
+ }
+
+ if err := objAPI.HealObjects(h.ctx, bucket, h.object, h.settings, h.healObject); err != nil {
+ return errFnHealFromAPIErr(h.ctx, err)
+ }
+ return nil
+}
+
+// healObject - heal the given object and record result
+func (h *healSequence) healObject(bucket, object, versionID string, scanMode madmin.HealScanMode) error {
+ if h.isQuitting() {
+ return errHealStopSignalled
+ }
+
+ err := h.queueHealTask(healSource{
+ bucket: bucket,
+ object: object,
+ versionID: versionID,
+ opts: &h.settings,
+ }, madmin.HealItemObject)
+
+ // Wait and proceed if there are active requests
+ waitForLowHTTPReq()
+
+ return err
+}
diff --git a/cmd/admin-router.go b/cmd/admin-router.go
new file mode 100644
index 00000000..d218e475
--- /dev/null
+++ b/cmd/admin-router.go
@@ -0,0 +1,441 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "net/http"
+
+ "github.com/klauspost/compress/gzhttp"
+ "github.com/klauspost/compress/gzip"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/mux"
+)
+
+const (
+ adminPathPrefix = minioReservedBucketPath + "/admin"
+ adminAPIVersion = madmin.AdminAPIVersion
+ adminAPIVersionPrefix = SlashSeparator + adminAPIVersion
+ adminAPISiteReplicationDevNull = "/site-replication/devnull"
+ adminAPISiteReplicationNetPerf = "/site-replication/netperf"
+ adminAPIClientDevNull = "/speedtest/client/devnull"
+ adminAPIClientDevExtraTime = "/speedtest/client/devnull/extratime"
+)
+
+var gzipHandler = func() func(http.Handler) http.HandlerFunc {
+ gz, err := gzhttp.NewWrapper(gzhttp.MinSize(1000), gzhttp.CompressionLevel(gzip.BestSpeed))
+ if err != nil {
+ // Static params, so this is very unlikely.
+ logger.Fatal(err, "Unable to initialize server")
+ }
+ return gz
+}()
+
+// Set of handler options as bit flags
+type hFlag uint8
+
+const (
+ // this flag disables gzip compression of responses
+ noGZFlag = 1 << iota
+
+ // this flag enables tracing body and headers instead of just headers
+ traceAllFlag
+
+ // pass this flag to skip checking if object layer is available
+ noObjLayerFlag
+)
+
+// Has checks if the given flag is enabled in `h`.
+func (h hFlag) Has(flag hFlag) bool {
+ // Use bitwise-AND and check if the result is non-zero.
+ return h&flag != 0
+}
+
+// adminMiddleware performs some common admin handler functionality for all
+// handlers:
+//
+// - updates request context with `logger.ReqInfo` and api name based on the
+// name of the function handler passed (this handler must be a method of
+// `adminAPIHandlers`).
+//
+// - sets up call to send AuditLog
+//
+// While this is a middleware function (i.e. it takes a handler function and
+// returns one), due to flags being passed based on required conditions, it is
+// done per-"handler function registration" in the router.
+//
+// The passed in handler function must be a method of `adminAPIHandlers` for the
+// name displayed in logs and trace to be accurate. The name is extracted via
+// reflection.
+//
+// When no flags are passed, gzip compression, http tracing of headers and
+// checking of object layer availability are all enabled. Use flags to modify
+// this behavior.
+func adminMiddleware(f http.HandlerFunc, flags ...hFlag) http.HandlerFunc {
+ // Collect all flags with bitwise-OR and assign operator
+ var handlerFlags hFlag
+ for _, flag := range flags {
+ handlerFlags |= flag
+ }
+
+ // Get name of the handler using reflection.
+ handlerName := getHandlerName(f, "adminAPIHandlers")
+
+ var handler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
+ // Update request context with `logger.ReqInfo`.
+ r = r.WithContext(newContext(r, w, handlerName))
+
+ defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
+
+ // Check if object layer is available, if not return error early.
+ if !handlerFlags.Has(noObjLayerFlag) {
+ objectAPI := newObjectLayerFn()
+ if objectAPI == nil || globalNotificationSys == nil {
+ writeErrorResponseJSON(r.Context(), w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+ }
+
+ // Apply http tracing "middleware" based on presence of flag.
+ var f2 http.HandlerFunc
+ if handlerFlags.Has(traceAllFlag) {
+ f2 = httpTraceAll(f)
+ } else {
+ f2 = httpTraceHdrs(f)
+ }
+
+ // call the final handler
+ f2(w, r)
+ }
+
+ // Enable compression of responses based on presence of flag.
+ if !handlerFlags.Has(noGZFlag) {
+ handler = gzipHandler(handler)
+ }
+
+ return handler
+}
+
+// adminAPIHandlers provides HTTP handlers for MinIO admin API.
+type adminAPIHandlers struct{}
+
+// registerAdminRouter - Add handler functions for each service REST API routes.
+func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
+ adminAPI := adminAPIHandlers{}
+ // Admin router
+ adminRouter := router.PathPrefix(adminPathPrefix).Subrouter()
+
+ adminVersions := []string{
+ adminAPIVersionPrefix,
+ }
+
+ for _, adminVersion := range adminVersions {
+ // Restart and stop MinIO service type=2
+ adminRouter.Methods(http.MethodPost).Path(adminVersion+"/service").HandlerFunc(adminMiddleware(adminAPI.ServiceV2Handler, traceAllFlag)).Queries("action", "{action:.*}", "type", "2")
+
+ // Deprecated: Restart and stop MinIO service.
+ adminRouter.Methods(http.MethodPost).Path(adminVersion+"/service").HandlerFunc(adminMiddleware(adminAPI.ServiceHandler, traceAllFlag)).Queries("action", "{action:.*}")
+
+ // Update all MinIO servers type=2
+ adminRouter.Methods(http.MethodPost).Path(adminVersion+"/update").HandlerFunc(adminMiddleware(adminAPI.ServerUpdateV2Handler, traceAllFlag)).Queries("updateURL", "{updateURL:.*}", "type", "2")
+
+ // Deprecated: Update MinIO servers.
+ adminRouter.Methods(http.MethodPost).Path(adminVersion+"/update").HandlerFunc(adminMiddleware(adminAPI.ServerUpdateHandler, traceAllFlag)).Queries("updateURL", "{updateURL:.*}")
+
+ // Info operations
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/info").HandlerFunc(adminMiddleware(adminAPI.ServerInfoHandler, traceAllFlag, noObjLayerFlag))
+ adminRouter.Methods(http.MethodGet, http.MethodPost).Path(adminVersion + "/inspect-data").HandlerFunc(adminMiddleware(adminAPI.InspectDataHandler, noGZFlag, traceHdrsS3HFlag))
+
+ // StorageInfo operations
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/storageinfo").HandlerFunc(adminMiddleware(adminAPI.StorageInfoHandler, traceAllFlag))
+ // DataUsageInfo operations
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/datausageinfo").HandlerFunc(adminMiddleware(adminAPI.DataUsageInfoHandler, traceAllFlag))
+ // Metrics operation
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/metrics").HandlerFunc(adminMiddleware(adminAPI.MetricsHandler, traceHdrsS3HFlag))
+
+ if globalIsDistErasure || globalIsErasure {
+ // Heal operations
+
+ // Heal processing endpoint.
+ adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/").HandlerFunc(adminMiddleware(adminAPI.HealHandler, traceAllFlag))
+ adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/{bucket}").HandlerFunc(adminMiddleware(adminAPI.HealHandler, traceAllFlag))
+ adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/{bucket}/{prefix:.*}").HandlerFunc(adminMiddleware(adminAPI.HealHandler, traceAllFlag))
+ adminRouter.Methods(http.MethodPost).Path(adminVersion + "/background-heal/status").HandlerFunc(adminMiddleware(adminAPI.BackgroundHealStatusHandler, traceAllFlag))
+
+ // Pool operations
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/pools/list").HandlerFunc(adminMiddleware(adminAPI.ListPools, traceAllFlag))
+ adminRouter.Methods(http.MethodGet).Path(adminVersion+"/pools/status").HandlerFunc(adminMiddleware(adminAPI.StatusPool, traceAllFlag)).Queries("pool", "{pool:.*}")
+
+ adminRouter.Methods(http.MethodPost).Path(adminVersion+"/pools/decommission").HandlerFunc(adminMiddleware(adminAPI.StartDecommission, traceAllFlag)).Queries("pool", "{pool:.*}")
+ adminRouter.Methods(http.MethodPost).Path(adminVersion+"/pools/cancel").HandlerFunc(adminMiddleware(adminAPI.CancelDecommission, traceAllFlag)).Queries("pool", "{pool:.*}")
+
+ // Rebalance operations
+ adminRouter.Methods(http.MethodPost).Path(adminVersion + "/rebalance/start").HandlerFunc(adminMiddleware(adminAPI.RebalanceStart, traceAllFlag))
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/rebalance/status").HandlerFunc(adminMiddleware(adminAPI.RebalanceStatus, traceAllFlag))
+ adminRouter.Methods(http.MethodPost).Path(adminVersion + "/rebalance/stop").HandlerFunc(adminMiddleware(adminAPI.RebalanceStop, traceAllFlag))
+ }
+
+ // Profiling operations - deprecated API
+ adminRouter.Methods(http.MethodPost).Path(adminVersion+"/profiling/start").HandlerFunc(adminMiddleware(adminAPI.StartProfilingHandler, traceAllFlag, noObjLayerFlag)).
+ Queries("profilerType", "{profilerType:.*}")
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/profiling/download").HandlerFunc(adminMiddleware(adminAPI.DownloadProfilingHandler, traceHdrsS3HFlag, noObjLayerFlag))
+ // Profiling operations
+ adminRouter.Methods(http.MethodPost).Path(adminVersion + "/profile").HandlerFunc(adminMiddleware(adminAPI.ProfileHandler, traceHdrsS3HFlag, noObjLayerFlag))
+
+ // Config KV operations.
+ if enableConfigOps {
+ adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-config-kv").HandlerFunc(adminMiddleware(adminAPI.GetConfigKVHandler)).Queries("key", "{key:.*}")
+ adminRouter.Methods(http.MethodPut).Path(adminVersion + "/set-config-kv").HandlerFunc(adminMiddleware(adminAPI.SetConfigKVHandler))
+ adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/del-config-kv").HandlerFunc(adminMiddleware(adminAPI.DelConfigKVHandler))
+ }
+
+ // Enable config help in all modes.
+ adminRouter.Methods(http.MethodGet).Path(adminVersion+"/help-config-kv").HandlerFunc(adminMiddleware(adminAPI.HelpConfigKVHandler, traceAllFlag)).Queries("subSys", "{subSys:.*}", "key", "{key:.*}")
+
+ // Config KV history operations.
+ if enableConfigOps {
+ adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-config-history-kv").HandlerFunc(adminMiddleware(adminAPI.ListConfigHistoryKVHandler, traceAllFlag)).Queries("count", "{count:[0-9]+}")
+ adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/clear-config-history-kv").HandlerFunc(adminMiddleware(adminAPI.ClearConfigHistoryKVHandler)).Queries("restoreId", "{restoreId:.*}")
+ adminRouter.Methods(http.MethodPut).Path(adminVersion+"/restore-config-history-kv").HandlerFunc(adminMiddleware(adminAPI.RestoreConfigHistoryKVHandler)).Queries("restoreId", "{restoreId:.*}")
+ }
+
+ // Config import/export bulk operations
+ if enableConfigOps {
+ // Get config
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/config").HandlerFunc(adminMiddleware(adminAPI.GetConfigHandler))
+ // Set config
+ adminRouter.Methods(http.MethodPut).Path(adminVersion + "/config").HandlerFunc(adminMiddleware(adminAPI.SetConfigHandler))
+ }
+
+ // -- IAM APIs --
+
+ // Add policy IAM
+ adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-canned-policy").HandlerFunc(adminMiddleware(adminAPI.AddCannedPolicy, traceAllFlag)).Queries("name", "{name:.*}")
+
+ // Add user IAM
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/accountinfo").HandlerFunc(adminMiddleware(adminAPI.AccountInfoHandler, traceAllFlag))
+
+ adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-user").HandlerFunc(adminMiddleware(adminAPI.AddUser)).Queries("accessKey", "{accessKey:.*}")
+
+ adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-user-status").HandlerFunc(adminMiddleware(adminAPI.SetUserStatus)).Queries("accessKey", "{accessKey:.*}").Queries("status", "{status:.*}")
+
+ // Service accounts ops
+ adminRouter.Methods(http.MethodPut).Path(adminVersion + "/add-service-account").HandlerFunc(adminMiddleware(adminAPI.AddServiceAccount))
+ adminRouter.Methods(http.MethodPost).Path(adminVersion+"/update-service-account").HandlerFunc(adminMiddleware(adminAPI.UpdateServiceAccount)).Queries("accessKey", "{accessKey:.*}")
+ adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-service-account").HandlerFunc(adminMiddleware(adminAPI.InfoServiceAccount)).Queries("accessKey", "{accessKey:.*}")
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-service-accounts").HandlerFunc(adminMiddleware(adminAPI.ListServiceAccounts))
+ adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/delete-service-account").HandlerFunc(adminMiddleware(adminAPI.DeleteServiceAccount)).Queries("accessKey", "{accessKey:.*}")
+
+ // STS accounts ops
+ adminRouter.Methods(http.MethodGet).Path(adminVersion+"/temporary-account-info").HandlerFunc(adminMiddleware(adminAPI.TemporaryAccountInfo)).Queries("accessKey", "{accessKey:.*}")
+
+ // Access key (service account/STS) operations
+ adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-access-keys-bulk").HandlerFunc(adminMiddleware(adminAPI.ListAccessKeysBulk)).Queries("listType", "{listType:.*}")
+ adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-access-key").HandlerFunc(adminMiddleware(adminAPI.InfoAccessKey)).Queries("accessKey", "{accessKey:.*}")
+
+ // Info policy IAM latest
+ adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-canned-policy").HandlerFunc(adminMiddleware(adminAPI.InfoCannedPolicy)).Queries("name", "{name:.*}")
+ // List policies latest
+ adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-canned-policies").HandlerFunc(adminMiddleware(adminAPI.ListBucketPolicies)).Queries("bucket", "{bucket:.*}")
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-canned-policies").HandlerFunc(adminMiddleware(adminAPI.ListCannedPolicies))
+
+ // Builtin IAM policy associations
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp/builtin/policy-entities").HandlerFunc(adminMiddleware(adminAPI.ListPolicyMappingEntities))
+
+ // Remove policy IAM
+ adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-canned-policy").HandlerFunc(adminMiddleware(adminAPI.RemoveCannedPolicy)).Queries("name", "{name:.*}")
+
+ // Set user or group policy
+ adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-user-or-group-policy").
+ HandlerFunc(adminMiddleware(adminAPI.SetPolicyForUserOrGroup)).
+ Queries("policyName", "{policyName:.*}", "userOrGroup", "{userOrGroup:.*}", "isGroup", "{isGroup:true|false}")
+
+ // Attach/Detach policies to/from user or group
+ adminRouter.Methods(http.MethodPost).Path(adminVersion + "/idp/builtin/policy/{operation}").HandlerFunc(adminMiddleware(adminAPI.AttachDetachPolicyBuiltin))
+
+ // Remove user IAM
+ adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-user").HandlerFunc(adminMiddleware(adminAPI.RemoveUser)).Queries("accessKey", "{accessKey:.*}")
+
+ // List users
+ adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-users").HandlerFunc(adminMiddleware(adminAPI.ListBucketUsers)).Queries("bucket", "{bucket:.*}")
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-users").HandlerFunc(adminMiddleware(adminAPI.ListUsers))
+
+ // User info
+ adminRouter.Methods(http.MethodGet).Path(adminVersion+"/user-info").HandlerFunc(adminMiddleware(adminAPI.GetUserInfo)).Queries("accessKey", "{accessKey:.*}")
+ // Add/Remove members from group
+ adminRouter.Methods(http.MethodPut).Path(adminVersion + "/update-group-members").HandlerFunc(adminMiddleware(adminAPI.UpdateGroupMembers))
+
+ // Get Group
+ adminRouter.Methods(http.MethodGet).Path(adminVersion+"/group").HandlerFunc(adminMiddleware(adminAPI.GetGroup)).Queries("group", "{group:.*}")
+
+ // List Groups
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/groups").HandlerFunc(adminMiddleware(adminAPI.ListGroups))
+
+ // Set Group Status
+ adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-group-status").HandlerFunc(adminMiddleware(adminAPI.SetGroupStatus)).Queries("group", "{group:.*}").Queries("status", "{status:.*}")
+
+ // Export IAM info to zipped file
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/export-iam").HandlerFunc(adminMiddleware(adminAPI.ExportIAM, noGZFlag))
+
+ // Import IAM info
+ adminRouter.Methods(http.MethodPut).Path(adminVersion + "/import-iam").HandlerFunc(adminMiddleware(adminAPI.ImportIAM, noGZFlag))
+ adminRouter.Methods(http.MethodPut).Path(adminVersion + "/import-iam-v2").HandlerFunc(adminMiddleware(adminAPI.ImportIAMV2, noGZFlag))
+
+ // Identity Provider configuration APIs
+ adminRouter.Methods(http.MethodPut).Path(adminVersion + "/idp-config/{type}/{name}").HandlerFunc(adminMiddleware(adminAPI.AddIdentityProviderCfg))
+ adminRouter.Methods(http.MethodPost).Path(adminVersion + "/idp-config/{type}/{name}").HandlerFunc(adminMiddleware(adminAPI.UpdateIdentityProviderCfg))
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp-config/{type}").HandlerFunc(adminMiddleware(adminAPI.ListIdentityProviderCfg))
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp-config/{type}/{name}").HandlerFunc(adminMiddleware(adminAPI.GetIdentityProviderCfg))
+ adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/idp-config/{type}/{name}").HandlerFunc(adminMiddleware(adminAPI.DeleteIdentityProviderCfg))
+
+ // LDAP specific service accounts ops
+ adminRouter.Methods(http.MethodPut).Path(adminVersion + "/idp/ldap/add-service-account").HandlerFunc(adminMiddleware(adminAPI.AddServiceAccountLDAP))
+ adminRouter.Methods(http.MethodGet).Path(adminVersion+"/idp/ldap/list-access-keys").
+ HandlerFunc(adminMiddleware(adminAPI.ListAccessKeysLDAP)).Queries("userDN", "{userDN:.*}", "listType", "{listType:.*}")
+ adminRouter.Methods(http.MethodGet).Path(adminVersion+"/idp/ldap/list-access-keys-bulk").
+ HandlerFunc(adminMiddleware(adminAPI.ListAccessKeysLDAPBulk)).Queries("listType", "{listType:.*}")
+
+ // LDAP IAM operations
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp/ldap/policy-entities").HandlerFunc(adminMiddleware(adminAPI.ListLDAPPolicyMappingEntities))
+ adminRouter.Methods(http.MethodPost).Path(adminVersion + "/idp/ldap/policy/{operation}").HandlerFunc(adminMiddleware(adminAPI.AttachDetachPolicyLDAP))
+
+ // OpenID specific service accounts ops
+ adminRouter.Methods(http.MethodGet).Path(adminVersion+"/idp/openid/list-access-keys-bulk").
+ HandlerFunc(adminMiddleware(adminAPI.ListAccessKeysOpenIDBulk)).Queries("listType", "{listType:.*}")
+
+ // -- END IAM APIs --
+
+ // GetBucketQuotaConfig
+ adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-bucket-quota").HandlerFunc(
+ adminMiddleware(adminAPI.GetBucketQuotaConfigHandler)).Queries("bucket", "{bucket:.*}")
+ // PutBucketQuotaConfig
+ adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-bucket-quota").HandlerFunc(
+ adminMiddleware(adminAPI.PutBucketQuotaConfigHandler)).Queries("bucket", "{bucket:.*}")
+
+ // Bucket replication operations
+ // GetBucketTargetHandler
+ adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-remote-targets").HandlerFunc(
+ adminMiddleware(adminAPI.ListRemoteTargetsHandler)).Queries("bucket", "{bucket:.*}", "type", "{type:.*}")
+ // SetRemoteTargetHandler
+ adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-remote-target").HandlerFunc(
+ adminMiddleware(adminAPI.SetRemoteTargetHandler)).Queries("bucket", "{bucket:.*}")
+ // RemoveRemoteTargetHandler
+ adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-remote-target").HandlerFunc(
+ adminMiddleware(adminAPI.RemoveRemoteTargetHandler)).Queries("bucket", "{bucket:.*}", "arn", "{arn:.*}")
+ // ReplicationDiff - MinIO extension API
+ adminRouter.Methods(http.MethodPost).Path(adminVersion+"/replication/diff").HandlerFunc(
+ adminMiddleware(adminAPI.ReplicationDiffHandler)).Queries("bucket", "{bucket:.*}")
+ // ReplicationMRFHandler - MinIO extension API
+ adminRouter.Methods(http.MethodGet).Path(adminVersion+"/replication/mrf").HandlerFunc(
+ adminMiddleware(adminAPI.ReplicationMRFHandler)).Queries("bucket", "{bucket:.*}")
+
+ // Batch job operations
+ adminRouter.Methods(http.MethodPost).Path(adminVersion + "/start-job").HandlerFunc(
+ adminMiddleware(adminAPI.StartBatchJob))
+
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-jobs").HandlerFunc(
+ adminMiddleware(adminAPI.ListBatchJobs))
+
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/status-job").HandlerFunc(
+ adminMiddleware(adminAPI.BatchJobStatus))
+
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/describe-job").HandlerFunc(
+ adminMiddleware(adminAPI.DescribeBatchJob))
+ adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/cancel-job").HandlerFunc(
+ adminMiddleware(adminAPI.CancelBatchJob))
+
+ // Bucket migration operations
+ // ExportBucketMetaHandler
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/export-bucket-metadata").HandlerFunc(
+ adminMiddleware(adminAPI.ExportBucketMetadataHandler))
+ // ImportBucketMetaHandler
+ adminRouter.Methods(http.MethodPut).Path(adminVersion + "/import-bucket-metadata").HandlerFunc(
+ adminMiddleware(adminAPI.ImportBucketMetadataHandler))
+
+ // Remote Tier management operations
+ adminRouter.Methods(http.MethodPut).Path(adminVersion + "/tier").HandlerFunc(adminMiddleware(adminAPI.AddTierHandler))
+ adminRouter.Methods(http.MethodPost).Path(adminVersion + "/tier/{tier}").HandlerFunc(adminMiddleware(adminAPI.EditTierHandler))
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier").HandlerFunc(adminMiddleware(adminAPI.ListTierHandler))
+ adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/tier/{tier}").HandlerFunc(adminMiddleware(adminAPI.RemoveTierHandler))
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier/{tier}").HandlerFunc(adminMiddleware(adminAPI.VerifyTierHandler))
+ // Tier stats
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier-stats").HandlerFunc(adminMiddleware(adminAPI.TierStatsHandler))
+
+ // Cluster Replication APIs
+ adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/add").HandlerFunc(adminMiddleware(adminAPI.SiteReplicationAdd))
+ adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/remove").HandlerFunc(adminMiddleware(adminAPI.SiteReplicationRemove))
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/info").HandlerFunc(adminMiddleware(adminAPI.SiteReplicationInfo))
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/metainfo").HandlerFunc(adminMiddleware(adminAPI.SiteReplicationMetaInfo))
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/status").HandlerFunc(adminMiddleware(adminAPI.SiteReplicationStatus))
+ adminRouter.Methods(http.MethodPost).Path(adminVersion + adminAPISiteReplicationDevNull).HandlerFunc(adminMiddleware(adminAPI.SiteReplicationDevNull, noObjLayerFlag))
+ adminRouter.Methods(http.MethodPost).Path(adminVersion + adminAPISiteReplicationNetPerf).HandlerFunc(adminMiddleware(adminAPI.SiteReplicationNetPerf, noObjLayerFlag))
+
+ adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/join").HandlerFunc(adminMiddleware(adminAPI.SRPeerJoin))
+ adminRouter.Methods(http.MethodPut).Path(adminVersion+"/site-replication/peer/bucket-ops").HandlerFunc(adminMiddleware(adminAPI.SRPeerBucketOps)).Queries("bucket", "{bucket:.*}").Queries("operation", "{operation:.*}")
+ adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/iam-item").HandlerFunc(adminMiddleware(adminAPI.SRPeerReplicateIAMItem))
+ adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/bucket-meta").HandlerFunc(adminMiddleware(adminAPI.SRPeerReplicateBucketItem))
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/peer/idp-settings").HandlerFunc(adminMiddleware(adminAPI.SRPeerGetIDPSettings))
+ adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/edit").HandlerFunc(adminMiddleware(adminAPI.SiteReplicationEdit))
+ adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/edit").HandlerFunc(adminMiddleware(adminAPI.SRPeerEdit))
+ adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/remove").HandlerFunc(adminMiddleware(adminAPI.SRPeerRemove))
+ adminRouter.Methods(http.MethodPut).Path(adminVersion+"/site-replication/resync/op").HandlerFunc(adminMiddleware(adminAPI.SiteReplicationResyncOp)).Queries("operation", "{operation:.*}")
+ adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/state/edit").HandlerFunc(adminMiddleware(adminAPI.SRStateEdit))
+
+ if globalIsDistErasure {
+ // Top locks
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/top/locks").HandlerFunc(adminMiddleware(adminAPI.TopLocksHandler))
+ // Force unlocks paths
+ adminRouter.Methods(http.MethodPost).Path(adminVersion+"/force-unlock").
+ Queries("paths", "{paths:.*}").HandlerFunc(adminMiddleware(adminAPI.ForceUnlockHandler))
+ }
+
+ adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest").HandlerFunc(adminMiddleware(adminAPI.ObjectSpeedTestHandler, noGZFlag))
+ adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/object").HandlerFunc(adminMiddleware(adminAPI.ObjectSpeedTestHandler, noGZFlag))
+ adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/drive").HandlerFunc(adminMiddleware(adminAPI.DriveSpeedtestHandler, noGZFlag))
+ adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/net").HandlerFunc(adminMiddleware(adminAPI.NetperfHandler, noGZFlag))
+ adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/site").HandlerFunc(adminMiddleware(adminAPI.SitePerfHandler, noGZFlag))
+ adminRouter.Methods(http.MethodPost).Path(adminVersion + adminAPIClientDevNull).HandlerFunc(adminMiddleware(adminAPI.ClientDevNull, noGZFlag))
+ adminRouter.Methods(http.MethodPost).Path(adminVersion + adminAPIClientDevExtraTime).HandlerFunc(adminMiddleware(adminAPI.ClientDevNullExtraTime, noGZFlag))
+
+ // HTTP Trace
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/trace").HandlerFunc(adminMiddleware(adminAPI.TraceHandler, noObjLayerFlag))
+
+ // Console Logs
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/log").HandlerFunc(adminMiddleware(adminAPI.ConsoleLogHandler, traceAllFlag))
+
+ // -- KMS APIs --
+ //
+ adminRouter.Methods(http.MethodPost).Path(adminVersion + "/kms/status").HandlerFunc(adminMiddleware(adminAPI.KMSStatusHandler, traceAllFlag))
+ adminRouter.Methods(http.MethodPost).Path(adminVersion+"/kms/key/create").HandlerFunc(adminMiddleware(adminAPI.KMSCreateKeyHandler, traceAllFlag)).Queries("key-id", "{key-id:.*}")
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/kms/key/status").HandlerFunc(adminMiddleware(adminAPI.KMSKeyStatusHandler, traceAllFlag))
+
+ // Keep obdinfo for backward compatibility with mc
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/obdinfo").
+ HandlerFunc(adminMiddleware(adminAPI.HealthInfoHandler))
+ // -- Health API --
+ adminRouter.Methods(http.MethodGet).Path(adminVersion + "/healthinfo").
+ 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
+ adminRouter.NotFoundHandler = httpTraceAll(errorResponseHandler)
+ adminRouter.MethodNotAllowedHandler = httpTraceAll(methodNotAllowedHandler("Admin"))
+}
diff --git a/cmd/admin-server-info.go b/cmd/admin-server-info.go
new file mode 100644
index 00000000..4a98f9ba
--- /dev/null
+++ b/cmd/admin-server-info.go
@@ -0,0 +1,171 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "math"
+ "net/http"
+ "os"
+ "runtime"
+ "runtime/debug"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/config"
+ "github.com/minio/minio/internal/kms"
+ xnet "github.com/minio/pkg/v3/net"
+)
+
+// getLocalServerProperty - returns madmin.ServerProperties for only the
+// local endpoints from given list of endpoints
+func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Request, metrics bool) madmin.ServerProperties {
+ addr := globalLocalNodeName
+ if r != nil {
+ addr = r.Host
+ }
+ if globalIsDistErasure {
+ addr = globalLocalNodeName
+ }
+ poolNumbers := make(map[int]struct{})
+ network := make(map[string]string)
+ for _, ep := range endpointServerPools {
+ for _, endpoint := range ep.Endpoints {
+ if endpoint.IsLocal {
+ poolNumbers[endpoint.PoolIdx+1] = struct{}{}
+ }
+ nodeName := endpoint.Host
+ if nodeName == "" {
+ nodeName = addr
+ }
+ if endpoint.IsLocal {
+ // Only proceed for local endpoints
+ network[nodeName] = string(madmin.ItemOnline)
+ continue
+ }
+ _, present := network[nodeName]
+ if !present {
+ if err := isServerResolvable(endpoint, 5*time.Second); err == nil {
+ network[nodeName] = string(madmin.ItemOnline)
+ } else {
+ if xnet.IsNetworkOrHostDown(err, false) {
+ network[nodeName] = string(madmin.ItemOffline)
+ } else if xnet.IsNetworkOrHostDown(err, true) {
+ network[nodeName] = "connection attempt timedout"
+ }
+ }
+ }
+ }
+ }
+
+ var memstats runtime.MemStats
+ runtime.ReadMemStats(&memstats)
+
+ gcStats := debug.GCStats{
+ // If stats.PauseQuantiles is non-empty, ReadGCStats fills
+ // it with quantiles summarizing the distribution of pause time.
+ // For example, if len(stats.PauseQuantiles) is 5, it will be
+ // filled with the minimum, 25%, 50%, 75%, and maximum pause times.
+ PauseQuantiles: make([]time.Duration, 5),
+ }
+ debug.ReadGCStats(&gcStats)
+ // Truncate GC stats to max 5 entries.
+ if len(gcStats.PauseEnd) > 5 {
+ gcStats.PauseEnd = gcStats.PauseEnd[len(gcStats.PauseEnd)-5:]
+ }
+ if len(gcStats.Pause) > 5 {
+ gcStats.Pause = gcStats.Pause[len(gcStats.Pause)-5:]
+ }
+
+ props := madmin.ServerProperties{
+ Endpoint: addr,
+ Uptime: UTCNow().Unix() - globalBootTime.Unix(),
+ Version: Version,
+ CommitID: CommitID,
+ Network: network,
+ MemStats: madmin.MemStats{
+ Alloc: memstats.Alloc,
+ TotalAlloc: memstats.TotalAlloc,
+ Mallocs: memstats.Mallocs,
+ Frees: memstats.Frees,
+ HeapAlloc: memstats.HeapAlloc,
+ },
+ GoMaxProcs: runtime.GOMAXPROCS(0),
+ NumCPU: runtime.NumCPU(),
+ RuntimeVersion: runtime.Version(),
+ GCStats: &madmin.GCStats{
+ LastGC: gcStats.LastGC,
+ NumGC: gcStats.NumGC,
+ PauseTotal: gcStats.PauseTotal,
+ Pause: gcStats.Pause,
+ PauseEnd: gcStats.PauseEnd,
+ },
+ MinioEnvVars: make(map[string]string, 10),
+ }
+
+ for poolNumber := range poolNumbers {
+ props.PoolNumbers = append(props.PoolNumbers, poolNumber)
+ }
+ sort.Ints(props.PoolNumbers)
+ props.PoolNumber = func() int {
+ if len(props.PoolNumbers) == 1 {
+ return props.PoolNumbers[0]
+ }
+ return math.MaxInt // this indicates that its unset.
+ }()
+
+ sensitive := map[string]struct{}{
+ config.EnvAccessKey: {},
+ config.EnvSecretKey: {},
+ config.EnvRootUser: {},
+ config.EnvRootPassword: {},
+ config.EnvMinIOSubnetAPIKey: {},
+ kms.EnvKMSSecretKey: {},
+ }
+ for _, v := range os.Environ() {
+ if !strings.HasPrefix(v, "MINIO") && !strings.HasPrefix(v, "_MINIO") {
+ continue
+ }
+ split := strings.SplitN(v, "=", 2)
+ key := split[0]
+ value := ""
+ if len(split) > 1 {
+ value = split[1]
+ }
+
+ // Do not send sensitive creds.
+ if _, ok := sensitive[key]; ok || strings.Contains(strings.ToLower(key), "password") || strings.HasSuffix(strings.ToLower(key), "key") {
+ props.MinioEnvVars[key] = "*** EXISTS, REDACTED ***"
+ continue
+ }
+ props.MinioEnvVars[key] = value
+ }
+
+ objLayer := newObjectLayerFn()
+ if objLayer != nil {
+ storageInfo := objLayer.LocalStorageInfo(GlobalContext, metrics)
+ props.State = string(madmin.ItemOnline)
+ props.Disks = storageInfo.Disks
+ } else {
+ props.State = string(madmin.ItemInitializing)
+ props.Disks = getOfflineDisks("", globalEndpoints)
+ }
+
+ return props
+}
diff --git a/cmd/api-datatypes.go b/cmd/api-datatypes.go
new file mode 100644
index 00000000..cc3bcb1c
--- /dev/null
+++ b/cmd/api-datatypes.go
@@ -0,0 +1,85 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "encoding/xml"
+ "time"
+)
+
+// DeletedObject objects deleted
+type DeletedObject struct {
+ DeleteMarker bool `xml:"DeleteMarker,omitempty"`
+ DeleteMarkerVersionID string `xml:"DeleteMarkerVersionId,omitempty"`
+ ObjectName string `xml:"Key,omitempty"`
+ VersionID string `xml:"VersionId,omitempty"`
+ // MTime of DeleteMarker on source that needs to be propagated to replica
+ DeleteMarkerMTime DeleteMarkerMTime `xml:"-"`
+ // MinIO extensions to support delete marker replication
+ ReplicationState ReplicationState `xml:"-"`
+
+ found bool // the object was found during deletion
+}
+
+// DeleteMarkerMTime is an embedded type containing time.Time for XML marshal
+type DeleteMarkerMTime struct {
+ time.Time
+}
+
+// MarshalXML encodes expiration date if it is non-zero and encodes
+// empty string otherwise
+func (t DeleteMarkerMTime) MarshalXML(e *xml.Encoder, startElement xml.StartElement) error {
+ if t.IsZero() {
+ return nil
+ }
+ return e.EncodeElement(t.Format(time.RFC3339), startElement)
+}
+
+// ObjectV object version key/versionId
+type ObjectV struct {
+ ObjectName string `xml:"Key"`
+ VersionID string `xml:"VersionId"`
+}
+
+// ObjectToDelete carries key name for the object to delete.
+type ObjectToDelete struct {
+ ObjectV
+ // Replication status of DeleteMarker
+ DeleteMarkerReplicationStatus string `xml:"DeleteMarkerReplicationStatus"`
+ // Status of versioned delete (of object or DeleteMarker)
+ VersionPurgeStatus VersionPurgeStatusType `xml:"VersionPurgeStatus"`
+ // VersionPurgeStatuses holds the internal
+ VersionPurgeStatuses string `xml:"VersionPurgeStatuses"`
+ // ReplicateDecisionStr stringified representation of replication decision
+ ReplicateDecisionStr string `xml:"-"`
+}
+
+// createBucketLocationConfiguration container for bucket configuration request from client.
+// Used for parsing the location from the request body for Makebucket.
+type createBucketLocationConfiguration struct {
+ XMLName xml.Name `xml:"CreateBucketConfiguration" json:"-"`
+ Location string `xml:"LocationConstraint"`
+}
+
+// DeleteObjectsRequest - xml carrying the object key names which needs to be deleted.
+type DeleteObjectsRequest struct {
+ // Element to enable quiet mode for the request
+ Quiet bool
+ // List of objects to be deleted
+ Objects []ObjectToDelete `xml:"Object"`
+}
diff --git a/cmd/api-errors.go b/cmd/api-errors.go
new file mode 100644
index 00000000..6ccd5fab
--- /dev/null
+++ b/cmd/api-errors.go
@@ -0,0 +1,2639 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+ "os"
+ "strconv"
+ "strings"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore"
+ "github.com/minio/minio/internal/ioutil"
+ "google.golang.org/api/googleapi"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio-go/v7"
+ "github.com/minio/minio-go/v7/pkg/tags"
+ "github.com/minio/minio/internal/auth"
+ "github.com/minio/minio/internal/bucket/lifecycle"
+ "github.com/minio/minio/internal/bucket/replication"
+ "github.com/minio/minio/internal/config/dns"
+ "github.com/minio/minio/internal/crypto"
+ "github.com/minio/minio/internal/kms"
+ "github.com/minio/minio/internal/logger"
+
+ objectlock "github.com/minio/minio/internal/bucket/object/lock"
+ "github.com/minio/minio/internal/bucket/versioning"
+ levent "github.com/minio/minio/internal/config/lambda/event"
+ "github.com/minio/minio/internal/event"
+ "github.com/minio/minio/internal/hash"
+ "github.com/minio/pkg/v3/policy"
+)
+
+// APIError structure
+type APIError struct {
+ Code string
+ Description string
+ HTTPStatusCode int
+ ObjectSize string
+ RangeRequested string
+}
+
+// APIErrorResponse - error response format
+type APIErrorResponse struct {
+ XMLName xml.Name `xml:"Error" json:"-"`
+ Code string
+ Message string
+ Key string `xml:"Key,omitempty" json:"Key,omitempty"`
+ BucketName string `xml:"BucketName,omitempty" json:"BucketName,omitempty"`
+ Resource string
+ Region string `xml:"Region,omitempty" json:"Region,omitempty"`
+ RequestID string `xml:"RequestId" json:"RequestId"`
+ HostID string `xml:"HostId" json:"HostId"`
+ ActualObjectSize string `xml:"ActualObjectSize,omitempty" json:"ActualObjectSize,omitempty"`
+ RangeRequested string `xml:"RangeRequested,omitempty" json:"RangeRequested,omitempty"`
+}
+
+// APIErrorCode type of error status.
+type APIErrorCode int
+
+//go:generate stringer -type=APIErrorCode -trimprefix=Err $GOFILE
+
+// Error codes, non exhaustive list - http://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html
+const (
+ ErrNone APIErrorCode = iota
+ ErrAccessDenied
+ ErrBadDigest
+ ErrEntityTooSmall
+ ErrEntityTooLarge
+ ErrPolicyTooLarge
+ ErrIncompleteBody
+ ErrInternalError
+ ErrInvalidAccessKeyID
+ ErrAccessKeyDisabled
+ ErrInvalidArgument
+ ErrInvalidBucketName
+ ErrInvalidDigest
+ ErrInvalidRange
+ ErrInvalidRangePartNumber
+ ErrInvalidCopyPartRange
+ ErrInvalidCopyPartRangeSource
+ ErrInvalidMaxKeys
+ ErrInvalidEncodingMethod
+ ErrInvalidMaxUploads
+ ErrInvalidMaxParts
+ ErrInvalidPartNumberMarker
+ ErrInvalidPartNumber
+ ErrInvalidRequestBody
+ ErrInvalidCopySource
+ ErrInvalidMetadataDirective
+ ErrInvalidCopyDest
+ ErrInvalidPolicyDocument
+ ErrInvalidObjectState
+ ErrMalformedXML
+ ErrMissingContentLength
+ ErrMissingContentMD5
+ ErrMissingRequestBodyError
+ ErrMissingSecurityHeader
+ ErrNoSuchBucket
+ ErrNoSuchBucketPolicy
+ ErrNoSuchBucketLifecycle
+ ErrNoSuchLifecycleConfiguration
+ ErrInvalidLifecycleWithObjectLock
+ ErrNoSuchBucketSSEConfig
+ ErrNoSuchCORSConfiguration
+ ErrNoSuchWebsiteConfiguration
+ ErrReplicationConfigurationNotFoundError
+ ErrRemoteDestinationNotFoundError
+ ErrReplicationDestinationMissingLock
+ ErrRemoteTargetNotFoundError
+ ErrReplicationRemoteConnectionError
+ ErrReplicationBandwidthLimitError
+ ErrBucketRemoteIdenticalToSource
+ ErrBucketRemoteAlreadyExists
+ ErrBucketRemoteLabelInUse
+ ErrBucketRemoteArnTypeInvalid
+ ErrBucketRemoteArnInvalid
+ ErrBucketRemoteRemoveDisallowed
+ ErrRemoteTargetNotVersionedError
+ ErrReplicationSourceNotVersionedError
+ ErrReplicationNeedsVersioningError
+ ErrReplicationBucketNeedsVersioningError
+ ErrReplicationDenyEditError
+ ErrRemoteTargetDenyAddError
+ ErrReplicationNoExistingObjects
+ ErrReplicationValidationError
+ ErrReplicationPermissionCheckError
+ ErrObjectRestoreAlreadyInProgress
+ ErrNoSuchKey
+ ErrNoSuchUpload
+ ErrInvalidVersionID
+ ErrNoSuchVersion
+ ErrNotImplemented
+ ErrPreconditionFailed
+ ErrRequestTimeTooSkewed
+ ErrSignatureDoesNotMatch
+ ErrMethodNotAllowed
+ ErrInvalidPart
+ ErrInvalidPartOrder
+ ErrMissingPart
+ ErrAuthorizationHeaderMalformed
+ ErrMalformedPOSTRequest
+ ErrPOSTFileRequired
+ ErrSignatureVersionNotSupported
+ ErrBucketNotEmpty
+ ErrAllAccessDisabled
+ ErrPolicyInvalidVersion
+ ErrMissingFields
+ ErrMissingCredTag
+ ErrCredMalformed
+ ErrInvalidRegion
+ ErrInvalidServiceS3
+ ErrInvalidServiceSTS
+ ErrInvalidRequestVersion
+ ErrMissingSignTag
+ ErrMissingSignHeadersTag
+ ErrMalformedDate
+ ErrMalformedPresignedDate
+ ErrMalformedCredentialDate
+ ErrMalformedExpires
+ ErrNegativeExpires
+ ErrAuthHeaderEmpty
+ ErrExpiredPresignRequest
+ ErrRequestNotReadyYet
+ ErrUnsignedHeaders
+ ErrMissingDateHeader
+ ErrInvalidQuerySignatureAlgo
+ ErrInvalidQueryParams
+ ErrBucketAlreadyOwnedByYou
+ ErrInvalidDuration
+ ErrBucketAlreadyExists
+ ErrMetadataTooLarge
+ ErrUnsupportedMetadata
+ ErrUnsupportedHostHeader
+ ErrMaximumExpires
+ ErrSlowDownRead
+ ErrSlowDownWrite
+ ErrMaxVersionsExceeded
+ ErrInvalidPrefixMarker
+ ErrBadRequest
+ ErrKeyTooLongError
+ ErrInvalidBucketObjectLockConfiguration
+ ErrObjectLockConfigurationNotFound
+ ErrObjectLockConfigurationNotAllowed
+ ErrNoSuchObjectLockConfiguration
+ ErrObjectLocked
+ ErrInvalidRetentionDate
+ ErrPastObjectLockRetainDate
+ ErrUnknownWORMModeDirective
+ ErrBucketTaggingNotFound
+ ErrObjectLockInvalidHeaders
+ ErrInvalidTagDirective
+ ErrPolicyAlreadyAttached
+ ErrPolicyNotAttached
+ ErrExcessData
+ ErrPolicyInvalidName
+ ErrNoTokenRevokeType
+ ErrAdminOpenIDNotEnabled
+ ErrAdminNoSuchAccessKey
+ // Add new error codes here.
+
+ // SSE-S3/SSE-KMS related API errors
+ ErrInvalidEncryptionMethod
+ ErrInvalidEncryptionKeyID
+
+ // Server-Side-Encryption (with Customer provided key) related API errors.
+ ErrInsecureSSECustomerRequest
+ ErrSSEMultipartEncrypted
+ ErrSSEEncryptedObject
+ ErrInvalidEncryptionParameters
+ ErrInvalidEncryptionParametersSSEC
+
+ ErrInvalidSSECustomerAlgorithm
+ ErrInvalidSSECustomerKey
+ ErrMissingSSECustomerKey
+ ErrMissingSSECustomerKeyMD5
+ ErrSSECustomerKeyMD5Mismatch
+ ErrInvalidSSECustomerParameters
+ ErrIncompatibleEncryptionMethod
+ ErrKMSNotConfigured
+ ErrKMSKeyNotFoundException
+ ErrKMSDefaultKeyAlreadyConfigured
+
+ ErrNoAccessKey
+ ErrInvalidToken
+
+ // Bucket notification related errors.
+ ErrEventNotification
+ ErrARNNotification
+ ErrRegionNotification
+ ErrOverlappingFilterNotification
+ ErrFilterNameInvalid
+ ErrFilterNamePrefix
+ ErrFilterNameSuffix
+ ErrFilterValueInvalid
+ ErrOverlappingConfigs
+ ErrUnsupportedNotification
+
+ // S3 extended errors.
+ ErrContentSHA256Mismatch
+ ErrContentChecksumMismatch
+
+ // Add new extended error codes here.
+
+ // MinIO extended errors.
+ ErrStorageFull
+ ErrRequestBodyParse
+ ErrObjectExistsAsDirectory
+ ErrInvalidObjectName
+ ErrInvalidObjectNamePrefixSlash
+ ErrInvalidResourceName
+ ErrInvalidLifecycleQueryParameter
+ ErrServerNotInitialized
+ ErrBucketMetadataNotInitialized
+ ErrRequestTimedout
+ ErrClientDisconnected
+ ErrTooManyRequests
+ ErrInvalidRequest
+ ErrTransitionStorageClassNotFoundError
+ // MinIO storage class error codes
+ ErrInvalidStorageClass
+ ErrBackendDown
+ // Add new extended error codes here.
+ // Please open a https://github.com/minio/minio/issues before adding
+ // new error codes here.
+
+ ErrMalformedJSON
+ ErrAdminNoSuchUser
+ ErrAdminNoSuchUserLDAPWarn
+ ErrAdminLDAPExpectedLoginName
+ ErrAdminNoSuchGroup
+ ErrAdminGroupNotEmpty
+ ErrAdminGroupDisabled
+ ErrAdminInvalidGroupName
+ ErrAdminNoSuchJob
+ ErrAdminNoSuchPolicy
+ ErrAdminPolicyChangeAlreadyApplied
+ ErrAdminInvalidArgument
+ ErrAdminInvalidAccessKey
+ ErrAdminInvalidSecretKey
+ ErrAdminConfigNoQuorum
+ ErrAdminConfigTooLarge
+ ErrAdminConfigBadJSON
+ ErrAdminNoSuchConfigTarget
+ ErrAdminConfigEnvOverridden
+ ErrAdminConfigDuplicateKeys
+ ErrAdminConfigInvalidIDPType
+ ErrAdminConfigLDAPNonDefaultConfigName
+ ErrAdminConfigLDAPValidation
+ ErrAdminConfigIDPCfgNameAlreadyExists
+ ErrAdminConfigIDPCfgNameDoesNotExist
+ ErrInsecureClientRequest
+ ErrObjectTampered
+ ErrAdminLDAPNotEnabled
+
+ // Site-Replication errors
+ ErrSiteReplicationInvalidRequest
+ ErrSiteReplicationPeerResp
+ ErrSiteReplicationBackendIssue
+ ErrSiteReplicationServiceAccountError
+ ErrSiteReplicationBucketConfigError
+ ErrSiteReplicationBucketMetaError
+ ErrSiteReplicationIAMError
+ ErrSiteReplicationConfigMissing
+ ErrSiteReplicationIAMConfigMismatch
+
+ // Pool rebalance errors
+ ErrAdminRebalanceAlreadyStarted
+ ErrAdminRebalanceNotStarted
+
+ // Bucket Quota error codes
+ ErrAdminBucketQuotaExceeded
+ ErrAdminNoSuchQuotaConfiguration
+
+ ErrHealNotImplemented
+ ErrHealNoSuchProcess
+ ErrHealInvalidClientToken
+ ErrHealMissingBucket
+ ErrHealAlreadyRunning
+ ErrHealOverlappingPaths
+ ErrIncorrectContinuationToken
+
+ // S3 Select Errors
+ ErrEmptyRequestBody
+ ErrUnsupportedFunction
+ ErrInvalidExpressionType
+ ErrBusy
+ ErrUnauthorizedAccess
+ ErrExpressionTooLong
+ ErrIllegalSQLFunctionArgument
+ ErrInvalidKeyPath
+ ErrInvalidCompressionFormat
+ ErrInvalidFileHeaderInfo
+ ErrInvalidJSONType
+ ErrInvalidQuoteFields
+ ErrInvalidRequestParameter
+ ErrInvalidDataType
+ ErrInvalidTextEncoding
+ ErrInvalidDataSource
+ ErrInvalidTableAlias
+ ErrMissingRequiredParameter
+ ErrObjectSerializationConflict
+ ErrUnsupportedSQLOperation
+ ErrUnsupportedSQLStructure
+ ErrUnsupportedSyntax
+ ErrUnsupportedRangeHeader
+ ErrLexerInvalidChar
+ ErrLexerInvalidOperator
+ ErrLexerInvalidLiteral
+ ErrLexerInvalidIONLiteral
+ ErrParseExpectedDatePart
+ ErrParseExpectedKeyword
+ ErrParseExpectedTokenType
+ ErrParseExpected2TokenTypes
+ ErrParseExpectedNumber
+ ErrParseExpectedRightParenBuiltinFunctionCall
+ ErrParseExpectedTypeName
+ ErrParseExpectedWhenClause
+ ErrParseUnsupportedToken
+ ErrParseUnsupportedLiteralsGroupBy
+ ErrParseExpectedMember
+ ErrParseUnsupportedSelect
+ ErrParseUnsupportedCase
+ ErrParseUnsupportedCaseClause
+ ErrParseUnsupportedAlias
+ ErrParseUnsupportedSyntax
+ ErrParseUnknownOperator
+ ErrParseMissingIdentAfterAt
+ ErrParseUnexpectedOperator
+ ErrParseUnexpectedTerm
+ ErrParseUnexpectedToken
+ ErrParseUnexpectedKeyword
+ ErrParseExpectedExpression
+ ErrParseExpectedLeftParenAfterCast
+ ErrParseExpectedLeftParenValueConstructor
+ ErrParseExpectedLeftParenBuiltinFunctionCall
+ ErrParseExpectedArgumentDelimiter
+ ErrParseCastArity
+ ErrParseInvalidTypeParam
+ ErrParseEmptySelect
+ ErrParseSelectMissingFrom
+ ErrParseExpectedIdentForGroupName
+ ErrParseExpectedIdentForAlias
+ ErrParseUnsupportedCallWithStar
+ ErrParseNonUnaryAggregateFunctionCall
+ ErrParseMalformedJoin
+ ErrParseExpectedIdentForAt
+ ErrParseAsteriskIsNotAloneInSelectList
+ ErrParseCannotMixSqbAndWildcardInSelectList
+ ErrParseInvalidContextForWildcardInSelectList
+ ErrIncorrectSQLFunctionArgumentType
+ ErrValueParseFailure
+ ErrEvaluatorInvalidArguments
+ ErrIntegerOverflow
+ ErrLikeInvalidInputs
+ ErrCastFailed
+ ErrInvalidCast
+ ErrEvaluatorInvalidTimestampFormatPattern
+ ErrEvaluatorInvalidTimestampFormatPatternSymbolForParsing
+ ErrEvaluatorTimestampFormatPatternDuplicateFields
+ ErrEvaluatorTimestampFormatPatternHourClockAmPmMismatch
+ ErrEvaluatorUnterminatedTimestampFormatPatternToken
+ ErrEvaluatorInvalidTimestampFormatPatternToken
+ ErrEvaluatorInvalidTimestampFormatPatternSymbol
+ ErrEvaluatorBindingDoesNotExist
+ ErrMissingHeaders
+ ErrInvalidColumnIndex
+
+ ErrAdminConfigNotificationTargetsFailed
+ ErrAdminProfilerNotEnabled
+ ErrInvalidDecompressedSize
+ ErrAddUserInvalidArgument
+ ErrAddUserValidUTF
+ ErrAdminResourceInvalidArgument
+ ErrAdminAccountNotEligible
+ ErrAccountNotEligible
+ ErrAdminServiceAccountNotFound
+ ErrPostPolicyConditionInvalidFormat
+
+ ErrInvalidChecksum
+
+ // Lambda functions
+ ErrLambdaARNInvalid
+ ErrLambdaARNNotFound
+
+ // New Codes for GetObjectAttributes and GetObjectVersionAttributes
+ ErrInvalidAttributeName
+
+ ErrAdminNoAccessKey
+ ErrAdminNoSecretKey
+
+ ErrIAMNotInitialized
+
+ apiErrCodeEnd // This is used only for the testing code
+)
+
+type errorCodeMap map[APIErrorCode]APIError
+
+func (e errorCodeMap) ToAPIErrWithErr(errCode APIErrorCode, err error) APIError {
+ apiErr, ok := e[errCode]
+ if !ok {
+ apiErr = e[ErrInternalError]
+ }
+ if err != nil {
+ apiErr.Description = fmt.Sprintf("%s (%s)", apiErr.Description, err)
+ }
+ if region := globalSite.Region(); region != "" {
+ if errCode == ErrAuthorizationHeaderMalformed {
+ apiErr.Description = fmt.Sprintf("The authorization header is malformed; the region is wrong; expecting '%s'.", region)
+ return apiErr
+ }
+ }
+ return apiErr
+}
+
+func (e errorCodeMap) ToAPIErr(errCode APIErrorCode) APIError {
+ return e.ToAPIErrWithErr(errCode, nil)
+}
+
+// error code to APIError structure, these fields carry respective
+// descriptions for all the error responses.
+var errorCodes = errorCodeMap{
+ ErrInvalidCopyDest: {
+ Code: "InvalidRequest",
+ Description: "This copy request is illegal because it is trying to copy an object to itself without changing the object's metadata, storage class, website redirect location or encryption attributes.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidCopySource: {
+ Code: "InvalidArgument",
+ Description: "Copy Source must mention the source bucket and key: sourcebucket/sourcekey.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidMetadataDirective: {
+ Code: "InvalidArgument",
+ Description: "Unknown metadata directive.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidStorageClass: {
+ Code: "InvalidStorageClass",
+ Description: "Invalid storage class.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidRequestBody: {
+ Code: "InvalidArgument",
+ Description: "Body shouldn't be set for this request.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidMaxUploads: {
+ Code: "InvalidArgument",
+ Description: "Argument max-uploads must be an integer between 0 and 2147483647",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidMaxKeys: {
+ Code: "InvalidArgument",
+ Description: "Argument maxKeys must be an integer between 0 and 2147483647",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidEncodingMethod: {
+ Code: "InvalidArgument",
+ Description: "Invalid Encoding Method specified in Request",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidMaxParts: {
+ Code: "InvalidArgument",
+ Description: "Part number must be an integer between 1 and 10000, inclusive",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidPartNumberMarker: {
+ Code: "InvalidArgument",
+ Description: "Argument partNumberMarker must be an integer.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidPartNumber: {
+ Code: "InvalidPartNumber",
+ Description: "The requested partnumber is not satisfiable",
+ HTTPStatusCode: http.StatusRequestedRangeNotSatisfiable,
+ },
+ ErrInvalidPolicyDocument: {
+ Code: "InvalidPolicyDocument",
+ Description: "The content of the form does not meet the conditions specified in the policy document.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAccessDenied: {
+ Code: "AccessDenied",
+ Description: "Access Denied.",
+ HTTPStatusCode: http.StatusForbidden,
+ },
+ ErrBadDigest: {
+ Code: "BadDigest",
+ Description: "The Content-Md5 you specified did not match what we received.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrEntityTooSmall: {
+ Code: "EntityTooSmall",
+ Description: "Your proposed upload is smaller than the minimum allowed object size.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrEntityTooLarge: {
+ Code: "EntityTooLarge",
+ Description: "Your proposed upload exceeds the maximum allowed object size.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrExcessData: {
+ Code: "ExcessData",
+ Description: "More data provided than indicated content length",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrPolicyInvalidName: {
+ Code: "PolicyInvalidName",
+ Description: "Policy name may not contain comma",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminOpenIDNotEnabled: {
+ Code: "OpenIDNotEnabled",
+ Description: "No enabled OpenID Connect identity providers",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrPolicyTooLarge: {
+ Code: "PolicyTooLarge",
+ Description: "Policy exceeds the maximum allowed document size.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrIncompleteBody: {
+ Code: "IncompleteBody",
+ Description: "You did not provide the number of bytes specified by the Content-Length HTTP header.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInternalError: {
+ Code: "InternalError",
+ Description: "We encountered an internal error, please try again.",
+ HTTPStatusCode: http.StatusInternalServerError,
+ },
+ ErrInvalidAccessKeyID: {
+ Code: "InvalidAccessKeyId",
+ Description: "The Access Key Id you provided does not exist in our records.",
+ HTTPStatusCode: http.StatusForbidden,
+ },
+ ErrAccessKeyDisabled: {
+ Code: "InvalidAccessKeyId",
+ Description: "Your account is disabled; please contact your administrator.",
+ HTTPStatusCode: http.StatusForbidden,
+ },
+ ErrInvalidArgument: {
+ Code: "InvalidArgument",
+ Description: "Invalid argument",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidBucketName: {
+ Code: "InvalidBucketName",
+ Description: "The specified bucket is not valid.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidDigest: {
+ Code: "InvalidDigest",
+ Description: "The Content-Md5 you specified is not valid.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidRange: {
+ Code: "InvalidRange",
+ Description: "The requested range is not satisfiable",
+ HTTPStatusCode: http.StatusRequestedRangeNotSatisfiable,
+ },
+ ErrInvalidRangePartNumber: {
+ Code: "InvalidRequest",
+ Description: "Cannot specify both Range header and partNumber query parameter",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrMalformedXML: {
+ Code: "MalformedXML",
+ Description: "The XML you provided was not well-formed or did not validate against our published schema.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrMissingContentLength: {
+ Code: "MissingContentLength",
+ Description: "You must provide the Content-Length HTTP header.",
+ HTTPStatusCode: http.StatusLengthRequired,
+ },
+ ErrMissingContentMD5: {
+ Code: "MissingContentMD5",
+ Description: "Missing or invalid required header for this request: Content-Md5 or Amz-Content-Checksum",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrMissingSecurityHeader: {
+ Code: "MissingSecurityHeader",
+ Description: "Your request was missing a required header",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrMissingRequestBodyError: {
+ Code: "MissingRequestBodyError",
+ Description: "Request body is empty.",
+ HTTPStatusCode: http.StatusLengthRequired,
+ },
+ ErrNoSuchBucket: {
+ Code: "NoSuchBucket",
+ Description: "The specified bucket does not exist",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrNoSuchBucketPolicy: {
+ Code: "NoSuchBucketPolicy",
+ Description: "The bucket policy does not exist",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrNoSuchBucketLifecycle: {
+ Code: "NoSuchBucketLifecycle",
+ Description: "The bucket lifecycle configuration does not exist",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrNoSuchLifecycleConfiguration: {
+ Code: "NoSuchLifecycleConfiguration",
+ Description: "The lifecycle configuration does not exist",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrInvalidLifecycleWithObjectLock: {
+ Code: "InvalidLifecycleWithObjectLock",
+ Description: "The lifecycle configuration containing MaxNoncurrentVersions is not supported with object locking",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrNoSuchBucketSSEConfig: {
+ Code: "ServerSideEncryptionConfigurationNotFoundError",
+ Description: "The server side encryption configuration was not found",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrNoSuchKey: {
+ Code: "NoSuchKey",
+ Description: "The specified key does not exist.",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrNoSuchUpload: {
+ Code: "NoSuchUpload",
+ Description: "The specified multipart upload does not exist. The upload ID may be invalid, or the upload may have been aborted or completed.",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrInvalidVersionID: {
+ Code: "InvalidArgument",
+ Description: "Invalid version id specified",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrNoSuchVersion: {
+ Code: "NoSuchVersion",
+ Description: "The specified version does not exist.",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrNotImplemented: {
+ Code: "NotImplemented",
+ Description: "A header you provided implies functionality that is not implemented",
+ HTTPStatusCode: http.StatusNotImplemented,
+ },
+ ErrPreconditionFailed: {
+ Code: "PreconditionFailed",
+ Description: "At least one of the pre-conditions you specified did not hold",
+ HTTPStatusCode: http.StatusPreconditionFailed,
+ },
+ ErrRequestTimeTooSkewed: {
+ Code: "RequestTimeTooSkewed",
+ Description: "The difference between the request time and the server's time is too large.",
+ HTTPStatusCode: http.StatusForbidden,
+ },
+ ErrSignatureDoesNotMatch: {
+ Code: "SignatureDoesNotMatch",
+ Description: "The request signature we calculated does not match the signature you provided. Check your key and signing method.",
+ HTTPStatusCode: http.StatusForbidden,
+ },
+ ErrMethodNotAllowed: {
+ Code: "MethodNotAllowed",
+ Description: "The specified method is not allowed against this resource.",
+ HTTPStatusCode: http.StatusMethodNotAllowed,
+ },
+ ErrInvalidPart: {
+ Code: "InvalidPart",
+ Description: "One or more of the specified parts could not be found. The part may not have been uploaded, or the specified entity tag may not match the part's entity tag.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrMissingPart: {
+ Code: "InvalidRequest",
+ Description: "You must specify at least one part",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidPartOrder: {
+ Code: "InvalidPartOrder",
+ Description: "The list of parts was not in ascending order. The parts list must be specified in order by part number.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidObjectState: {
+ Code: "InvalidObjectState",
+ Description: "The operation is not valid for the current state of the object.",
+ HTTPStatusCode: http.StatusForbidden,
+ },
+ ErrAuthorizationHeaderMalformed: {
+ Code: "AuthorizationHeaderMalformed",
+ Description: "The authorization header is malformed; the region is wrong; expecting 'us-east-1'.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrMalformedPOSTRequest: {
+ Code: "MalformedPOSTRequest",
+ Description: "The body of your POST request is not well-formed multipart/form-data.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrPOSTFileRequired: {
+ Code: "InvalidArgument",
+ Description: "POST requires exactly one file upload per request.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrSignatureVersionNotSupported: {
+ Code: "InvalidRequest",
+ Description: "The authorization mechanism you have provided is not supported. Please use AWS4-HMAC-SHA256.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrBucketNotEmpty: {
+ Code: "BucketNotEmpty",
+ Description: "The bucket you tried to delete is not empty",
+ HTTPStatusCode: http.StatusConflict,
+ },
+ ErrBucketAlreadyExists: {
+ Code: "BucketAlreadyExists",
+ Description: "The requested bucket name is not available. The bucket namespace is shared by all users of the system. Please select a different name and try again.",
+ HTTPStatusCode: http.StatusConflict,
+ },
+ ErrAllAccessDisabled: {
+ Code: "AllAccessDisabled",
+ Description: "All access to this resource has been disabled.",
+ HTTPStatusCode: http.StatusForbidden,
+ },
+ ErrPolicyInvalidVersion: {
+ Code: "MalformedPolicy",
+ Description: "The policy must contain a valid version string",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrMissingFields: {
+ Code: "MissingFields",
+ Description: "Missing fields in request.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrMissingCredTag: {
+ Code: "InvalidRequest",
+ Description: "Missing Credential field for this request.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrCredMalformed: {
+ Code: "AuthorizationQueryParametersError",
+ Description: "Error parsing the X-Amz-Credential parameter; the Credential is mal-formed; expecting \"/YYYYMMDD/REGION/SERVICE/aws4_request\".",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrMalformedDate: {
+ Code: "MalformedDate",
+ Description: "Invalid date format header, expected to be in ISO8601, RFC1123 or RFC1123Z time format.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrMalformedPresignedDate: {
+ Code: "AuthorizationQueryParametersError",
+ Description: "X-Amz-Date must be in the ISO8601 Long Format \"yyyyMMdd'T'HHmmss'Z'\"",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrMalformedCredentialDate: {
+ Code: "AuthorizationQueryParametersError",
+ Description: "Error parsing the X-Amz-Credential parameter; incorrect date format. This date in the credential must be in the format \"yyyyMMdd\".",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidRegion: {
+ Code: "InvalidRegion",
+ Description: "Region does not match.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidServiceS3: {
+ Code: "AuthorizationParametersError",
+ Description: "Error parsing the Credential/X-Amz-Credential parameter; incorrect service. This endpoint belongs to \"s3\".",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidServiceSTS: {
+ Code: "AuthorizationParametersError",
+ Description: "Error parsing the Credential parameter; incorrect service. This endpoint belongs to \"sts\".",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidRequestVersion: {
+ Code: "AuthorizationQueryParametersError",
+ Description: "Error parsing the X-Amz-Credential parameter; incorrect terminal. This endpoint uses \"aws4_request\".",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrMissingSignTag: {
+ Code: "AccessDenied",
+ Description: "Signature header missing Signature field.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrMissingSignHeadersTag: {
+ Code: "InvalidArgument",
+ Description: "Signature header missing SignedHeaders field.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrMalformedExpires: {
+ Code: "AuthorizationQueryParametersError",
+ Description: "X-Amz-Expires should be a number",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrNegativeExpires: {
+ Code: "AuthorizationQueryParametersError",
+ Description: "X-Amz-Expires must be non-negative",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAuthHeaderEmpty: {
+ Code: "InvalidArgument",
+ Description: "Authorization header is invalid -- one and only one ' ' (space) required.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrMissingDateHeader: {
+ Code: "AccessDenied",
+ Description: "AWS authentication requires a valid Date or x-amz-date header",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidQuerySignatureAlgo: {
+ Code: "AuthorizationQueryParametersError",
+ Description: "X-Amz-Algorithm only supports \"AWS4-HMAC-SHA256\".",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrExpiredPresignRequest: {
+ Code: "AccessDenied",
+ Description: "Request has expired",
+ HTTPStatusCode: http.StatusForbidden,
+ },
+ ErrRequestNotReadyYet: {
+ Code: "AccessDenied",
+ Description: "Request is not valid yet",
+ HTTPStatusCode: http.StatusForbidden,
+ },
+ ErrSlowDownRead: {
+ Code: "SlowDownRead",
+ Description: "Resource requested is unreadable, please reduce your request rate",
+ HTTPStatusCode: http.StatusServiceUnavailable,
+ },
+ ErrSlowDownWrite: {
+ Code: "SlowDownWrite",
+ Description: "Resource requested is unwritable, please reduce your request rate",
+ HTTPStatusCode: http.StatusServiceUnavailable,
+ },
+ ErrMaxVersionsExceeded: {
+ Code: "MaxVersionsExceeded",
+ Description: "You've exceeded the limit on the number of versions you can create on this object",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidPrefixMarker: {
+ Code: "InvalidPrefixMarker",
+ Description: "Invalid marker prefix combination",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrBadRequest: {
+ Code: "BadRequest",
+ Description: "400 BadRequest",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrKeyTooLongError: {
+ Code: "KeyTooLongError",
+ Description: "Your key is too long",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrUnsignedHeaders: {
+ Code: "AccessDenied",
+ Description: "There were headers present in the request which were not signed",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidQueryParams: {
+ Code: "AuthorizationQueryParametersError",
+ Description: "Query-string authentication version 4 requires the X-Amz-Algorithm, X-Amz-Credential, X-Amz-Signature, X-Amz-Date, X-Amz-SignedHeaders, and X-Amz-Expires parameters.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrBucketAlreadyOwnedByYou: {
+ Code: "BucketAlreadyOwnedByYou",
+ Description: "Your previous request to create the named bucket succeeded and you already own it.",
+ HTTPStatusCode: http.StatusConflict,
+ },
+ ErrInvalidDuration: {
+ Code: "InvalidDuration",
+ Description: "Duration provided in the request is invalid.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidBucketObjectLockConfiguration: {
+ Code: "InvalidRequest",
+ Description: "Bucket is missing ObjectLockConfiguration",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrBucketTaggingNotFound: {
+ Code: "NoSuchTagSet",
+ Description: "The TagSet does not exist",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrObjectLockConfigurationNotFound: {
+ Code: "ObjectLockConfigurationNotFoundError",
+ Description: "Object Lock configuration does not exist for this bucket",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrObjectLockConfigurationNotAllowed: {
+ Code: "InvalidBucketState",
+ Description: "Object Lock configuration cannot be enabled on existing buckets",
+ HTTPStatusCode: http.StatusConflict,
+ },
+ ErrNoSuchCORSConfiguration: {
+ Code: "NoSuchCORSConfiguration",
+ Description: "The CORS configuration does not exist",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrNoSuchWebsiteConfiguration: {
+ Code: "NoSuchWebsiteConfiguration",
+ Description: "The specified bucket does not have a website configuration",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrReplicationConfigurationNotFoundError: {
+ Code: "ReplicationConfigurationNotFoundError",
+ Description: "The replication configuration was not found",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrRemoteDestinationNotFoundError: {
+ Code: "RemoteDestinationNotFoundError",
+ Description: "The remote destination bucket does not exist",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrReplicationDestinationMissingLock: {
+ Code: "ReplicationDestinationMissingLockError",
+ Description: "The replication destination bucket does not have object locking enabled",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrRemoteTargetNotFoundError: {
+ Code: "XMinioAdminRemoteTargetNotFoundError",
+ Description: "The remote target does not exist",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrReplicationRemoteConnectionError: {
+ Code: "XMinioAdminReplicationRemoteConnectionError",
+ Description: "Remote service connection error",
+ HTTPStatusCode: http.StatusServiceUnavailable,
+ },
+ ErrReplicationBandwidthLimitError: {
+ Code: "XMinioAdminReplicationBandwidthLimitError",
+ Description: "Bandwidth limit for remote target must be at least 100MBps",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrReplicationNoExistingObjects: {
+ Code: "XMinioReplicationNoExistingObjects",
+ Description: "No matching ExistingObjects rule enabled",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrRemoteTargetDenyAddError: {
+ Code: "XMinioAdminRemoteTargetDenyAdd",
+ Description: "Cannot add remote target endpoint since this server is in a cluster replication setup",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrReplicationDenyEditError: {
+ Code: "XMinioReplicationDenyEdit",
+ Description: "Cannot alter local replication config since this server is in a cluster replication setup",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrBucketRemoteIdenticalToSource: {
+ Code: "XMinioAdminRemoteIdenticalToSource",
+ Description: "The remote target cannot be identical to source",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrBucketRemoteAlreadyExists: {
+ Code: "XMinioAdminBucketRemoteAlreadyExists",
+ Description: "The remote target already exists",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrBucketRemoteLabelInUse: {
+ Code: "XMinioAdminBucketRemoteLabelInUse",
+ Description: "The remote target with this label already exists",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrBucketRemoteRemoveDisallowed: {
+ Code: "XMinioAdminRemoteRemoveDisallowed",
+ Description: "This ARN is in use by an existing configuration",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrBucketRemoteArnTypeInvalid: {
+ Code: "XMinioAdminRemoteARNTypeInvalid",
+ Description: "The bucket remote ARN type is not valid",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrBucketRemoteArnInvalid: {
+ Code: "XMinioAdminRemoteArnInvalid",
+ Description: "The bucket remote ARN does not have correct format",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrRemoteTargetNotVersionedError: {
+ Code: "RemoteTargetNotVersionedError",
+ Description: "The remote target does not have versioning enabled",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrReplicationSourceNotVersionedError: {
+ Code: "ReplicationSourceNotVersionedError",
+ Description: "The replication source does not have versioning enabled",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrReplicationNeedsVersioningError: {
+ Code: "InvalidRequest",
+ Description: "Versioning must be 'Enabled' on the bucket to apply a replication configuration",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrReplicationBucketNeedsVersioningError: {
+ Code: "InvalidRequest",
+ Description: "Versioning must be 'Enabled' on the bucket to add a replication target",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrReplicationValidationError: {
+ Code: "InvalidRequest",
+ Description: "Replication validation failed on target",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrReplicationPermissionCheckError: {
+ Code: "ReplicationPermissionCheck",
+ Description: "X-Minio-Source-Replication-Check cannot be specified in request. Request cannot be completed",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrNoSuchObjectLockConfiguration: {
+ Code: "NoSuchObjectLockConfiguration",
+ Description: "The specified object does not have a ObjectLock configuration",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrObjectLocked: {
+ Code: "InvalidRequest",
+ Description: "Object is WORM protected and cannot be overwritten",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidRetentionDate: {
+ Code: "InvalidRequest",
+ Description: "Date must be provided in ISO 8601 format",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrPastObjectLockRetainDate: {
+ Code: "InvalidRequest",
+ Description: "the retain until date must be in the future",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrUnknownWORMModeDirective: {
+ Code: "InvalidRequest",
+ Description: "unknown wormMode directive",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrObjectLockInvalidHeaders: {
+ Code: "InvalidRequest",
+ Description: "x-amz-object-lock-retain-until-date and x-amz-object-lock-mode must both be supplied",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrObjectRestoreAlreadyInProgress: {
+ Code: "RestoreAlreadyInProgress",
+ Description: "Object restore is already in progress",
+ HTTPStatusCode: http.StatusConflict,
+ },
+ ErrTransitionStorageClassNotFoundError: {
+ Code: "TransitionStorageClassNotFoundError",
+ Description: "The transition storage class was not found",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+
+ // Bucket notification related errors.
+ ErrEventNotification: {
+ Code: "InvalidArgument",
+ Description: "A specified event is not supported for notifications.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrARNNotification: {
+ Code: "InvalidArgument",
+ Description: "A specified destination ARN does not exist or is not well-formed. Verify the destination ARN.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrRegionNotification: {
+ Code: "InvalidArgument",
+ Description: "A specified destination is in a different region than the bucket. You must use a destination that resides in the same region as the bucket.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrOverlappingFilterNotification: {
+ Code: "InvalidArgument",
+ Description: "An object key name filtering rule defined with overlapping prefixes, overlapping suffixes, or overlapping combinations of prefixes and suffixes for the same event types.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrFilterNameInvalid: {
+ Code: "InvalidArgument",
+ Description: "filter rule name must be either prefix or suffix",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrFilterNamePrefix: {
+ Code: "InvalidArgument",
+ Description: "Cannot specify more than one prefix rule in a filter.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrFilterNameSuffix: {
+ Code: "InvalidArgument",
+ Description: "Cannot specify more than one suffix rule in a filter.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrFilterValueInvalid: {
+ Code: "InvalidArgument",
+ Description: "Size of filter rule value cannot exceed 1024 bytes in UTF-8 representation",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrOverlappingConfigs: {
+ Code: "InvalidArgument",
+ Description: "Configurations overlap. Configurations on the same bucket cannot share a common event type.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrUnsupportedNotification: {
+ Code: "UnsupportedNotification",
+ Description: "MinIO server does not support Topic or Cloud Function based notifications.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidCopyPartRange: {
+ Code: "InvalidArgument",
+ Description: "The x-amz-copy-source-range value must be of the form bytes=first-last where first and last are the zero-based offsets of the first and last bytes to copy",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidCopyPartRangeSource: {
+ Code: "InvalidArgument",
+ Description: "Range specified is not valid for source object",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrMetadataTooLarge: {
+ Code: "MetadataTooLarge",
+ Description: "Your metadata headers exceed the maximum allowed metadata size.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidTagDirective: {
+ Code: "InvalidArgument",
+ Description: "Unknown tag directive.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidEncryptionMethod: {
+ Code: "InvalidArgument",
+ Description: "Server Side Encryption with AWS KMS managed key requires HTTP header x-amz-server-side-encryption : aws:kms",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrIncompatibleEncryptionMethod: {
+ Code: "InvalidArgument",
+ Description: "Server Side Encryption with Customer provided key is incompatible with the encryption method specified",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidEncryptionKeyID: {
+ Code: "InvalidRequest",
+ Description: "The specified KMS KeyID contains unsupported characters",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInsecureSSECustomerRequest: {
+ Code: "InvalidRequest",
+ Description: "Requests specifying Server Side Encryption with Customer provided keys must be made over a secure connection.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrSSEMultipartEncrypted: {
+ Code: "InvalidRequest",
+ Description: "The multipart upload initiate requested encryption. Subsequent part requests must include the appropriate encryption parameters.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrSSEEncryptedObject: {
+ Code: "InvalidRequest",
+ Description: "The object was stored using a form of Server Side Encryption. The correct parameters must be provided to retrieve the object.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidEncryptionParameters: {
+ Code: "InvalidRequest",
+ Description: "The encryption parameters are not applicable to this object.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidEncryptionParametersSSEC: {
+ Code: "InvalidRequest",
+ Description: "SSE-C encryption parameters are not supported on replicated bucket.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidSSECustomerAlgorithm: {
+ Code: "InvalidArgument",
+ Description: "Requests specifying Server Side Encryption with Customer provided keys must provide a valid encryption algorithm.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidSSECustomerKey: {
+ Code: "InvalidArgument",
+ Description: "The secret key was invalid for the specified algorithm.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrMissingSSECustomerKey: {
+ Code: "InvalidArgument",
+ Description: "Requests specifying Server Side Encryption with Customer provided keys must provide an appropriate secret key.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrMissingSSECustomerKeyMD5: {
+ Code: "InvalidArgument",
+ Description: "Requests specifying Server Side Encryption with Customer provided keys must provide the client calculated MD5 of the secret key.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrSSECustomerKeyMD5Mismatch: {
+ Code: "InvalidArgument",
+ Description: "The calculated MD5 hash of the key did not match the hash that was provided.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidSSECustomerParameters: {
+ Code: "InvalidArgument",
+ Description: "The provided encryption parameters did not match the ones used originally.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrKMSNotConfigured: {
+ Code: "NotImplemented",
+ Description: "Server side encryption specified but KMS is not configured",
+ HTTPStatusCode: http.StatusNotImplemented,
+ },
+ ErrKMSKeyNotFoundException: {
+ Code: "KMS.NotFoundException",
+ Description: "Invalid keyId",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrKMSDefaultKeyAlreadyConfigured: {
+ Code: "KMS.DefaultKeyAlreadyConfiguredException",
+ Description: "A default encryption already exists and cannot be changed on KMS",
+ HTTPStatusCode: http.StatusConflict,
+ },
+ ErrNoAccessKey: {
+ Code: "AccessDenied",
+ Description: "No AWSAccessKey was presented",
+ HTTPStatusCode: http.StatusForbidden,
+ },
+ ErrInvalidToken: {
+ Code: "InvalidTokenId",
+ Description: "The security token included in the request is invalid",
+ HTTPStatusCode: http.StatusForbidden,
+ },
+ ErrNoTokenRevokeType: {
+ Code: "InvalidArgument",
+ Description: "No token revoke type specified and one could not be inferred from the request",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminNoSuchAccessKey: {
+ Code: "XMinioAdminNoSuchAccessKey",
+ Description: "The specified access key does not exist.",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+
+ // S3 extensions.
+ ErrContentSHA256Mismatch: {
+ Code: "XAmzContentSHA256Mismatch",
+ Description: "The provided 'x-amz-content-sha256' header does not match what was computed.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrContentChecksumMismatch: {
+ Code: "XAmzContentChecksumMismatch",
+ Description: "The provided 'x-amz-checksum' header does not match what was computed.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+
+ // MinIO extensions.
+ ErrStorageFull: {
+ Code: "XMinioStorageFull",
+ Description: "Storage backend has reached its minimum free drive threshold. Please delete a few objects to proceed.",
+ HTTPStatusCode: http.StatusInsufficientStorage,
+ },
+ ErrRequestBodyParse: {
+ Code: "XMinioRequestBodyParse",
+ Description: "The request body failed to parse.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrObjectExistsAsDirectory: {
+ Code: "XMinioObjectExistsAsDirectory",
+ Description: "Object name already exists as a directory.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidObjectName: {
+ Code: "XMinioInvalidObjectName",
+ Description: "Object name contains unsupported characters.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidObjectNamePrefixSlash: {
+ Code: "XMinioInvalidObjectName",
+ Description: "Object name contains a leading slash.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidResourceName: {
+ Code: "XMinioInvalidResourceName",
+ Description: "Resource name contains bad components such as \"..\" or \".\".",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrServerNotInitialized: {
+ Code: "XMinioServerNotInitialized",
+ Description: "Server not initialized yet, please try again.",
+ HTTPStatusCode: http.StatusServiceUnavailable,
+ },
+ ErrIAMNotInitialized: {
+ Code: "XMinioIAMNotInitialized",
+ Description: "IAM sub-system not initialized yet, please try again.",
+ HTTPStatusCode: http.StatusServiceUnavailable,
+ },
+ ErrBucketMetadataNotInitialized: {
+ Code: "XMinioBucketMetadataNotInitialized",
+ Description: "Bucket metadata not initialized yet, please try again.",
+ HTTPStatusCode: http.StatusServiceUnavailable,
+ },
+ ErrMalformedJSON: {
+ Code: "XMinioMalformedJSON",
+ Description: "The JSON you provided was not well-formed or did not validate against our published format.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidLifecycleQueryParameter: {
+ Code: "XMinioInvalidLifecycleParameter",
+ Description: "The boolean value provided for withUpdatedAt query parameter was invalid.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminNoSuchUser: {
+ Code: "XMinioAdminNoSuchUser",
+ Description: "The specified user does not exist.",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrAdminNoSuchUserLDAPWarn: {
+ Code: "XMinioAdminNoSuchUser",
+ Description: "The specified user does not exist. If you meant a user in LDAP, use `mc idp ldap`",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrAdminNoSuchGroup: {
+ Code: "XMinioAdminNoSuchGroup",
+ Description: "The specified group does not exist.",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrAdminNoSuchJob: {
+ Code: "XMinioAdminNoSuchJob",
+ Description: "The specified job does not exist.",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrAdminGroupNotEmpty: {
+ Code: "XMinioAdminGroupNotEmpty",
+ Description: "The specified group is not empty - cannot remove it.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminGroupDisabled: {
+ Code: "XMinioAdminGroupDisabled",
+ Description: "The specified group is disabled.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminNoSuchPolicy: {
+ Code: "XMinioAdminNoSuchPolicy",
+ Description: "The canned policy does not exist.",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrAdminPolicyChangeAlreadyApplied: {
+ Code: "XMinioAdminPolicyChangeAlreadyApplied",
+ Description: "The specified policy change is already in effect.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+
+ ErrAdminInvalidArgument: {
+ Code: "XMinioAdminInvalidArgument",
+ Description: "Invalid arguments specified.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminInvalidAccessKey: {
+ Code: "XMinioAdminInvalidAccessKey",
+ Description: "The access key is invalid.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminInvalidSecretKey: {
+ Code: "XMinioAdminInvalidSecretKey",
+ Description: "The secret key is invalid.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminNoAccessKey: {
+ Code: "XMinioAdminNoAccessKey",
+ Description: "No access key was provided.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminNoSecretKey: {
+ Code: "XMinioAdminNoSecretKey",
+ Description: "No secret key was provided.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminConfigNoQuorum: {
+ Code: "XMinioAdminConfigNoQuorum",
+ Description: "Configuration update failed because server quorum was not met",
+ HTTPStatusCode: http.StatusServiceUnavailable,
+ },
+ ErrAdminConfigTooLarge: {
+ Code: "XMinioAdminConfigTooLarge",
+ Description: fmt.Sprintf("Configuration data provided exceeds the allowed maximum of %d bytes",
+ maxEConfigJSONSize),
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminNoSuchConfigTarget: {
+ Code: "XMinioAdminNoSuchConfigTarget",
+ Description: "No such named configuration target exists",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminConfigBadJSON: {
+ Code: "XMinioAdminConfigBadJSON",
+ Description: "JSON configuration provided is of incorrect format",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminConfigEnvOverridden: {
+ Code: "XMinioAdminConfigEnvOverridden",
+ Description: "Unable to update config via Admin API due to environment variable override",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminConfigDuplicateKeys: {
+ Code: "XMinioAdminConfigDuplicateKeys",
+ Description: "JSON configuration provided has objects with duplicate keys",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminConfigInvalidIDPType: {
+ Code: "XMinioAdminConfigInvalidIDPType",
+ Description: fmt.Sprintf("Invalid IDP configuration type - must be one of %v", madmin.ValidIDPConfigTypes),
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminConfigLDAPNonDefaultConfigName: {
+ Code: "XMinioAdminConfigLDAPNonDefaultConfigName",
+ Description: "Only a single LDAP configuration is supported - config name must be empty or `_`",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminConfigLDAPValidation: {
+ Code: "XMinioAdminConfigLDAPValidation",
+ Description: "LDAP Configuration validation failed",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminConfigIDPCfgNameAlreadyExists: {
+ Code: "XMinioAdminConfigIDPCfgNameAlreadyExists",
+ Description: "An IDP configuration with the given name already exists",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminConfigIDPCfgNameDoesNotExist: {
+ Code: "XMinioAdminConfigIDPCfgNameDoesNotExist",
+ Description: "No such IDP configuration exists",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminConfigNotificationTargetsFailed: {
+ Code: "XMinioAdminNotificationTargetsTestFailed",
+ Description: "Configuration update failed due an unsuccessful attempt to connect to one or more notification servers",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminProfilerNotEnabled: {
+ Code: "XMinioAdminProfilerNotEnabled",
+ Description: "Unable to perform the requested operation because profiling is not enabled",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminBucketQuotaExceeded: {
+ Code: "XMinioAdminBucketQuotaExceeded",
+ Description: "Bucket quota exceeded",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminNoSuchQuotaConfiguration: {
+ Code: "XMinioAdminNoSuchQuotaConfiguration",
+ Description: "The quota configuration does not exist",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrInsecureClientRequest: {
+ Code: "XMinioInsecureClientRequest",
+ Description: "Cannot respond to plain-text request from TLS-encrypted server",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrRequestTimedout: {
+ Code: "RequestTimeout",
+ Description: "A timeout occurred while trying to lock a resource, please reduce your request rate",
+ HTTPStatusCode: http.StatusServiceUnavailable,
+ },
+ ErrClientDisconnected: {
+ Code: "ClientDisconnected",
+ Description: "Client disconnected before response was ready",
+ HTTPStatusCode: 499, // No official code, use nginx value.
+ },
+ ErrTooManyRequests: {
+ Code: "TooManyRequests",
+ Description: "Please reduce your request rate",
+ HTTPStatusCode: http.StatusTooManyRequests,
+ },
+ ErrUnsupportedMetadata: {
+ Code: "InvalidArgument",
+ Description: "Your metadata headers are not supported.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrUnsupportedHostHeader: {
+ Code: "InvalidArgument",
+ Description: "Your Host header is malformed.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrObjectTampered: {
+ Code: "XMinioObjectTampered",
+ Description: errObjectTampered.Error(),
+ HTTPStatusCode: http.StatusPartialContent,
+ },
+
+ ErrSiteReplicationInvalidRequest: {
+ Code: "XMinioSiteReplicationInvalidRequest",
+ Description: "Invalid site-replication request",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrSiteReplicationPeerResp: {
+ Code: "XMinioSiteReplicationPeerResp",
+ Description: "Error received when contacting a peer site",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrSiteReplicationBackendIssue: {
+ Code: "XMinioSiteReplicationBackendIssue",
+ Description: "Error when requesting object layer backend",
+ HTTPStatusCode: http.StatusServiceUnavailable,
+ },
+ ErrSiteReplicationServiceAccountError: {
+ Code: "XMinioSiteReplicationServiceAccountError",
+ Description: "Site replication related service account error",
+ HTTPStatusCode: http.StatusServiceUnavailable,
+ },
+ ErrSiteReplicationBucketConfigError: {
+ Code: "XMinioSiteReplicationBucketConfigError",
+ Description: "Error while configuring replication on a bucket",
+ HTTPStatusCode: http.StatusServiceUnavailable,
+ },
+ ErrSiteReplicationBucketMetaError: {
+ Code: "XMinioSiteReplicationBucketMetaError",
+ Description: "Error while replicating bucket metadata",
+ HTTPStatusCode: http.StatusServiceUnavailable,
+ },
+ ErrSiteReplicationIAMError: {
+ Code: "XMinioSiteReplicationIAMError",
+ Description: "Error while replicating an IAM item",
+ HTTPStatusCode: http.StatusServiceUnavailable,
+ },
+ ErrSiteReplicationConfigMissing: {
+ Code: "XMinioSiteReplicationConfigMissingError",
+ Description: "Site not found in site replication configuration",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrSiteReplicationIAMConfigMismatch: {
+ Code: "XMinioSiteReplicationIAMConfigMismatch",
+ Description: "IAM configuration mismatch between sites",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminRebalanceAlreadyStarted: {
+ Code: "XMinioAdminRebalanceAlreadyStarted",
+ Description: "Pool rebalance is already started",
+ HTTPStatusCode: http.StatusConflict,
+ },
+ ErrAdminRebalanceNotStarted: {
+ Code: "XMinioAdminRebalanceNotStarted",
+ Description: "Pool rebalance is not started",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrMaximumExpires: {
+ Code: "AuthorizationQueryParametersError",
+ Description: "X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+
+ // Generic Invalid-Request error. Should be used for response errors only for unlikely
+ // corner case errors for which introducing new APIErrorCode is not worth it. LogIf()
+ // should be used to log the error at the source of the error for debugging purposes.
+ ErrInvalidRequest: {
+ Code: "InvalidRequest",
+ Description: "Invalid Request",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrHealNotImplemented: {
+ Code: "XMinioHealNotImplemented",
+ Description: "This server does not implement heal functionality.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrHealNoSuchProcess: {
+ Code: "XMinioHealNoSuchProcess",
+ Description: "No such heal process is running on the server",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrHealInvalidClientToken: {
+ Code: "XMinioHealInvalidClientToken",
+ Description: "Client token mismatch",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrHealMissingBucket: {
+ Code: "XMinioHealMissingBucket",
+ Description: "A heal start request with a non-empty object-prefix parameter requires a bucket to be specified.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrHealAlreadyRunning: {
+ Code: "XMinioHealAlreadyRunning",
+ Description: "",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrHealOverlappingPaths: {
+ Code: "XMinioHealOverlappingPaths",
+ Description: "",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrBackendDown: {
+ Code: "XMinioBackendDown",
+ Description: "Remote backend is unreachable",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrIncorrectContinuationToken: {
+ Code: "InvalidArgument",
+ Description: "The continuation token provided is incorrect",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ // S3 Select API Errors
+ ErrEmptyRequestBody: {
+ Code: "EmptyRequestBody",
+ Description: "Request body cannot be empty.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrUnsupportedFunction: {
+ Code: "UnsupportedFunction",
+ Description: "Encountered an unsupported SQL function.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidDataSource: {
+ Code: "InvalidDataSource",
+ Description: "Invalid data source type. Only CSV and JSON are supported at this time.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidExpressionType: {
+ Code: "InvalidExpressionType",
+ Description: "The ExpressionType is invalid. Only SQL expressions are supported at this time.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrBusy: {
+ Code: "ServerBusy",
+ Description: "The service is unavailable. Please retry.",
+ HTTPStatusCode: http.StatusServiceUnavailable,
+ },
+ ErrUnauthorizedAccess: {
+ Code: "UnauthorizedAccess",
+ Description: "You are not authorized to perform this operation",
+ HTTPStatusCode: http.StatusUnauthorized,
+ },
+ ErrExpressionTooLong: {
+ Code: "ExpressionTooLong",
+ Description: "The SQL expression is too long: The maximum byte-length for the SQL expression is 256 KB.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrIllegalSQLFunctionArgument: {
+ Code: "IllegalSqlFunctionArgument",
+ Description: "Illegal argument was used in the SQL function.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidKeyPath: {
+ Code: "InvalidKeyPath",
+ Description: "Key path in the SQL expression is invalid.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidCompressionFormat: {
+ Code: "InvalidCompressionFormat",
+ Description: "The file is not in a supported compression format. Only GZIP is supported at this time.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidFileHeaderInfo: {
+ Code: "InvalidFileHeaderInfo",
+ Description: "The FileHeaderInfo is invalid. Only NONE, USE, and IGNORE are supported.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidJSONType: {
+ Code: "InvalidJsonType",
+ Description: "The JsonType is invalid. Only DOCUMENT and LINES are supported at this time.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidQuoteFields: {
+ Code: "InvalidQuoteFields",
+ Description: "The QuoteFields is invalid. Only ALWAYS and ASNEEDED are supported.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidRequestParameter: {
+ Code: "InvalidRequestParameter",
+ Description: "The value of a parameter in SelectRequest element is invalid. Check the service API documentation and try again.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidDataType: {
+ Code: "InvalidDataType",
+ Description: "The SQL expression contains an invalid data type.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidTextEncoding: {
+ Code: "InvalidTextEncoding",
+ Description: "Invalid encoding type. Only UTF-8 encoding is supported at this time.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidTableAlias: {
+ Code: "InvalidTableAlias",
+ Description: "The SQL expression contains an invalid table alias.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrMissingRequiredParameter: {
+ Code: "MissingRequiredParameter",
+ Description: "The SelectRequest entity is missing a required parameter. Check the service documentation and try again.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrObjectSerializationConflict: {
+ Code: "ObjectSerializationConflict",
+ Description: "The SelectRequest entity can only contain one of CSV or JSON. Check the service documentation and try again.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrUnsupportedSQLOperation: {
+ Code: "UnsupportedSqlOperation",
+ Description: "Encountered an unsupported SQL operation.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrUnsupportedSQLStructure: {
+ Code: "UnsupportedSqlStructure",
+ Description: "Encountered an unsupported SQL structure. Check the SQL Reference.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrUnsupportedSyntax: {
+ Code: "UnsupportedSyntax",
+ Description: "Encountered invalid syntax.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrUnsupportedRangeHeader: {
+ Code: "UnsupportedRangeHeader",
+ Description: "Range header is not supported for this operation.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrLexerInvalidChar: {
+ Code: "LexerInvalidChar",
+ Description: "The SQL expression contains an invalid character.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrLexerInvalidOperator: {
+ Code: "LexerInvalidOperator",
+ Description: "The SQL expression contains an invalid literal.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrLexerInvalidLiteral: {
+ Code: "LexerInvalidLiteral",
+ Description: "The SQL expression contains an invalid operator.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrLexerInvalidIONLiteral: {
+ Code: "LexerInvalidIONLiteral",
+ Description: "The SQL expression contains an invalid operator.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseExpectedDatePart: {
+ Code: "ParseExpectedDatePart",
+ Description: "Did not find the expected date part in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseExpectedKeyword: {
+ Code: "ParseExpectedKeyword",
+ Description: "Did not find the expected keyword in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseExpectedTokenType: {
+ Code: "ParseExpectedTokenType",
+ Description: "Did not find the expected token in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseExpected2TokenTypes: {
+ Code: "ParseExpected2TokenTypes",
+ Description: "Did not find the expected token in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseExpectedNumber: {
+ Code: "ParseExpectedNumber",
+ Description: "Did not find the expected number in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseExpectedRightParenBuiltinFunctionCall: {
+ Code: "ParseExpectedRightParenBuiltinFunctionCall",
+ Description: "Did not find the expected right parenthesis character in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseExpectedTypeName: {
+ Code: "ParseExpectedTypeName",
+ Description: "Did not find the expected type name in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseExpectedWhenClause: {
+ Code: "ParseExpectedWhenClause",
+ Description: "Did not find the expected WHEN clause in the SQL expression. CASE is not supported.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseUnsupportedToken: {
+ Code: "ParseUnsupportedToken",
+ Description: "The SQL expression contains an unsupported token.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseUnsupportedLiteralsGroupBy: {
+ Code: "ParseUnsupportedLiteralsGroupBy",
+ Description: "The SQL expression contains an unsupported use of GROUP BY.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseExpectedMember: {
+ Code: "ParseExpectedMember",
+ Description: "The SQL expression contains an unsupported use of MEMBER.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseUnsupportedSelect: {
+ Code: "ParseUnsupportedSelect",
+ Description: "The SQL expression contains an unsupported use of SELECT.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseUnsupportedCase: {
+ Code: "ParseUnsupportedCase",
+ Description: "The SQL expression contains an unsupported use of CASE.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseUnsupportedCaseClause: {
+ Code: "ParseUnsupportedCaseClause",
+ Description: "The SQL expression contains an unsupported use of CASE.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseUnsupportedAlias: {
+ Code: "ParseUnsupportedAlias",
+ Description: "The SQL expression contains an unsupported use of ALIAS.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseUnsupportedSyntax: {
+ Code: "ParseUnsupportedSyntax",
+ Description: "The SQL expression contains unsupported syntax.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseUnknownOperator: {
+ Code: "ParseUnknownOperator",
+ Description: "The SQL expression contains an invalid operator.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseMissingIdentAfterAt: {
+ Code: "ParseMissingIdentAfterAt",
+ Description: "Did not find the expected identifier after the @ symbol in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseUnexpectedOperator: {
+ Code: "ParseUnexpectedOperator",
+ Description: "The SQL expression contains an unexpected operator.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseUnexpectedTerm: {
+ Code: "ParseUnexpectedTerm",
+ Description: "The SQL expression contains an unexpected term.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseUnexpectedToken: {
+ Code: "ParseUnexpectedToken",
+ Description: "The SQL expression contains an unexpected token.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseUnexpectedKeyword: {
+ Code: "ParseUnexpectedKeyword",
+ Description: "The SQL expression contains an unexpected keyword.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseExpectedExpression: {
+ Code: "ParseExpectedExpression",
+ Description: "Did not find the expected SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseExpectedLeftParenAfterCast: {
+ Code: "ParseExpectedLeftParenAfterCast",
+ Description: "Did not find expected the left parenthesis in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseExpectedLeftParenValueConstructor: {
+ Code: "ParseExpectedLeftParenValueConstructor",
+ Description: "Did not find expected the left parenthesis in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseExpectedLeftParenBuiltinFunctionCall: {
+ Code: "ParseExpectedLeftParenBuiltinFunctionCall",
+ Description: "Did not find the expected left parenthesis in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseExpectedArgumentDelimiter: {
+ Code: "ParseExpectedArgumentDelimiter",
+ Description: "Did not find the expected argument delimiter in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseCastArity: {
+ Code: "ParseCastArity",
+ Description: "The SQL expression CAST has incorrect arity.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseInvalidTypeParam: {
+ Code: "ParseInvalidTypeParam",
+ Description: "The SQL expression contains an invalid parameter value.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseEmptySelect: {
+ Code: "ParseEmptySelect",
+ Description: "The SQL expression contains an empty SELECT.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseSelectMissingFrom: {
+ Code: "ParseSelectMissingFrom",
+ Description: "GROUP is not supported in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseExpectedIdentForGroupName: {
+ Code: "ParseExpectedIdentForGroupName",
+ Description: "GROUP is not supported in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseExpectedIdentForAlias: {
+ Code: "ParseExpectedIdentForAlias",
+ Description: "Did not find the expected identifier for the alias in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseUnsupportedCallWithStar: {
+ Code: "ParseUnsupportedCallWithStar",
+ Description: "Only COUNT with (*) as a parameter is supported in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseNonUnaryAggregateFunctionCall: {
+ Code: "ParseNonUnaryAggregateFunctionCall",
+ Description: "Only one argument is supported for aggregate functions in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseMalformedJoin: {
+ Code: "ParseMalformedJoin",
+ Description: "JOIN is not supported in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseExpectedIdentForAt: {
+ Code: "ParseExpectedIdentForAt",
+ Description: "Did not find the expected identifier for AT name in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseAsteriskIsNotAloneInSelectList: {
+ Code: "ParseAsteriskIsNotAloneInSelectList",
+ Description: "Other expressions are not allowed in the SELECT list when '*' is used without dot notation in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseCannotMixSqbAndWildcardInSelectList: {
+ Code: "ParseCannotMixSqbAndWildcardInSelectList",
+ Description: "Cannot mix [] and * in the same expression in a SELECT list in SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrParseInvalidContextForWildcardInSelectList: {
+ Code: "ParseInvalidContextForWildcardInSelectList",
+ Description: "Invalid use of * in SELECT list in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrIncorrectSQLFunctionArgumentType: {
+ Code: "IncorrectSqlFunctionArgumentType",
+ Description: "Incorrect type of arguments in function call in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrValueParseFailure: {
+ Code: "ValueParseFailure",
+ Description: "Time stamp parse failure in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrEvaluatorInvalidArguments: {
+ Code: "EvaluatorInvalidArguments",
+ Description: "Incorrect number of arguments in the function call in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrIntegerOverflow: {
+ Code: "IntegerOverflow",
+ Description: "Int overflow or underflow in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrLikeInvalidInputs: {
+ Code: "LikeInvalidInputs",
+ Description: "Invalid argument given to the LIKE clause in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrCastFailed: {
+ Code: "CastFailed",
+ Description: "Attempt to convert from one data type to another using CAST failed in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidCast: {
+ Code: "InvalidCast",
+ Description: "Attempt to convert from one data type to another using CAST failed in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrEvaluatorInvalidTimestampFormatPattern: {
+ Code: "EvaluatorInvalidTimestampFormatPattern",
+ Description: "Time stamp format pattern requires additional fields in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrEvaluatorInvalidTimestampFormatPatternSymbolForParsing: {
+ Code: "EvaluatorInvalidTimestampFormatPatternSymbolForParsing",
+ Description: "Time stamp format pattern contains a valid format symbol that cannot be applied to time stamp parsing in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrEvaluatorTimestampFormatPatternDuplicateFields: {
+ Code: "EvaluatorTimestampFormatPatternDuplicateFields",
+ Description: "Time stamp format pattern contains multiple format specifiers representing the time stamp field in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrEvaluatorTimestampFormatPatternHourClockAmPmMismatch: {
+ Code: "EvaluatorUnterminatedTimestampFormatPatternToken",
+ Description: "Time stamp format pattern contains unterminated token in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrEvaluatorUnterminatedTimestampFormatPatternToken: {
+ Code: "EvaluatorInvalidTimestampFormatPatternToken",
+ Description: "Time stamp format pattern contains an invalid token in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrEvaluatorInvalidTimestampFormatPatternToken: {
+ Code: "EvaluatorInvalidTimestampFormatPatternToken",
+ Description: "Time stamp format pattern contains an invalid token in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrEvaluatorInvalidTimestampFormatPatternSymbol: {
+ Code: "EvaluatorInvalidTimestampFormatPatternSymbol",
+ Description: "Time stamp format pattern contains an invalid symbol in the SQL expression.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrEvaluatorBindingDoesNotExist: {
+ Code: "ErrEvaluatorBindingDoesNotExist",
+ Description: "A column name or a path provided does not exist in the SQL expression",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrMissingHeaders: {
+ Code: "MissingHeaders",
+ Description: "Some headers in the query are missing from the file. Check the file and try again.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidColumnIndex: {
+ Code: "InvalidColumnIndex",
+ Description: "The column index is invalid. Please check the service documentation and try again.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrInvalidDecompressedSize: {
+ Code: "XMinioInvalidDecompressedSize",
+ Description: "The data provided is unfit for decompression",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAddUserInvalidArgument: {
+ Code: "XMinioInvalidIAMCredentials",
+ Description: "Credential is not allowed to be same as admin access key",
+ HTTPStatusCode: http.StatusForbidden,
+ },
+ ErrAdminResourceInvalidArgument: {
+ Code: "XMinioInvalidResource",
+ Description: "Policy, user or group names are not allowed to begin or end with space characters",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminAccountNotEligible: {
+ Code: "XMinioInvalidIAMCredentials",
+ Description: "The administrator key is not eligible for this operation",
+ HTTPStatusCode: http.StatusForbidden,
+ },
+ ErrAccountNotEligible: {
+ Code: "XMinioInvalidIAMCredentials",
+ Description: "The account key is not eligible for this operation",
+ HTTPStatusCode: http.StatusForbidden,
+ },
+ ErrAdminServiceAccountNotFound: {
+ Code: "XMinioInvalidIAMCredentials",
+ Description: "The specified service account is not found",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrPostPolicyConditionInvalidFormat: {
+ Code: "PostPolicyInvalidKeyName",
+ Description: "Invalid according to Policy: Policy Condition failed",
+ HTTPStatusCode: http.StatusForbidden,
+ },
+ ErrInvalidChecksum: {
+ Code: "InvalidArgument",
+ Description: "Invalid checksum provided.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrLambdaARNInvalid: {
+ Code: "LambdaARNInvalid",
+ Description: "The specified lambda ARN is invalid",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrLambdaARNNotFound: {
+ Code: "LambdaARNNotFound",
+ Description: "The specified lambda ARN does not exist",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrPolicyAlreadyAttached: {
+ Code: "XMinioPolicyAlreadyAttached",
+ Description: "The specified policy is already attached.",
+ HTTPStatusCode: http.StatusConflict,
+ },
+ ErrPolicyNotAttached: {
+ Code: "XMinioPolicyNotAttached",
+ Description: "The specified policy is not found.",
+ HTTPStatusCode: http.StatusNotFound,
+ },
+ ErrInvalidAttributeName: {
+ Code: "InvalidArgument",
+ Description: "Invalid attribute name specified.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminLDAPNotEnabled: {
+ Code: "XMinioLDAPNotEnabled",
+ Description: "LDAP is not enabled. LDAP must be enabled to make LDAP requests.",
+ HTTPStatusCode: http.StatusNotImplemented,
+ },
+ ErrAdminLDAPExpectedLoginName: {
+ Code: "XMinioLDAPExpectedLoginName",
+ Description: "Expected LDAP short username but was given full DN.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAdminInvalidGroupName: {
+ Code: "XMinioInvalidGroupName",
+ Description: "The group name is invalid.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ ErrAddUserValidUTF: {
+ Code: "XMinioInvalidUTF",
+ Description: "Invalid UTF-8 character detected.",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+}
+
+// toAPIErrorCode - Converts embedded errors. Convenience
+// function written to handle all cases where we have known types of
+// errors returned by underlying layers.
+func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
+ if err == nil {
+ return ErrNone
+ }
+
+ // Errors that are generated by net.Conn and any context errors must be handled here.
+ if errors.Is(err, os.ErrDeadlineExceeded) || errors.Is(err, context.DeadlineExceeded) {
+ return ErrRequestTimedout
+ }
+
+ // Only return ErrClientDisconnected if the provided context is actually canceled.
+ // This way downstream context.Canceled will still report ErrRequestTimedout
+ if contextCanceled(ctx) && errors.Is(ctx.Err(), context.Canceled) {
+ return ErrClientDisconnected
+ }
+
+ // Unwrap the error first
+ err = unwrapAll(err)
+
+ switch err {
+ case errInvalidArgument:
+ apiErr = ErrAdminInvalidArgument
+ case errNoSuchPolicy:
+ apiErr = ErrAdminNoSuchPolicy
+ case errNoSuchUser:
+ apiErr = ErrAdminNoSuchUser
+ case errNoSuchUserLDAPWarn:
+ apiErr = ErrAdminNoSuchUserLDAPWarn
+ case errNoSuchServiceAccount:
+ apiErr = ErrAdminServiceAccountNotFound
+ case errNoSuchAccessKey:
+ apiErr = ErrAdminNoSuchAccessKey
+ case errNoSuchGroup:
+ apiErr = ErrAdminNoSuchGroup
+ case errGroupNotEmpty:
+ apiErr = ErrAdminGroupNotEmpty
+ case errGroupNameContainsReservedChars:
+ apiErr = ErrAdminInvalidGroupName
+ case errNoSuchJob:
+ apiErr = ErrAdminNoSuchJob
+ case errNoPolicyToAttachOrDetach:
+ apiErr = ErrAdminPolicyChangeAlreadyApplied
+ case errSignatureMismatch:
+ apiErr = ErrSignatureDoesNotMatch
+ case errInvalidRange:
+ apiErr = ErrInvalidRange
+ case errDataTooLarge:
+ apiErr = ErrEntityTooLarge
+ case errDataTooSmall:
+ apiErr = ErrEntityTooSmall
+ case errAuthentication:
+ apiErr = ErrAccessDenied
+ case auth.ErrContainsReservedChars:
+ apiErr = ErrAdminInvalidAccessKey
+ case auth.ErrInvalidAccessKeyLength:
+ apiErr = ErrAdminInvalidAccessKey
+ case auth.ErrInvalidSecretKeyLength:
+ apiErr = ErrAdminInvalidSecretKey
+ case auth.ErrNoAccessKeyWithSecretKey:
+ apiErr = ErrAdminNoAccessKey
+ case auth.ErrNoSecretKeyWithAccessKey:
+ apiErr = ErrAdminNoSecretKey
+ case errInvalidStorageClass:
+ apiErr = ErrInvalidStorageClass
+ case errErasureReadQuorum:
+ apiErr = ErrSlowDownRead
+ case errErasureWriteQuorum:
+ apiErr = ErrSlowDownWrite
+ case errMaxVersionsExceeded:
+ apiErr = ErrMaxVersionsExceeded
+ // SSE errors
+ case errInvalidEncryptionParameters:
+ apiErr = ErrInvalidEncryptionParameters
+ case errInvalidEncryptionParametersSSEC:
+ apiErr = ErrInvalidEncryptionParametersSSEC
+ case crypto.ErrInvalidEncryptionMethod:
+ apiErr = ErrInvalidEncryptionMethod
+ case crypto.ErrInvalidEncryptionKeyID:
+ apiErr = ErrInvalidEncryptionKeyID
+ case crypto.ErrInvalidCustomerAlgorithm:
+ apiErr = ErrInvalidSSECustomerAlgorithm
+ case crypto.ErrMissingCustomerKey:
+ apiErr = ErrMissingSSECustomerKey
+ case crypto.ErrMissingCustomerKeyMD5:
+ apiErr = ErrMissingSSECustomerKeyMD5
+ case crypto.ErrCustomerKeyMD5Mismatch:
+ apiErr = ErrSSECustomerKeyMD5Mismatch
+ case errObjectTampered:
+ apiErr = ErrObjectTampered
+ case errEncryptedObject:
+ apiErr = ErrSSEEncryptedObject
+ case errInvalidSSEParameters:
+ apiErr = ErrInvalidSSECustomerParameters
+ case crypto.ErrInvalidCustomerKey, crypto.ErrSecretKeyMismatch:
+ apiErr = ErrAccessDenied // no access without correct key
+ case crypto.ErrIncompatibleEncryptionMethod:
+ apiErr = ErrIncompatibleEncryptionMethod
+ case errKMSNotConfigured:
+ apiErr = ErrKMSNotConfigured
+ case errKMSKeyNotFound:
+ apiErr = ErrKMSKeyNotFoundException
+ case errKMSDefaultKeyAlreadyConfigured:
+ apiErr = ErrKMSDefaultKeyAlreadyConfigured
+ case context.Canceled:
+ apiErr = ErrClientDisconnected
+ case context.DeadlineExceeded:
+ apiErr = ErrRequestTimedout
+ case objectlock.ErrInvalidRetentionDate:
+ apiErr = ErrInvalidRetentionDate
+ case objectlock.ErrPastObjectLockRetainDate:
+ apiErr = ErrPastObjectLockRetainDate
+ case objectlock.ErrUnknownWORMModeDirective:
+ apiErr = ErrUnknownWORMModeDirective
+ case objectlock.ErrObjectLockInvalidHeaders:
+ apiErr = ErrObjectLockInvalidHeaders
+ case objectlock.ErrMalformedXML:
+ apiErr = ErrMalformedXML
+ case errInvalidMaxParts:
+ apiErr = ErrInvalidMaxParts
+ case ioutil.ErrOverread:
+ apiErr = ErrExcessData
+ case errServerNotInitialized:
+ apiErr = ErrServerNotInitialized
+ case errBucketMetadataNotInitialized:
+ apiErr = ErrBucketMetadataNotInitialized
+ case hash.ErrInvalidChecksum:
+ apiErr = ErrInvalidChecksum
+ }
+
+ // Compression errors
+ if err == errInvalidDecompressedSize {
+ apiErr = ErrInvalidDecompressedSize
+ }
+
+ if apiErr != ErrNone {
+ // If there was a match in the above switch case.
+ return apiErr
+ }
+
+ // etcd specific errors, a key is always a bucket for us return
+ // ErrNoSuchBucket in such a case.
+ if errors.Is(err, dns.ErrNoEntriesFound) {
+ return ErrNoSuchBucket
+ }
+
+ switch err.(type) {
+ case StorageFull:
+ apiErr = ErrStorageFull
+ case hash.BadDigest:
+ apiErr = ErrBadDigest
+ case AllAccessDisabled:
+ apiErr = ErrAllAccessDisabled
+ case IncompleteBody:
+ apiErr = ErrIncompleteBody
+ case ObjectExistsAsDirectory:
+ apiErr = ErrObjectExistsAsDirectory
+ case PrefixAccessDenied:
+ apiErr = ErrAccessDenied
+ case BucketNameInvalid:
+ apiErr = ErrInvalidBucketName
+ case BucketNotFound:
+ apiErr = ErrNoSuchBucket
+ case BucketAlreadyOwnedByYou:
+ apiErr = ErrBucketAlreadyOwnedByYou
+ case BucketNotEmpty:
+ apiErr = ErrBucketNotEmpty
+ case BucketAlreadyExists:
+ apiErr = ErrBucketAlreadyExists
+ case BucketExists:
+ apiErr = ErrBucketAlreadyOwnedByYou
+ case ObjectNotFound:
+ apiErr = ErrNoSuchKey
+ case MethodNotAllowed:
+ apiErr = ErrMethodNotAllowed
+ case ObjectLocked:
+ apiErr = ErrObjectLocked
+ case InvalidVersionID:
+ apiErr = ErrInvalidVersionID
+ case VersionNotFound:
+ apiErr = ErrNoSuchVersion
+ case ObjectAlreadyExists:
+ apiErr = ErrMethodNotAllowed
+ case ObjectNameInvalid:
+ apiErr = ErrInvalidObjectName
+ case ObjectNamePrefixAsSlash:
+ apiErr = ErrInvalidObjectNamePrefixSlash
+ case InvalidUploadID:
+ apiErr = ErrNoSuchUpload
+ case InvalidPart:
+ apiErr = ErrInvalidPart
+ case InsufficientWriteQuorum:
+ apiErr = ErrSlowDownWrite
+ case InsufficientReadQuorum:
+ apiErr = ErrSlowDownRead
+ case InvalidUploadIDKeyCombination:
+ apiErr = ErrNotImplemented
+ case MalformedUploadID:
+ apiErr = ErrNoSuchUpload
+ case PartTooSmall:
+ apiErr = ErrEntityTooSmall
+ case SignatureDoesNotMatch:
+ apiErr = ErrSignatureDoesNotMatch
+ case hash.SHA256Mismatch:
+ apiErr = ErrContentSHA256Mismatch
+ case hash.ChecksumMismatch:
+ apiErr = ErrContentChecksumMismatch
+ case hash.SizeTooSmall:
+ apiErr = ErrEntityTooSmall
+ case hash.SizeTooLarge:
+ apiErr = ErrEntityTooLarge
+ case NotImplemented:
+ apiErr = ErrNotImplemented
+ case PartTooBig:
+ apiErr = ErrEntityTooLarge
+ case UnsupportedMetadata:
+ apiErr = ErrUnsupportedMetadata
+ case BucketPolicyNotFound:
+ apiErr = ErrNoSuchBucketPolicy
+ case BucketLifecycleNotFound:
+ apiErr = ErrNoSuchLifecycleConfiguration
+ case BucketSSEConfigNotFound:
+ apiErr = ErrNoSuchBucketSSEConfig
+ case BucketTaggingNotFound:
+ apiErr = ErrBucketTaggingNotFound
+ case BucketObjectLockConfigNotFound:
+ apiErr = ErrObjectLockConfigurationNotFound
+ case BucketQuotaConfigNotFound:
+ apiErr = ErrAdminNoSuchQuotaConfiguration
+ case BucketReplicationConfigNotFound:
+ apiErr = ErrReplicationConfigurationNotFoundError
+ case BucketRemoteDestinationNotFound:
+ apiErr = ErrRemoteDestinationNotFoundError
+ case BucketRemoteTargetNotFound:
+ apiErr = ErrRemoteTargetNotFoundError
+ case RemoteTargetConnectionErr:
+ apiErr = ErrReplicationRemoteConnectionError
+ case BucketRemoteAlreadyExists:
+ apiErr = ErrBucketRemoteAlreadyExists
+ case BucketRemoteLabelInUse:
+ apiErr = ErrBucketRemoteLabelInUse
+ case BucketRemoteArnTypeInvalid:
+ apiErr = ErrBucketRemoteArnTypeInvalid
+ case BucketRemoteArnInvalid:
+ apiErr = ErrBucketRemoteArnInvalid
+ case BucketRemoteRemoveDisallowed:
+ apiErr = ErrBucketRemoteRemoveDisallowed
+ case BucketRemoteTargetNotVersioned:
+ apiErr = ErrRemoteTargetNotVersionedError
+ case BucketReplicationSourceNotVersioned:
+ apiErr = ErrReplicationSourceNotVersionedError
+ case TransitionStorageClassNotFound:
+ apiErr = ErrTransitionStorageClassNotFoundError
+ case InvalidObjectState:
+ apiErr = ErrInvalidObjectState
+ case PreConditionFailed:
+ apiErr = ErrPreconditionFailed
+ case BucketQuotaExceeded:
+ apiErr = ErrAdminBucketQuotaExceeded
+ case *event.ErrInvalidEventName:
+ apiErr = ErrEventNotification
+ case *event.ErrInvalidARN:
+ apiErr = ErrARNNotification
+ case *event.ErrARNNotFound:
+ apiErr = ErrARNNotification
+ case *levent.ErrInvalidARN:
+ apiErr = ErrLambdaARNInvalid
+ case *levent.ErrARNNotFound:
+ apiErr = ErrLambdaARNNotFound
+ case *event.ErrUnknownRegion:
+ apiErr = ErrRegionNotification
+ case *event.ErrInvalidFilterName:
+ apiErr = ErrFilterNameInvalid
+ case *event.ErrFilterNamePrefix:
+ apiErr = ErrFilterNamePrefix
+ case *event.ErrFilterNameSuffix:
+ apiErr = ErrFilterNameSuffix
+ case *event.ErrInvalidFilterValue:
+ apiErr = ErrFilterValueInvalid
+ case *event.ErrDuplicateEventName:
+ apiErr = ErrOverlappingConfigs
+ case *event.ErrDuplicateQueueConfiguration:
+ apiErr = ErrOverlappingFilterNotification
+ case *event.ErrUnsupportedConfiguration:
+ apiErr = ErrUnsupportedNotification
+ case OperationTimedOut:
+ apiErr = ErrRequestTimedout
+ case BackendDown:
+ apiErr = ErrBackendDown
+ case ObjectNameTooLong:
+ apiErr = ErrKeyTooLongError
+ case dns.ErrInvalidBucketName:
+ apiErr = ErrInvalidBucketName
+ case dns.ErrBucketConflict:
+ apiErr = ErrBucketAlreadyExists
+ default:
+ if strings.Contains(err.Error(), "request declared a Content-Length") {
+ apiErr = ErrIncompleteBody
+ } else {
+ apiErr = ErrInternalError
+ }
+ }
+
+ return apiErr
+}
+
+var noError = APIError{}
+
+// toAPIError - Converts embedded errors. Convenience
+// function written to handle all cases where we have known types of
+// errors returned by underlying layers.
+func toAPIError(ctx context.Context, err error) APIError {
+ if err == nil {
+ return noError
+ }
+
+ apiErr := errorCodes.ToAPIErr(toAPIErrorCode(ctx, err))
+ switch apiErr.Code {
+ case "NotImplemented":
+ apiErr = APIError{
+ Code: apiErr.Code,
+ Description: fmt.Sprintf("%s (%v)", apiErr.Description, err),
+ HTTPStatusCode: apiErr.HTTPStatusCode,
+ }
+ case "XMinioBackendDown":
+ apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, err)
+ case "InternalError":
+ // If we see an internal error try to interpret
+ // any underlying errors if possible depending on
+ // their internal error types.
+ switch e := err.(type) {
+ case kms.Error:
+ apiErr = APIError{
+ Code: e.APICode,
+ Description: e.Err,
+ HTTPStatusCode: e.Code,
+ }
+ case batchReplicationJobError:
+ apiErr = APIError{
+ Description: e.Description,
+ Code: e.Code,
+ HTTPStatusCode: e.HTTPStatusCode,
+ }
+ case InvalidRange:
+ apiErr = APIError{
+ Code: "InvalidRange",
+ Description: e.Error(),
+ HTTPStatusCode: errorCodes[ErrInvalidRange].HTTPStatusCode,
+ ObjectSize: strconv.FormatInt(e.ResourceSize, 10),
+ RangeRequested: fmt.Sprintf("%d-%d", e.OffsetBegin, e.OffsetEnd),
+ }
+ case InvalidArgument:
+ apiErr = APIError{
+ Code: "InvalidArgument",
+ Description: e.Error(),
+ HTTPStatusCode: errorCodes[ErrInvalidRequest].HTTPStatusCode,
+ }
+ case *xml.SyntaxError:
+ apiErr = APIError{
+ Code: "MalformedXML",
+ Description: fmt.Sprintf("%s (%s)", errorCodes[ErrMalformedXML].Description, e),
+ HTTPStatusCode: errorCodes[ErrMalformedXML].HTTPStatusCode,
+ }
+ case url.EscapeError:
+ apiErr = APIError{
+ Code: "XMinioInvalidObjectName",
+ Description: fmt.Sprintf("%s (%s)", errorCodes[ErrInvalidObjectName].Description, e),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ case versioning.Error:
+ apiErr = APIError{
+ Code: "IllegalVersioningConfigurationException",
+ Description: fmt.Sprintf("Versioning configuration specified in the request is invalid. (%s)", e),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ case lifecycle.Error:
+ apiErr = APIError{
+ Code: "InvalidArgument",
+ Description: e.Error(),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ case replication.Error:
+ apiErr = APIError{
+ Code: "MalformedXML",
+ Description: e.Error(),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ case tags.Error:
+ apiErr = APIError{
+ Code: e.Code(),
+ Description: e.Error(),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ case policy.Error:
+ apiErr = APIError{
+ Code: "MalformedPolicy",
+ Description: e.Error(),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ case crypto.Error:
+ apiErr = APIError{
+ Code: "XMinioEncryptionError",
+ Description: e.Error(),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ case minio.ErrorResponse:
+ apiErr = APIError{
+ Code: e.Code,
+ Description: e.Message,
+ HTTPStatusCode: e.StatusCode,
+ }
+ if strings.Contains(e.Message, "KMS is not configured") {
+ apiErr = APIError{
+ Code: "NotImplemented",
+ Description: e.Message,
+ HTTPStatusCode: http.StatusNotImplemented,
+ }
+ }
+ case *googleapi.Error:
+ apiErr = APIError{
+ Code: "XGCSInternalError",
+ Description: e.Message,
+ HTTPStatusCode: e.Code,
+ }
+ // GCS may send multiple errors, just pick the first one
+ // since S3 only sends one Error XML response.
+ if len(e.Errors) >= 1 {
+ apiErr.Code = e.Errors[0].Reason
+ }
+ case *azcore.ResponseError:
+ apiErr = APIError{
+ Code: e.ErrorCode,
+ Description: e.Error(),
+ HTTPStatusCode: e.StatusCode,
+ }
+ // Add more other SDK related errors here if any in future.
+ default:
+ //nolint:gocritic
+ if errors.Is(err, errMalformedEncoding) || errors.Is(err, errChunkTooBig) || errors.Is(err, strconv.ErrRange) {
+ apiErr = APIError{
+ Code: "BadRequest",
+ Description: err.Error(),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ } else {
+ apiErr = APIError{
+ Code: apiErr.Code,
+ Description: fmt.Sprintf("%s: cause(%v)", apiErr.Description, err),
+ HTTPStatusCode: apiErr.HTTPStatusCode,
+ }
+ }
+ }
+ }
+
+ if apiErr.Code == "InternalError" {
+ // Make sure to log the errors which we cannot translate
+ // to a meaningful S3 API errors. This is added to aid in
+ // debugging unexpected/unhandled errors.
+ internalLogIf(ctx, err)
+ }
+
+ return apiErr
+}
+
+// getAPIError provides API Error for input API error code.
+func getAPIError(code APIErrorCode) APIError {
+ if apiErr, ok := errorCodes[code]; ok {
+ return apiErr
+ }
+ return errorCodes.ToAPIErr(ErrInternalError)
+}
+
+// getAPIErrorResponse gets in standard error and resource value and
+// provides a encodable populated response values
+func getAPIErrorResponse(ctx context.Context, err APIError, resource, requestID, hostID string) APIErrorResponse {
+ reqInfo := logger.GetReqInfo(ctx)
+ return APIErrorResponse{
+ Code: err.Code,
+ Message: err.Description,
+ BucketName: reqInfo.BucketName,
+ Key: reqInfo.ObjectName,
+ Resource: resource,
+ Region: globalSite.Region(),
+ RequestID: requestID,
+ HostID: hostID,
+ ActualObjectSize: err.ObjectSize,
+ RangeRequested: err.RangeRequested,
+ }
+}
diff --git a/cmd/api-errors_test.go b/cmd/api-errors_test.go
new file mode 100644
index 00000000..fda913b6
--- /dev/null
+++ b/cmd/api-errors_test.go
@@ -0,0 +1,89 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/minio/minio/internal/crypto"
+ "github.com/minio/minio/internal/hash"
+)
+
+var toAPIErrorTests = []struct {
+ err error
+ errCode APIErrorCode
+}{
+ {err: hash.BadDigest{}, errCode: ErrBadDigest},
+ {err: hash.SHA256Mismatch{}, errCode: ErrContentSHA256Mismatch},
+ {err: IncompleteBody{}, errCode: ErrIncompleteBody},
+ {err: ObjectExistsAsDirectory{}, errCode: ErrObjectExistsAsDirectory},
+ {err: BucketNameInvalid{}, errCode: ErrInvalidBucketName},
+ {err: BucketExists{}, errCode: ErrBucketAlreadyOwnedByYou},
+ {err: ObjectNotFound{}, errCode: ErrNoSuchKey},
+ {err: ObjectNameInvalid{}, errCode: ErrInvalidObjectName},
+ {err: InvalidUploadID{}, errCode: ErrNoSuchUpload},
+ {err: InvalidPart{}, errCode: ErrInvalidPart},
+ {err: InsufficientReadQuorum{}, errCode: ErrSlowDownRead},
+ {err: InsufficientWriteQuorum{}, errCode: ErrSlowDownWrite},
+ {err: InvalidUploadIDKeyCombination{}, errCode: ErrNotImplemented},
+ {err: MalformedUploadID{}, errCode: ErrNoSuchUpload},
+ {err: PartTooSmall{}, errCode: ErrEntityTooSmall},
+ {err: BucketNotEmpty{}, errCode: ErrBucketNotEmpty},
+ {err: BucketNotFound{}, errCode: ErrNoSuchBucket},
+ {err: StorageFull{}, errCode: ErrStorageFull},
+ {err: NotImplemented{}, errCode: ErrNotImplemented},
+ {err: errSignatureMismatch, errCode: ErrSignatureDoesNotMatch},
+
+ // SSE-C errors
+ {err: crypto.ErrInvalidCustomerAlgorithm, errCode: ErrInvalidSSECustomerAlgorithm},
+ {err: crypto.ErrMissingCustomerKey, errCode: ErrMissingSSECustomerKey},
+ {err: crypto.ErrInvalidCustomerKey, errCode: ErrAccessDenied},
+ {err: crypto.ErrMissingCustomerKeyMD5, errCode: ErrMissingSSECustomerKeyMD5},
+ {err: crypto.ErrCustomerKeyMD5Mismatch, errCode: ErrSSECustomerKeyMD5Mismatch},
+ {err: errObjectTampered, errCode: ErrObjectTampered},
+
+ {err: nil, errCode: ErrNone},
+ {err: errors.New("Custom error"), errCode: ErrInternalError}, // Case where err type is unknown.
+}
+
+func TestAPIErrCode(t *testing.T) {
+ ctx := t.Context()
+ for i, testCase := range toAPIErrorTests {
+ errCode := toAPIErrorCode(ctx, testCase.err)
+ if errCode != testCase.errCode {
+ t.Errorf("Test %d: Expected error code %d, got %d", i+1, testCase.errCode, errCode)
+ }
+ }
+}
+
+// Check if an API error is properly defined
+func TestAPIErrCodeDefinition(t *testing.T) {
+ for errAPI := ErrNone + 1; errAPI < apiErrCodeEnd; errAPI++ {
+ errCode, ok := errorCodes[errAPI]
+ if !ok {
+ t.Fatal(errAPI, "error code is not defined in the API error code table")
+ }
+ if errCode.Code == "" {
+ t.Fatal(errAPI, "error code has an empty XML code")
+ }
+ if errCode.HTTPStatusCode == 0 {
+ t.Fatal(errAPI, "error code has a zero HTTP status code")
+ }
+ }
+}
diff --git a/cmd/api-headers.go b/cmd/api-headers.go
new file mode 100644
index 00000000..c2ca23fb
--- /dev/null
+++ b/cmd/api-headers.go
@@ -0,0 +1,269 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "encoding/xml"
+ "fmt"
+ "mime"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/minio/minio-go/v7/pkg/tags"
+ "github.com/minio/minio/internal/crypto"
+ xhttp "github.com/minio/minio/internal/http"
+ xxml "github.com/minio/xxml"
+)
+
+// Returns a hexadecimal representation of time at the
+// time response is sent to the client.
+func mustGetRequestID(t time.Time) string {
+ return fmt.Sprintf("%X", t.UnixNano())
+}
+
+// setEventStreamHeaders to allow proxies to avoid buffering proxy responses
+func setEventStreamHeaders(w http.ResponseWriter) {
+ w.Header().Set(xhttp.ContentType, "text/event-stream")
+ w.Header().Set(xhttp.CacheControl, "no-cache") // nginx to turn off buffering
+ w.Header().Set("X-Accel-Buffering", "no") // nginx to turn off buffering
+}
+
+// Write http common headers
+func setCommonHeaders(w http.ResponseWriter) {
+ // Set the "Server" http header.
+ w.Header().Set(xhttp.ServerInfo, MinioStoreName)
+
+ // Set `x-amz-bucket-region` only if region is set on the server
+ // by default minio uses an empty region.
+ if region := globalSite.Region(); region != "" {
+ w.Header().Set(xhttp.AmzBucketRegion, region)
+ }
+ w.Header().Set(xhttp.AcceptRanges, "bytes")
+
+ // Remove sensitive information
+ crypto.RemoveSensitiveHeaders(w.Header())
+}
+
+// Encodes the response headers into XML format.
+func encodeResponse(response any) []byte {
+ var buf bytes.Buffer
+ buf.WriteString(xml.Header)
+ if err := xml.NewEncoder(&buf).Encode(response); err != nil {
+ bugLogIf(GlobalContext, err)
+ return nil
+ }
+ return buf.Bytes()
+}
+
+// Use this encodeResponseList() to support control characters
+// this function must be used by only ListObjects() for objects
+// with control characters, this is a specialized extension
+// to support AWS S3 compatible behavior.
+//
+// Do not use this function for anything other than ListObjects()
+// variants, please open a github discussion if you wish to use
+// this in other places.
+func encodeResponseList(response any) []byte {
+ var buf bytes.Buffer
+ buf.WriteString(xxml.Header)
+ if err := xxml.NewEncoder(&buf).Encode(response); err != nil {
+ bugLogIf(GlobalContext, err)
+ return nil
+ }
+ return buf.Bytes()
+}
+
+// Encodes the response headers into JSON format.
+func encodeResponseJSON(response any) []byte {
+ var bytesBuffer bytes.Buffer
+ e := json.NewEncoder(&bytesBuffer)
+ e.Encode(response)
+ return bytesBuffer.Bytes()
+}
+
+// Write parts count
+func setPartsCountHeaders(w http.ResponseWriter, objInfo ObjectInfo) {
+ if strings.Contains(objInfo.ETag, "-") && len(objInfo.Parts) > 0 {
+ w.Header()[xhttp.AmzMpPartsCount] = []string{strconv.Itoa(len(objInfo.Parts))}
+ }
+}
+
+// Write object header
+func setObjectHeaders(ctx context.Context, w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSpec, opts ObjectOptions) (err error) {
+ // set common headers
+ setCommonHeaders(w)
+
+ // Set last modified time.
+ lastModified := objInfo.ModTime.UTC().Format(http.TimeFormat)
+ w.Header().Set(xhttp.LastModified, lastModified)
+
+ // Set Etag if available.
+ if objInfo.ETag != "" {
+ w.Header()[xhttp.ETag] = []string{"\"" + objInfo.ETag + "\""}
+ }
+
+ if objInfo.ContentType != "" {
+ w.Header().Set(xhttp.ContentType, objInfo.ContentType)
+ }
+
+ if objInfo.ContentEncoding != "" {
+ w.Header().Set(xhttp.ContentEncoding, objInfo.ContentEncoding)
+ }
+
+ if !objInfo.Expires.IsZero() {
+ w.Header().Set(xhttp.Expires, objInfo.Expires.UTC().Format(http.TimeFormat))
+ }
+
+ // Set tag count if object has tags
+ if len(objInfo.UserTags) > 0 {
+ tags, _ := tags.ParseObjectTags(objInfo.UserTags)
+ if tags != nil && tags.Count() > 0 {
+ w.Header()[xhttp.AmzTagCount] = []string{strconv.Itoa(tags.Count())}
+ if opts.Tagging {
+ // This is MinIO only extension to return back tags along with the count.
+ w.Header()[xhttp.AmzObjectTagging] = []string{objInfo.UserTags}
+ }
+ }
+ }
+
+ // Set all other user defined metadata.
+ for k, v := range objInfo.UserDefined {
+ // Empty values for object lock and retention can be skipped.
+ if v == "" && equals(k, xhttp.AmzObjectLockMode, xhttp.AmzObjectLockRetainUntilDate) {
+ continue
+ }
+
+ if stringsHasPrefixFold(k, ReservedMetadataPrefixLower) {
+ // Do not need to send any internal metadata
+ // values to client.
+ continue
+ }
+
+ // https://github.com/google/security-research/security/advisories/GHSA-76wf-9vgp-pj7w
+ if equals(k, xhttp.AmzMetaUnencryptedContentLength, xhttp.AmzMetaUnencryptedContentMD5) {
+ continue
+ }
+
+ var isSet bool
+ for _, userMetadataPrefix := range userMetadataKeyPrefixes {
+ if !stringsHasPrefixFold(k, userMetadataPrefix) {
+ continue
+ }
+ // check the doc https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingMetadata.html
+ // For metadata values like "ö", "ÄMÄZÕÑ S3", and "öha, das sollte eigentlich
+ // funktionieren", tested against a real AWS S3 bucket, S3 may encode incorrectly. For
+ // example, "ö" was encoded as =?UTF-8?B?w4PCtg==?=, producing invalid UTF-8 instead
+ // of =?UTF-8?B?w7Y=?=. This mirrors errors like the ä½ in another string.
+ //
+ // S3 uses B-encoding (Base64) for non-ASCII-heavy metadata and Q-encoding
+ // (quoted-printable) for mostly ASCII strings. Long strings are split at word
+ // boundaries to fit RFC 2047’s 75-character limit, ensuring HTTP parser
+ // compatibility.
+ //
+ // However, this splitting increases header size and can introduce errors, unlike Go’s
+ // mime package in MinIO, which correctly encodes strings with fixed B/Q encodings,
+ // avoiding S3’s heuristic-driven issues.
+ //
+ // For MinIO developers, decode S3 metadata with mime.WordDecoder, validate outputs,
+ // report encoding bugs to AWS, and use ASCII-only metadata to ensure reliable S3 API
+ // compatibility.
+ if needsMimeEncoding(v) {
+ // see https://github.com/golang/go/blob/release-branch.go1.24/src/net/mail/message.go#L325
+ if strings.ContainsAny(v, "\"#$%&'(),.:;<>@[]^`{|}~") {
+ v = mime.BEncoding.Encode("UTF-8", v)
+ } else {
+ v = mime.QEncoding.Encode("UTF-8", v)
+ }
+ }
+ w.Header()[strings.ToLower(k)] = []string{v}
+ isSet = true
+ break
+ }
+
+ if !isSet {
+ w.Header().Set(k, v)
+ }
+ }
+
+ var start, rangeLen int64
+ totalObjectSize, err := objInfo.GetActualSize()
+ if err != nil {
+ return err
+ }
+
+ if rs == nil && opts.PartNumber > 0 {
+ rs = partNumberToRangeSpec(objInfo, opts.PartNumber)
+ }
+
+ // For providing ranged content
+ start, rangeLen, err = rs.GetOffsetLength(totalObjectSize)
+ if err != nil {
+ return err
+ }
+
+ // Set content length.
+ w.Header().Set(xhttp.ContentLength, strconv.FormatInt(rangeLen, 10))
+ if rs != nil {
+ contentRange := fmt.Sprintf("bytes %d-%d/%d", start, start+rangeLen-1, totalObjectSize)
+ w.Header().Set(xhttp.ContentRange, contentRange)
+ }
+
+ // Set the relevant version ID as part of the response header.
+ if objInfo.VersionID != "" && objInfo.VersionID != nullVersionID {
+ w.Header()[xhttp.AmzVersionID] = []string{objInfo.VersionID}
+ }
+
+ if objInfo.ReplicationStatus.String() != "" {
+ w.Header()[xhttp.AmzBucketReplicationStatus] = []string{objInfo.ReplicationStatus.String()}
+ }
+
+ if objInfo.IsRemote() {
+ // Check if object is being restored. For more information on x-amz-restore header see
+ // https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html#API_HeadObject_ResponseSyntax
+ w.Header()[xhttp.AmzStorageClass] = []string{filterStorageClass(ctx, objInfo.TransitionedObject.Tier)}
+ }
+
+ if lc, err := globalLifecycleSys.Get(objInfo.Bucket); err == nil {
+ lc.SetPredictionHeaders(w, objInfo.ToLifecycleOpts())
+ }
+
+ if v, ok := objInfo.UserDefined[ReservedMetadataPrefix+"compression"]; ok {
+ if i := strings.LastIndexByte(v, '/'); i >= 0 {
+ v = v[i+1:]
+ }
+ w.Header()[xhttp.MinIOCompressed] = []string{v}
+ }
+
+ return nil
+}
+
+// needsEncoding reports whether s contains any bytes that need to be encoded.
+// see mime.needsEncoding
+func needsMimeEncoding(s string) bool {
+ for _, b := range s {
+ if (b < ' ' || b > '~') && b != '\t' {
+ return true
+ }
+ }
+ return false
+}
diff --git a/cmd/api-headers_test.go b/cmd/api-headers_test.go
new file mode 100644
index 00000000..9db65460
--- /dev/null
+++ b/cmd/api-headers_test.go
@@ -0,0 +1,42 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "testing"
+)
+
+func TestNewRequestID(t *testing.T) {
+ // Ensure that it returns an alphanumeric result of length 16.
+ id := mustGetRequestID(UTCNow())
+
+ if len(id) != 16 {
+ t.Fail()
+ }
+
+ var e rune
+ for _, char := range id {
+ e = char
+
+ // Ensure that it is alphanumeric, in this case, between 0-9 and A-Z.
+ isAlnum := ('0' <= e && e <= '9') || ('A' <= e && e <= 'Z')
+ if !isAlnum {
+ t.Fail()
+ }
+ }
+}
diff --git a/cmd/api-resources.go b/cmd/api-resources.go
new file mode 100644
index 00000000..11cf53ad
--- /dev/null
+++ b/cmd/api-resources.go
@@ -0,0 +1,153 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "encoding/base64"
+ "net/url"
+ "strconv"
+)
+
+// Parse bucket url queries
+func getListObjectsV1Args(values url.Values) (prefix, marker, delimiter string, maxkeys int, encodingType string, errCode APIErrorCode) {
+ errCode = ErrNone
+
+ if values.Get("max-keys") != "" {
+ var err error
+ if maxkeys, err = strconv.Atoi(values.Get("max-keys")); err != nil {
+ errCode = ErrInvalidMaxKeys
+ return prefix, marker, delimiter, maxkeys, encodingType, errCode
+ }
+ } else {
+ maxkeys = maxObjectList
+ }
+
+ prefix = values.Get("prefix")
+ marker = values.Get("marker")
+ delimiter = values.Get("delimiter")
+ encodingType = values.Get("encoding-type")
+ return prefix, marker, delimiter, maxkeys, encodingType, errCode
+}
+
+func getListBucketObjectVersionsArgs(values url.Values) (prefix, marker, delimiter string, maxkeys int, encodingType, versionIDMarker string, errCode APIErrorCode) {
+ errCode = ErrNone
+
+ if values.Get("max-keys") != "" {
+ var err error
+ if maxkeys, err = strconv.Atoi(values.Get("max-keys")); err != nil {
+ errCode = ErrInvalidMaxKeys
+ return prefix, marker, delimiter, maxkeys, encodingType, versionIDMarker, errCode
+ }
+ } else {
+ maxkeys = maxObjectList
+ }
+
+ prefix = values.Get("prefix")
+ marker = values.Get("key-marker")
+ delimiter = values.Get("delimiter")
+ encodingType = values.Get("encoding-type")
+ versionIDMarker = values.Get("version-id-marker")
+ return prefix, marker, delimiter, maxkeys, encodingType, versionIDMarker, errCode
+}
+
+// Parse bucket url queries for ListObjects V2.
+func getListObjectsV2Args(values url.Values) (prefix, token, startAfter, delimiter string, fetchOwner bool, maxkeys int, encodingType string, errCode APIErrorCode) {
+ errCode = ErrNone
+
+ // The continuation-token cannot be empty.
+ if val, ok := values["continuation-token"]; ok {
+ if len(val[0]) == 0 {
+ errCode = ErrIncorrectContinuationToken
+ return prefix, token, startAfter, delimiter, fetchOwner, maxkeys, encodingType, errCode
+ }
+ }
+
+ if values.Get("max-keys") != "" {
+ var err error
+ if maxkeys, err = strconv.Atoi(values.Get("max-keys")); err != nil {
+ errCode = ErrInvalidMaxKeys
+ return prefix, token, startAfter, delimiter, fetchOwner, maxkeys, encodingType, errCode
+ }
+ } else {
+ maxkeys = maxObjectList
+ }
+
+ prefix = values.Get("prefix")
+ startAfter = values.Get("start-after")
+ delimiter = values.Get("delimiter")
+ fetchOwner = values.Get("fetch-owner") == "true"
+ encodingType = values.Get("encoding-type")
+
+ if token = values.Get("continuation-token"); token != "" {
+ decodedToken, err := base64.StdEncoding.DecodeString(token)
+ if err != nil {
+ errCode = ErrIncorrectContinuationToken
+ return prefix, token, startAfter, delimiter, fetchOwner, maxkeys, encodingType, errCode
+ }
+ token = string(decodedToken)
+ }
+ return prefix, token, startAfter, delimiter, fetchOwner, maxkeys, encodingType, errCode
+}
+
+// Parse bucket url queries for ?uploads
+func getBucketMultipartResources(values url.Values) (prefix, keyMarker, uploadIDMarker, delimiter string, maxUploads int, encodingType string, errCode APIErrorCode) {
+ errCode = ErrNone
+
+ if values.Get("max-uploads") != "" {
+ var err error
+ if maxUploads, err = strconv.Atoi(values.Get("max-uploads")); err != nil {
+ errCode = ErrInvalidMaxUploads
+ return prefix, keyMarker, uploadIDMarker, delimiter, maxUploads, encodingType, errCode
+ }
+ } else {
+ maxUploads = maxUploadsList
+ }
+
+ prefix = values.Get("prefix")
+ keyMarker = values.Get("key-marker")
+ uploadIDMarker = values.Get("upload-id-marker")
+ delimiter = values.Get("delimiter")
+ encodingType = values.Get("encoding-type")
+ return prefix, keyMarker, uploadIDMarker, delimiter, maxUploads, encodingType, errCode
+}
+
+// Parse object url queries
+func getObjectResources(values url.Values) (uploadID string, partNumberMarker, maxParts int, encodingType string, errCode APIErrorCode) {
+ var err error
+ errCode = ErrNone
+
+ if values.Get("max-parts") != "" {
+ if maxParts, err = strconv.Atoi(values.Get("max-parts")); err != nil {
+ errCode = ErrInvalidMaxParts
+ return uploadID, partNumberMarker, maxParts, encodingType, errCode
+ }
+ } else {
+ maxParts = maxPartsList
+ }
+
+ if values.Get("part-number-marker") != "" {
+ if partNumberMarker, err = strconv.Atoi(values.Get("part-number-marker")); err != nil {
+ errCode = ErrInvalidPartNumberMarker
+ return uploadID, partNumberMarker, maxParts, encodingType, errCode
+ }
+ }
+
+ uploadID = values.Get("uploadId")
+ encodingType = values.Get("encoding-type")
+ return uploadID, partNumberMarker, maxParts, encodingType, errCode
+}
diff --git a/cmd/api-resources_test.go b/cmd/api-resources_test.go
new file mode 100644
index 00000000..a9aac0b6
--- /dev/null
+++ b/cmd/api-resources_test.go
@@ -0,0 +1,222 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "net/url"
+ "testing"
+)
+
+// Test list objects resources V2.
+func TestListObjectsV2Resources(t *testing.T) {
+ testCases := []struct {
+ values url.Values
+ prefix, token, startAfter, delimiter string
+ fetchOwner bool
+ maxKeys int
+ encodingType string
+ errCode APIErrorCode
+ }{
+ {
+ values: url.Values{
+ "prefix": []string{"photos/"},
+ "continuation-token": []string{"dG9rZW4="},
+ "start-after": []string{"start-after"},
+ "delimiter": []string{SlashSeparator},
+ "fetch-owner": []string{"true"},
+ "max-keys": []string{"100"},
+ "encoding-type": []string{"gzip"},
+ },
+ prefix: "photos/",
+ token: "token",
+ startAfter: "start-after",
+ delimiter: SlashSeparator,
+ fetchOwner: true,
+ maxKeys: 100,
+ encodingType: "gzip",
+ errCode: ErrNone,
+ },
+ {
+ values: url.Values{
+ "prefix": []string{"photos/"},
+ "continuation-token": []string{"dG9rZW4="},
+ "start-after": []string{"start-after"},
+ "delimiter": []string{SlashSeparator},
+ "fetch-owner": []string{"true"},
+ "encoding-type": []string{"gzip"},
+ },
+ prefix: "photos/",
+ token: "token",
+ startAfter: "start-after",
+ delimiter: SlashSeparator,
+ fetchOwner: true,
+ maxKeys: maxObjectList,
+ encodingType: "gzip",
+ errCode: ErrNone,
+ },
+ {
+ values: url.Values{
+ "prefix": []string{"photos/"},
+ "continuation-token": []string{""},
+ "start-after": []string{"start-after"},
+ "delimiter": []string{SlashSeparator},
+ "fetch-owner": []string{"true"},
+ "encoding-type": []string{"gzip"},
+ },
+ prefix: "",
+ token: "",
+ startAfter: "",
+ delimiter: "",
+ fetchOwner: false,
+ maxKeys: 0,
+ encodingType: "",
+ errCode: ErrIncorrectContinuationToken,
+ },
+ }
+
+ for i, testCase := range testCases {
+ prefix, token, startAfter, delimiter, fetchOwner, maxKeys, encodingType, errCode := getListObjectsV2Args(testCase.values)
+
+ if errCode != testCase.errCode {
+ t.Errorf("Test %d: Expected error code:%d, got %d", i+1, testCase.errCode, errCode)
+ }
+ if prefix != testCase.prefix {
+ t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.prefix, prefix)
+ }
+ if token != testCase.token {
+ t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.token, token)
+ }
+ if startAfter != testCase.startAfter {
+ t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.startAfter, startAfter)
+ }
+ if delimiter != testCase.delimiter {
+ t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.delimiter, delimiter)
+ }
+ if fetchOwner != testCase.fetchOwner {
+ t.Errorf("Test %d: Expected %t, got %t", i+1, testCase.fetchOwner, fetchOwner)
+ }
+ if maxKeys != testCase.maxKeys {
+ t.Errorf("Test %d: Expected %d, got %d", i+1, testCase.maxKeys, maxKeys)
+ }
+ if encodingType != testCase.encodingType {
+ t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.encodingType, encodingType)
+ }
+ }
+}
+
+// Test list objects resources V1.
+func TestListObjectsV1Resources(t *testing.T) {
+ testCases := []struct {
+ values url.Values
+ prefix, marker, delimiter string
+ maxKeys int
+ encodingType string
+ }{
+ {
+ values: url.Values{
+ "prefix": []string{"photos/"},
+ "marker": []string{"test"},
+ "delimiter": []string{SlashSeparator},
+ "max-keys": []string{"100"},
+ "encoding-type": []string{"gzip"},
+ },
+ prefix: "photos/",
+ marker: "test",
+ delimiter: SlashSeparator,
+ maxKeys: 100,
+ encodingType: "gzip",
+ },
+ {
+ values: url.Values{
+ "prefix": []string{"photos/"},
+ "marker": []string{"test"},
+ "delimiter": []string{SlashSeparator},
+ "encoding-type": []string{"gzip"},
+ },
+ prefix: "photos/",
+ marker: "test",
+ delimiter: SlashSeparator,
+ maxKeys: maxObjectList,
+ encodingType: "gzip",
+ },
+ }
+
+ for i, testCase := range testCases {
+ prefix, marker, delimiter, maxKeys, encodingType, argsErr := getListObjectsV1Args(testCase.values)
+ if argsErr != ErrNone {
+ t.Errorf("Test %d: argument parsing failed, got %v", i+1, argsErr)
+ }
+ if prefix != testCase.prefix {
+ t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.prefix, prefix)
+ }
+ if marker != testCase.marker {
+ t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.marker, marker)
+ }
+ if delimiter != testCase.delimiter {
+ t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.delimiter, delimiter)
+ }
+ if maxKeys != testCase.maxKeys {
+ t.Errorf("Test %d: Expected %d, got %d", i+1, testCase.maxKeys, maxKeys)
+ }
+ if encodingType != testCase.encodingType {
+ t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.encodingType, encodingType)
+ }
+ }
+}
+
+// Validates extracting information for object resources.
+func TestGetObjectsResources(t *testing.T) {
+ testCases := []struct {
+ values url.Values
+ uploadID string
+ partNumberMarker, maxParts int
+ encodingType string
+ }{
+ {
+ values: url.Values{
+ "uploadId": []string{"11123-11312312311231-12313"},
+ "part-number-marker": []string{"1"},
+ "max-parts": []string{"1000"},
+ "encoding-type": []string{"gzip"},
+ },
+ uploadID: "11123-11312312311231-12313",
+ partNumberMarker: 1,
+ maxParts: 1000,
+ encodingType: "gzip",
+ },
+ }
+
+ for i, testCase := range testCases {
+ uploadID, partNumberMarker, maxParts, encodingType, argsErr := getObjectResources(testCase.values)
+ if argsErr != ErrNone {
+ t.Errorf("Test %d: argument parsing failed, got %v", i+1, argsErr)
+ }
+ if uploadID != testCase.uploadID {
+ t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.uploadID, uploadID)
+ }
+ if partNumberMarker != testCase.partNumberMarker {
+ t.Errorf("Test %d: Expected %d, got %d", i+1, testCase.partNumberMarker, partNumberMarker)
+ }
+ if maxParts != testCase.maxParts {
+ t.Errorf("Test %d: Expected %d, got %d", i+1, testCase.maxParts, maxParts)
+ }
+ if encodingType != testCase.encodingType {
+ t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.encodingType, encodingType)
+ }
+ }
+}
diff --git a/cmd/api-response.go b/cmd/api-response.go
new file mode 100644
index 00000000..28501b5b
--- /dev/null
+++ b/cmd/api-response.go
@@ -0,0 +1,1017 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/base64"
+ "encoding/xml"
+ "fmt"
+ "net/http"
+ "net/url"
+ "path"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/minio/minio/internal/amztime"
+ "github.com/minio/minio/internal/crypto"
+ "github.com/minio/minio/internal/handlers"
+ "github.com/minio/minio/internal/hash"
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/policy"
+ xxml "github.com/minio/xxml"
+)
+
+const (
+ maxObjectList = 1000 // Limit number of objects in a listObjectsResponse/listObjectsVersionsResponse.
+ maxDeleteList = 1000 // Limit number of objects deleted in a delete call.
+ maxUploadsList = 10000 // Limit number of uploads in a listUploadsResponse.
+ maxPartsList = 10000 // Limit number of parts in a listPartsResponse.
+)
+
+// LocationResponse - format for location response.
+type LocationResponse struct {
+ XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ LocationConstraint" json:"-"`
+ Location string `xml:",chardata"`
+}
+
+// PolicyStatus captures information returned by GetBucketPolicyStatusHandler
+type PolicyStatus struct {
+ XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ PolicyStatus" json:"-"`
+ IsPublic string
+}
+
+// ListVersionsResponse - format for list bucket versions response.
+type ListVersionsResponse struct {
+ XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListVersionsResult" json:"-"`
+
+ Name string
+ Prefix string
+ KeyMarker string
+
+ // When response is truncated (the IsTruncated element value in the response
+ // is true), you can use the key name in this field as marker in the subsequent
+ // request to get next set of objects. Server lists objects in alphabetical
+ // order Note: This element is returned only if you have delimiter request parameter
+ // specified. If response does not include the NextMaker and it is truncated,
+ // you can use the value of the last Key in the response as the marker in the
+ // subsequent request to get the next set of object keys.
+ NextKeyMarker string `xml:"NextKeyMarker,omitempty"`
+
+ // When the number of responses exceeds the value of MaxKeys,
+ // NextVersionIdMarker specifies the first object version not
+ // returned that satisfies the search criteria. Use this value
+ // for the version-id-marker request parameter in a subsequent request.
+ NextVersionIDMarker string `xml:"NextVersionIdMarker"`
+
+ // Marks the last version of the Key returned in a truncated response.
+ VersionIDMarker string `xml:"VersionIdMarker"`
+
+ MaxKeys int
+ Delimiter string `xml:"Delimiter,omitempty"`
+ // A flag that indicates whether or not ListObjects returned all of the results
+ // that satisfied the search criteria.
+ IsTruncated bool
+
+ CommonPrefixes []CommonPrefix
+ Versions []ObjectVersion
+
+ // Encoding type used to encode object keys in the response.
+ EncodingType string `xml:"EncodingType,omitempty"`
+}
+
+// ListObjectsResponse - format for list objects response.
+type ListObjectsResponse struct {
+ XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListBucketResult" json:"-"`
+
+ Name string
+ Prefix string
+ Marker string
+
+ // When response is truncated (the IsTruncated element value in the response
+ // is true), you can use the key name in this field as marker in the subsequent
+ // request to get next set of objects. Server lists objects in alphabetical
+ // order Note: This element is returned only if you have delimiter request parameter
+ // specified. If response does not include the NextMaker and it is truncated,
+ // you can use the value of the last Key in the response as the marker in the
+ // subsequent request to get the next set of object keys.
+ NextMarker string `xml:"NextMarker,omitempty"`
+
+ MaxKeys int
+ Delimiter string `xml:"Delimiter,omitempty"`
+ // A flag that indicates whether or not ListObjects returned all of the results
+ // that satisfied the search criteria.
+ IsTruncated bool
+
+ Contents []Object
+ CommonPrefixes []CommonPrefix
+
+ // Encoding type used to encode object keys in the response.
+ EncodingType string `xml:"EncodingType,omitempty"`
+}
+
+// ListObjectsV2Response - format for list objects response.
+type ListObjectsV2Response struct {
+ XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListBucketResult" json:"-"`
+
+ Name string
+ Prefix string
+ StartAfter string `xml:"StartAfter,omitempty"`
+ // When response is truncated (the IsTruncated element value in the response
+ // is true), you can use the key name in this field as marker in the subsequent
+ // request to get next set of objects. Server lists objects in alphabetical
+ // order Note: This element is returned only if you have delimiter request parameter
+ // specified. If response does not include the NextMaker and it is truncated,
+ // you can use the value of the last Key in the response as the marker in the
+ // subsequent request to get the next set of object keys.
+ ContinuationToken string `xml:"ContinuationToken,omitempty"`
+ NextContinuationToken string `xml:"NextContinuationToken,omitempty"`
+
+ KeyCount int
+ MaxKeys int
+ Delimiter string `xml:"Delimiter,omitempty"`
+ // A flag that indicates whether or not ListObjects returned all of the results
+ // that satisfied the search criteria.
+ IsTruncated bool
+
+ Contents []Object
+ CommonPrefixes []CommonPrefix
+
+ // Encoding type used to encode object keys in the response.
+ EncodingType string `xml:"EncodingType,omitempty"`
+}
+
+// Part container for part metadata.
+type Part struct {
+ PartNumber int
+ LastModified string
+ ETag string
+ Size int64
+
+ // Checksum values
+ ChecksumCRC32 string `xml:"ChecksumCRC32,omitempty"`
+ ChecksumCRC32C string `xml:"ChecksumCRC32C,omitempty"`
+ ChecksumSHA1 string `xml:"ChecksumSHA1,omitempty"`
+ ChecksumSHA256 string `xml:"ChecksumSHA256,omitempty"`
+ ChecksumCRC64NVME string `xml:",omitempty"`
+}
+
+// ListPartsResponse - format for list parts response.
+type ListPartsResponse struct {
+ XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListPartsResult" json:"-"`
+
+ Bucket string
+ Key string
+ UploadID string `xml:"UploadId"`
+
+ Initiator Initiator
+ Owner Owner
+
+ // The class of storage used to store the object.
+ StorageClass string
+
+ PartNumberMarker int
+ NextPartNumberMarker int
+ MaxParts int
+ IsTruncated bool
+
+ ChecksumAlgorithm string
+ ChecksumType string
+
+ // List of parts.
+ Parts []Part `xml:"Part"`
+}
+
+// ListMultipartUploadsResponse - format for list multipart uploads response.
+type ListMultipartUploadsResponse struct {
+ XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListMultipartUploadsResult" json:"-"`
+
+ Bucket string
+ KeyMarker string
+ UploadIDMarker string `xml:"UploadIdMarker"`
+ NextKeyMarker string
+ NextUploadIDMarker string `xml:"NextUploadIdMarker"`
+ Delimiter string `xml:"Delimiter,omitempty"`
+ Prefix string
+ EncodingType string `xml:"EncodingType,omitempty"`
+ MaxUploads int
+ IsTruncated bool
+
+ // List of pending uploads.
+ Uploads []Upload `xml:"Upload"`
+
+ // Delimed common prefixes.
+ CommonPrefixes []CommonPrefix
+}
+
+// ListBucketsResponse - format for list buckets response
+type ListBucketsResponse struct {
+ XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListAllMyBucketsResult" json:"-"`
+
+ Owner Owner
+
+ // Container for one or more buckets.
+ Buckets struct {
+ Buckets []Bucket `xml:"Bucket"`
+ } // Buckets are nested
+}
+
+// Upload container for in progress multipart upload
+type Upload struct {
+ Key string
+ UploadID string `xml:"UploadId"`
+ Initiator Initiator
+ Owner Owner
+ StorageClass string
+ Initiated string
+}
+
+// CommonPrefix container for prefix response in ListObjectsResponse
+type CommonPrefix struct {
+ Prefix string
+}
+
+// Bucket container for bucket metadata
+type Bucket struct {
+ Name string
+ CreationDate string // time string of format "2006-01-02T15:04:05.000Z"
+}
+
+// ObjectVersion container for object version metadata
+type ObjectVersion struct {
+ Object
+ IsLatest bool
+ VersionID string `xml:"VersionId"`
+
+ isDeleteMarker bool
+}
+
+// MarshalXML - marshal ObjectVersion
+func (o ObjectVersion) MarshalXML(e *xxml.Encoder, start xxml.StartElement) error {
+ if o.isDeleteMarker {
+ start.Name.Local = "DeleteMarker"
+ } else {
+ start.Name.Local = "Version"
+ }
+ type objectVersionWrapper ObjectVersion
+ return e.EncodeElement(objectVersionWrapper(o), start)
+}
+
+// DeleteMarkerVersion container for delete marker metadata
+type DeleteMarkerVersion struct {
+ Key string
+ LastModified string // time string of format "2006-01-02T15:04:05.000Z"
+
+ // Owner of the object.
+ Owner Owner
+
+ IsLatest bool
+ VersionID string `xml:"VersionId"`
+}
+
+// Metadata metadata items implemented to ensure XML marshaling works.
+type Metadata struct {
+ Items []struct {
+ Key string
+ Value string
+ }
+}
+
+// Set add items, duplicate items get replaced.
+func (s *Metadata) Set(k, v string) {
+ for i, item := range s.Items {
+ if item.Key == k {
+ s.Items[i] = struct {
+ Key string
+ Value string
+ }{
+ Key: k,
+ Value: v,
+ }
+ return
+ }
+ }
+ s.Items = append(s.Items, struct {
+ Key string
+ Value string
+ }{
+ Key: k,
+ Value: v,
+ })
+}
+
+type xmlKeyEntry struct {
+ XMLName xxml.Name
+ Value string `xml:",chardata"`
+}
+
+// MarshalXML - StringMap marshals into XML.
+func (s *Metadata) MarshalXML(e *xxml.Encoder, start xxml.StartElement) error {
+ if s == nil {
+ return nil
+ }
+
+ if len(s.Items) == 0 {
+ return nil
+ }
+
+ if err := e.EncodeToken(start); err != nil {
+ return err
+ }
+
+ for _, item := range s.Items {
+ if err := e.Encode(xmlKeyEntry{
+ XMLName: xxml.Name{Local: item.Key},
+ Value: item.Value,
+ }); err != nil {
+ return err
+ }
+ }
+
+ return e.EncodeToken(start.End())
+}
+
+// ObjectInternalInfo contains some internal information about a given
+// object, it will printed in listing calls with enabled metadata.
+type ObjectInternalInfo struct {
+ K int // Data blocks
+ M int // Parity blocks
+}
+
+// Object container for object metadata
+type Object struct {
+ Key string
+ LastModified string // time string of format "2006-01-02T15:04:05.000Z"
+ ETag string
+ Size int64
+
+ // Owner of the object.
+ Owner *Owner `xml:"Owner,omitempty"`
+
+ // The class of storage used to store the object.
+ StorageClass string
+
+ // UserMetadata user-defined metadata
+ UserMetadata *Metadata `xml:"UserMetadata,omitempty"`
+ UserTags string `xml:"UserTags,omitempty"`
+
+ Internal *ObjectInternalInfo `xml:"Internal,omitempty"`
+}
+
+// CopyObjectResponse container returns ETag and LastModified of the successfully copied object
+type CopyObjectResponse struct {
+ XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CopyObjectResult" json:"-"`
+ LastModified string // time string of format "2006-01-02T15:04:05.000Z"
+ ETag string // md5sum of the copied object.
+}
+
+// CopyObjectPartResponse container returns ETag and LastModified of the successfully copied object
+type CopyObjectPartResponse struct {
+ XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CopyPartResult" json:"-"`
+ LastModified string // time string of format "2006-01-02T15:04:05.000Z"
+ ETag string // md5sum of the copied object part.
+}
+
+// Initiator inherit from Owner struct, fields are same
+type Initiator Owner
+
+// Owner - bucket owner/principal
+type Owner struct {
+ ID string
+ DisplayName string
+}
+
+// InitiateMultipartUploadResponse container for InitiateMultiPartUpload response, provides uploadID to start MultiPart upload
+type InitiateMultipartUploadResponse struct {
+ XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ InitiateMultipartUploadResult" json:"-"`
+
+ Bucket string
+ Key string
+ UploadID string `xml:"UploadId"`
+}
+
+// CompleteMultipartUploadResponse container for completed multipart upload response
+type CompleteMultipartUploadResponse struct {
+ XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CompleteMultipartUploadResult" json:"-"`
+
+ Location string
+ Bucket string
+ Key string
+ ETag string
+
+ ChecksumCRC32 string `xml:"ChecksumCRC32,omitempty"`
+ ChecksumCRC32C string `xml:"ChecksumCRC32C,omitempty"`
+ ChecksumSHA1 string `xml:"ChecksumSHA1,omitempty"`
+ ChecksumSHA256 string `xml:"ChecksumSHA256,omitempty"`
+ ChecksumCRC64NVME string `xml:",omitempty"`
+}
+
+// DeleteError structure.
+type DeleteError struct {
+ Code string
+ Message string
+ Key string
+ VersionID string `xml:"VersionId"`
+}
+
+// DeleteObjectsResponse container for multiple object deletes.
+type DeleteObjectsResponse struct {
+ XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ DeleteResult" json:"-"`
+
+ // Collection of all deleted objects
+ DeletedObjects []DeletedObject `xml:"Deleted,omitempty"`
+
+ // Collection of errors deleting certain objects.
+ Errors []DeleteError `xml:"Error,omitempty"`
+}
+
+// PostResponse container for POST object request when success_action_status is set to 201
+type PostResponse struct {
+ Bucket string
+ Key string
+ ETag string
+ Location string
+}
+
+// returns "https" if the tls boolean is true, "http" otherwise.
+func getURLScheme(tls bool) string {
+ if tls {
+ return httpsScheme
+ }
+ return httpScheme
+}
+
+// getObjectLocation gets the fully qualified URL of an object.
+func getObjectLocation(r *http.Request, domains []string, bucket, object string) string {
+ // unit tests do not have host set.
+ if r.Host == "" {
+ return path.Clean(r.URL.Path)
+ }
+ proto := handlers.GetSourceScheme(r)
+ if proto == "" {
+ proto = getURLScheme(globalIsTLS)
+ }
+ u := &url.URL{
+ Host: r.Host,
+ Path: path.Join(SlashSeparator, bucket, object),
+ Scheme: proto,
+ }
+ // If domain is set then we need to use bucket DNS style.
+ for _, domain := range domains {
+ if strings.HasPrefix(r.Host, bucket+"."+domain) {
+ u.Path = path.Join(SlashSeparator, object)
+ break
+ }
+ }
+ return u.String()
+}
+
+// generates ListBucketsResponse from array of BucketInfo which can be
+// serialized to match XML and JSON API spec output.
+func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
+ listbuckets := make([]Bucket, 0, len(buckets))
+ data := ListBucketsResponse{}
+ owner := Owner{
+ ID: globalMinioDefaultOwnerID,
+ DisplayName: "minio",
+ }
+
+ for _, bucket := range buckets {
+ listbuckets = append(listbuckets, Bucket{
+ Name: bucket.Name,
+ CreationDate: amztime.ISO8601Format(bucket.Created.UTC()),
+ })
+ }
+
+ data.Owner = owner
+ data.Buckets.Buckets = listbuckets
+
+ return data
+}
+
+func cleanReservedKeys(metadata map[string]string) map[string]string {
+ m := cloneMSS(metadata)
+
+ switch kind, _ := crypto.IsEncrypted(metadata); kind {
+ case crypto.S3:
+ m[xhttp.AmzServerSideEncryption] = xhttp.AmzEncryptionAES
+ case crypto.S3KMS:
+ m[xhttp.AmzServerSideEncryption] = xhttp.AmzEncryptionKMS
+ m[xhttp.AmzServerSideEncryptionKmsID] = kmsKeyIDFromMetadata(metadata)
+ if kmsCtx, ok := metadata[crypto.MetaContext]; ok {
+ m[xhttp.AmzServerSideEncryptionKmsContext] = kmsCtx
+ }
+ case crypto.SSEC:
+ m[xhttp.AmzServerSideEncryptionCustomerAlgorithm] = xhttp.AmzEncryptionAES
+ }
+
+ var toRemove []string
+ for k := range cleanMinioInternalMetadataKeys(m) {
+ if stringsHasPrefixFold(k, ReservedMetadataPrefixLower) {
+ // Do not need to send any internal metadata
+ // values to client.
+ toRemove = append(toRemove, k)
+ continue
+ }
+
+ // https://github.com/google/security-research/security/advisories/GHSA-76wf-9vgp-pj7w
+ if equals(k, xhttp.AmzMetaUnencryptedContentLength, xhttp.AmzMetaUnencryptedContentMD5) {
+ toRemove = append(toRemove, k)
+ continue
+ }
+ }
+
+ for _, k := range toRemove {
+ delete(m, k)
+ delete(m, strings.ToLower(k))
+ }
+
+ return m
+}
+
+// generates an ListBucketVersions response for the said bucket with other enumerated options.
+func generateListVersionsResponse(ctx context.Context, bucket, prefix, marker, versionIDMarker, delimiter, encodingType string, maxKeys int, resp ListObjectVersionsInfo, metadata metaCheckFn) ListVersionsResponse {
+ versions := make([]ObjectVersion, 0, len(resp.Objects))
+
+ owner := &Owner{
+ ID: globalMinioDefaultOwnerID,
+ DisplayName: "minio",
+ }
+ data := ListVersionsResponse{}
+ var lastObjMetaName string
+ var tagErr, metaErr APIErrorCode = -1, -1
+
+ for _, object := range resp.Objects {
+ if object.Name == "" {
+ continue
+ }
+ // Cache checks for the same object
+ if metadata != nil && lastObjMetaName != object.Name {
+ tagErr = metadata(object.Name, policy.GetObjectTaggingAction)
+ metaErr = metadata(object.Name, policy.GetObjectAction)
+ lastObjMetaName = object.Name
+ }
+ content := ObjectVersion{}
+ content.Key = s3EncodeName(object.Name, encodingType)
+ content.LastModified = amztime.ISO8601Format(object.ModTime.UTC())
+ if object.ETag != "" {
+ content.ETag = "\"" + object.ETag + "\""
+ }
+ content.Size = object.Size
+ if object.StorageClass != "" {
+ content.StorageClass = filterStorageClass(ctx, object.StorageClass)
+ } else {
+ content.StorageClass = globalMinioDefaultStorageClass
+ }
+ if tagErr == ErrNone {
+ content.UserTags = object.UserTags
+ }
+ if metaErr == ErrNone {
+ content.UserMetadata = &Metadata{}
+ switch kind, _ := crypto.IsEncrypted(object.UserDefined); kind {
+ case crypto.S3:
+ content.UserMetadata.Set(xhttp.AmzServerSideEncryption, xhttp.AmzEncryptionAES)
+ case crypto.S3KMS:
+ content.UserMetadata.Set(xhttp.AmzServerSideEncryption, xhttp.AmzEncryptionKMS)
+ case crypto.SSEC:
+ content.UserMetadata.Set(xhttp.AmzServerSideEncryptionCustomerAlgorithm, xhttp.AmzEncryptionAES)
+ }
+ for k, v := range cleanReservedKeys(object.UserDefined) {
+ content.UserMetadata.Set(k, v)
+ }
+ content.Internal = &ObjectInternalInfo{
+ K: object.DataBlocks,
+ M: object.ParityBlocks,
+ }
+ }
+ content.Owner = owner
+ content.VersionID = object.VersionID
+ if content.VersionID == "" {
+ content.VersionID = nullVersionID
+ }
+ content.IsLatest = object.IsLatest
+ content.isDeleteMarker = object.DeleteMarker
+ versions = append(versions, content)
+ }
+
+ data.Name = bucket
+ data.Versions = versions
+ data.EncodingType = encodingType
+ data.Prefix = s3EncodeName(prefix, encodingType)
+ data.KeyMarker = s3EncodeName(marker, encodingType)
+ data.Delimiter = s3EncodeName(delimiter, encodingType)
+ data.MaxKeys = maxKeys
+
+ data.NextKeyMarker = s3EncodeName(resp.NextMarker, encodingType)
+ data.NextVersionIDMarker = resp.NextVersionIDMarker
+ data.VersionIDMarker = versionIDMarker
+ data.IsTruncated = resp.IsTruncated
+
+ prefixes := make([]CommonPrefix, 0, len(resp.Prefixes))
+ for _, prefix := range resp.Prefixes {
+ prefixItem := CommonPrefix{}
+ prefixItem.Prefix = s3EncodeName(prefix, encodingType)
+ prefixes = append(prefixes, prefixItem)
+ }
+ data.CommonPrefixes = prefixes
+ return data
+}
+
+// generates an ListObjectsV1 response for the said bucket with other enumerated options.
+func generateListObjectsV1Response(ctx context.Context, bucket, prefix, marker, delimiter, encodingType string, maxKeys int, resp ListObjectsInfo) ListObjectsResponse {
+ contents := make([]Object, 0, len(resp.Objects))
+ owner := &Owner{
+ ID: globalMinioDefaultOwnerID,
+ DisplayName: "minio",
+ }
+ data := ListObjectsResponse{}
+
+ for _, object := range resp.Objects {
+ content := Object{}
+ if object.Name == "" {
+ continue
+ }
+ content.Key = s3EncodeName(object.Name, encodingType)
+ content.LastModified = amztime.ISO8601Format(object.ModTime.UTC())
+ if object.ETag != "" {
+ content.ETag = "\"" + object.ETag + "\""
+ }
+ content.Size = object.Size
+ if object.StorageClass != "" {
+ content.StorageClass = filterStorageClass(ctx, object.StorageClass)
+ } else {
+ content.StorageClass = globalMinioDefaultStorageClass
+ }
+ content.Owner = owner
+ contents = append(contents, content)
+ }
+ data.Name = bucket
+ data.Contents = contents
+
+ data.EncodingType = encodingType
+ data.Prefix = s3EncodeName(prefix, encodingType)
+ data.Marker = s3EncodeName(marker, encodingType)
+ data.Delimiter = s3EncodeName(delimiter, encodingType)
+ data.MaxKeys = maxKeys
+ data.NextMarker = s3EncodeName(resp.NextMarker, encodingType)
+ data.IsTruncated = resp.IsTruncated
+
+ prefixes := make([]CommonPrefix, 0, len(resp.Prefixes))
+ for _, prefix := range resp.Prefixes {
+ prefixItem := CommonPrefix{}
+ prefixItem.Prefix = s3EncodeName(prefix, encodingType)
+ prefixes = append(prefixes, prefixItem)
+ }
+ data.CommonPrefixes = prefixes
+ return data
+}
+
+// generates an ListObjectsV2 response for the said bucket with other enumerated options.
+func generateListObjectsV2Response(ctx context.Context, bucket, prefix, token, nextToken, startAfter, delimiter, encodingType string, fetchOwner, isTruncated bool, maxKeys int, objects []ObjectInfo, prefixes []string, metadata metaCheckFn) ListObjectsV2Response {
+ contents := make([]Object, 0, len(objects))
+ var owner *Owner
+ if fetchOwner {
+ owner = &Owner{
+ ID: globalMinioDefaultOwnerID,
+ DisplayName: "minio",
+ }
+ }
+
+ data := ListObjectsV2Response{}
+
+ for _, object := range objects {
+ content := Object{}
+ if object.Name == "" {
+ continue
+ }
+ content.Key = s3EncodeName(object.Name, encodingType)
+ content.LastModified = amztime.ISO8601Format(object.ModTime.UTC())
+ if object.ETag != "" {
+ content.ETag = "\"" + object.ETag + "\""
+ }
+ content.Size = object.Size
+ if object.StorageClass != "" {
+ content.StorageClass = filterStorageClass(ctx, object.StorageClass)
+ } else {
+ content.StorageClass = globalMinioDefaultStorageClass
+ }
+ content.Owner = owner
+ if metadata != nil {
+ if metadata(object.Name, policy.GetObjectTaggingAction) == ErrNone {
+ content.UserTags = object.UserTags
+ }
+ if metadata(object.Name, policy.GetObjectAction) == ErrNone {
+ content.UserMetadata = &Metadata{}
+ switch kind, _ := crypto.IsEncrypted(object.UserDefined); kind {
+ case crypto.S3:
+ content.UserMetadata.Set(xhttp.AmzServerSideEncryption, xhttp.AmzEncryptionAES)
+ case crypto.S3KMS:
+ content.UserMetadata.Set(xhttp.AmzServerSideEncryption, xhttp.AmzEncryptionKMS)
+ case crypto.SSEC:
+ content.UserMetadata.Set(xhttp.AmzServerSideEncryptionCustomerAlgorithm, xhttp.AmzEncryptionAES)
+ }
+ for k, v := range cleanReservedKeys(object.UserDefined) {
+ content.UserMetadata.Set(k, v)
+ }
+ content.Internal = &ObjectInternalInfo{
+ K: object.DataBlocks,
+ M: object.ParityBlocks,
+ }
+ }
+ }
+ contents = append(contents, content)
+ }
+ data.Name = bucket
+ data.Contents = contents
+
+ data.EncodingType = encodingType
+ data.StartAfter = s3EncodeName(startAfter, encodingType)
+ data.Delimiter = s3EncodeName(delimiter, encodingType)
+ data.Prefix = s3EncodeName(prefix, encodingType)
+ data.MaxKeys = maxKeys
+ data.ContinuationToken = base64.StdEncoding.EncodeToString([]byte(token))
+ data.NextContinuationToken = base64.StdEncoding.EncodeToString([]byte(nextToken))
+ data.IsTruncated = isTruncated
+
+ commonPrefixes := make([]CommonPrefix, 0, len(prefixes))
+ for _, prefix := range prefixes {
+ prefixItem := CommonPrefix{}
+ prefixItem.Prefix = s3EncodeName(prefix, encodingType)
+ commonPrefixes = append(commonPrefixes, prefixItem)
+ }
+ data.CommonPrefixes = commonPrefixes
+ data.KeyCount = len(data.Contents) + len(data.CommonPrefixes)
+ return data
+}
+
+type metaCheckFn = func(name string, action policy.Action) (s3Err APIErrorCode)
+
+// generates CopyObjectResponse from etag and lastModified time.
+func generateCopyObjectResponse(etag string, lastModified time.Time) CopyObjectResponse {
+ return CopyObjectResponse{
+ ETag: "\"" + etag + "\"",
+ LastModified: amztime.ISO8601Format(lastModified.UTC()),
+ }
+}
+
+// generates CopyObjectPartResponse from etag and lastModified time.
+func generateCopyObjectPartResponse(etag string, lastModified time.Time) CopyObjectPartResponse {
+ return CopyObjectPartResponse{
+ ETag: "\"" + etag + "\"",
+ LastModified: amztime.ISO8601Format(lastModified.UTC()),
+ }
+}
+
+// generates InitiateMultipartUploadResponse for given bucket, key and uploadID.
+func generateInitiateMultipartUploadResponse(bucket, key, uploadID string) InitiateMultipartUploadResponse {
+ return InitiateMultipartUploadResponse{
+ Bucket: bucket,
+ Key: key,
+ UploadID: uploadID,
+ }
+}
+
+// generates CompleteMultipartUploadResponse for given bucket, key, location and ETag.
+func generateCompleteMultipartUploadResponse(bucket, key, location string, oi ObjectInfo, h http.Header) CompleteMultipartUploadResponse {
+ cs, _ := oi.decryptChecksums(0, h)
+ c := CompleteMultipartUploadResponse{
+ Location: location,
+ Bucket: bucket,
+ Key: key,
+ // AWS S3 quotes the ETag in XML, make sure we are compatible here.
+ ETag: "\"" + oi.ETag + "\"",
+ ChecksumSHA1: cs[hash.ChecksumSHA1.String()],
+ ChecksumSHA256: cs[hash.ChecksumSHA256.String()],
+ ChecksumCRC32: cs[hash.ChecksumCRC32.String()],
+ ChecksumCRC32C: cs[hash.ChecksumCRC32C.String()],
+ ChecksumCRC64NVME: cs[hash.ChecksumCRC64NVME.String()],
+ }
+ return c
+}
+
+// generates ListPartsResponse from ListPartsInfo.
+func generateListPartsResponse(partsInfo ListPartsInfo, encodingType string) ListPartsResponse {
+ listPartsResponse := ListPartsResponse{}
+ listPartsResponse.Bucket = partsInfo.Bucket
+ listPartsResponse.Key = s3EncodeName(partsInfo.Object, encodingType)
+ listPartsResponse.UploadID = partsInfo.UploadID
+ listPartsResponse.StorageClass = globalMinioDefaultStorageClass
+
+ // Dumb values not meaningful
+ listPartsResponse.Initiator = Initiator{
+ ID: globalMinioDefaultOwnerID,
+ DisplayName: globalMinioDefaultOwnerID,
+ }
+ listPartsResponse.Owner = Owner{
+ ID: globalMinioDefaultOwnerID,
+ DisplayName: globalMinioDefaultOwnerID,
+ }
+
+ listPartsResponse.MaxParts = partsInfo.MaxParts
+ listPartsResponse.PartNumberMarker = partsInfo.PartNumberMarker
+ listPartsResponse.IsTruncated = partsInfo.IsTruncated
+ listPartsResponse.NextPartNumberMarker = partsInfo.NextPartNumberMarker
+ listPartsResponse.ChecksumAlgorithm = partsInfo.ChecksumAlgorithm
+ listPartsResponse.ChecksumType = partsInfo.ChecksumType
+
+ listPartsResponse.Parts = make([]Part, len(partsInfo.Parts))
+ for index, part := range partsInfo.Parts {
+ newPart := Part{}
+ newPart.PartNumber = part.PartNumber
+ newPart.ETag = "\"" + part.ETag + "\""
+ newPart.Size = part.Size
+ newPart.LastModified = amztime.ISO8601Format(part.LastModified.UTC())
+ newPart.ChecksumCRC32 = part.ChecksumCRC32
+ newPart.ChecksumCRC32C = part.ChecksumCRC32C
+ newPart.ChecksumSHA1 = part.ChecksumSHA1
+ newPart.ChecksumSHA256 = part.ChecksumSHA256
+ newPart.ChecksumCRC64NVME = part.ChecksumCRC64NVME
+ listPartsResponse.Parts[index] = newPart
+ }
+ return listPartsResponse
+}
+
+// generates ListMultipartUploadsResponse for given bucket and ListMultipartsInfo.
+func generateListMultipartUploadsResponse(bucket string, multipartsInfo ListMultipartsInfo, encodingType string) ListMultipartUploadsResponse {
+ listMultipartUploadsResponse := ListMultipartUploadsResponse{}
+ listMultipartUploadsResponse.Bucket = bucket
+ listMultipartUploadsResponse.Delimiter = s3EncodeName(multipartsInfo.Delimiter, encodingType)
+ listMultipartUploadsResponse.IsTruncated = multipartsInfo.IsTruncated
+ listMultipartUploadsResponse.EncodingType = encodingType
+ listMultipartUploadsResponse.Prefix = s3EncodeName(multipartsInfo.Prefix, encodingType)
+ listMultipartUploadsResponse.KeyMarker = s3EncodeName(multipartsInfo.KeyMarker, encodingType)
+ listMultipartUploadsResponse.NextKeyMarker = s3EncodeName(multipartsInfo.NextKeyMarker, encodingType)
+ listMultipartUploadsResponse.MaxUploads = multipartsInfo.MaxUploads
+ listMultipartUploadsResponse.NextUploadIDMarker = multipartsInfo.NextUploadIDMarker
+ listMultipartUploadsResponse.UploadIDMarker = multipartsInfo.UploadIDMarker
+ listMultipartUploadsResponse.CommonPrefixes = make([]CommonPrefix, len(multipartsInfo.CommonPrefixes))
+ for index, commonPrefix := range multipartsInfo.CommonPrefixes {
+ listMultipartUploadsResponse.CommonPrefixes[index] = CommonPrefix{
+ Prefix: s3EncodeName(commonPrefix, encodingType),
+ }
+ }
+ listMultipartUploadsResponse.Uploads = make([]Upload, len(multipartsInfo.Uploads))
+ for index, upload := range multipartsInfo.Uploads {
+ newUpload := Upload{}
+ newUpload.UploadID = upload.UploadID
+ newUpload.Key = s3EncodeName(upload.Object, encodingType)
+ newUpload.Initiated = amztime.ISO8601Format(upload.Initiated.UTC())
+ listMultipartUploadsResponse.Uploads[index] = newUpload
+ }
+ return listMultipartUploadsResponse
+}
+
+// generate multi objects delete response.
+func generateMultiDeleteResponse(quiet bool, deletedObjects []DeletedObject, errs []DeleteError) DeleteObjectsResponse {
+ deleteResp := DeleteObjectsResponse{}
+ if !quiet {
+ deleteResp.DeletedObjects = deletedObjects
+ }
+ deleteResp.Errors = errs
+ return deleteResp
+}
+
+func writeResponse(w http.ResponseWriter, statusCode int, response []byte, mType mimeType) {
+ if statusCode == 0 {
+ statusCode = 200
+ }
+ // Similar check to http.checkWriteHeaderCode
+ if statusCode < 100 || statusCode > 999 {
+ bugLogIf(context.Background(), fmt.Errorf("invalid WriteHeader code %v", statusCode))
+ statusCode = http.StatusInternalServerError
+ }
+ setCommonHeaders(w)
+ if mType != mimeNone {
+ w.Header().Set(xhttp.ContentType, string(mType))
+ }
+ w.Header().Set(xhttp.ContentLength, strconv.Itoa(len(response)))
+ w.WriteHeader(statusCode)
+ if response != nil {
+ w.Write(response)
+ }
+}
+
+// mimeType represents various MIME type used API responses.
+type mimeType string
+
+const (
+ // Means no response type.
+ mimeNone mimeType = ""
+ // Means response type is JSON.
+ mimeJSON mimeType = "application/json"
+ // Means response type is XML.
+ mimeXML mimeType = "application/xml"
+)
+
+// writeSuccessResponseJSON writes success headers and response if any,
+// with content-type set to `application/json`.
+func writeSuccessResponseJSON(w http.ResponseWriter, response []byte) {
+ writeResponse(w, http.StatusOK, response, mimeJSON)
+}
+
+// writeSuccessResponseXML writes success headers and response if any,
+// with content-type set to `application/xml`.
+func writeSuccessResponseXML(w http.ResponseWriter, response []byte) {
+ writeResponse(w, http.StatusOK, response, mimeXML)
+}
+
+// writeSuccessNoContent writes success headers with http status 204
+func writeSuccessNoContent(w http.ResponseWriter) {
+ writeResponse(w, http.StatusNoContent, nil, mimeNone)
+}
+
+// writeRedirectSeeOther writes Location header with http status 303
+func writeRedirectSeeOther(w http.ResponseWriter, location string) {
+ w.Header().Set(xhttp.Location, location)
+ writeResponse(w, http.StatusSeeOther, nil, mimeNone)
+}
+
+func writeSuccessResponseHeadersOnly(w http.ResponseWriter) {
+ writeResponse(w, http.StatusOK, nil, mimeNone)
+}
+
+// writeErrorResponse writes error headers
+func writeErrorResponse(ctx context.Context, w http.ResponseWriter, err APIError, reqURL *url.URL) {
+ switch err.HTTPStatusCode {
+ case http.StatusServiceUnavailable, http.StatusTooManyRequests:
+ // Set retry-after header to indicate user-agents to retry request after 60 seconds.
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
+ w.Header().Set(xhttp.RetryAfter, "60")
+ }
+
+ switch err.Code {
+ case "InvalidRegion":
+ err.Description = fmt.Sprintf("Region does not match; expecting '%s'.", globalSite.Region())
+ case "AuthorizationHeaderMalformed":
+ err.Description = fmt.Sprintf("The authorization header is malformed; the region is wrong; expecting '%s'.", globalSite.Region())
+ }
+
+ // Similar check to http.checkWriteHeaderCode
+ if err.HTTPStatusCode < 100 || err.HTTPStatusCode > 999 {
+ bugLogIf(ctx, fmt.Errorf("invalid WriteHeader code %v from %v", err.HTTPStatusCode, err.Code))
+ err.HTTPStatusCode = http.StatusInternalServerError
+ }
+
+ // Generate error response.
+ errorResponse := getAPIErrorResponse(ctx, err, reqURL.Path,
+ w.Header().Get(xhttp.AmzRequestID), w.Header().Get(xhttp.AmzRequestHostID))
+ encodedErrorResponse := encodeResponse(errorResponse)
+ writeResponse(w, err.HTTPStatusCode, encodedErrorResponse, mimeXML)
+}
+
+func writeErrorResponseHeadersOnly(w http.ResponseWriter, err APIError) {
+ w.Header().Set(xMinIOErrCodeHeader, err.Code)
+ w.Header().Set(xMinIOErrDescHeader, "\""+err.Description+"\"")
+ writeResponse(w, err.HTTPStatusCode, nil, mimeNone)
+}
+
+func writeErrorResponseString(ctx context.Context, w http.ResponseWriter, err APIError, reqURL *url.URL) {
+ // Generate string error response.
+ writeResponse(w, err.HTTPStatusCode, []byte(err.Description), mimeNone)
+}
+
+// writeErrorResponseJSON - writes error response in JSON format;
+// useful for admin APIs.
+func writeErrorResponseJSON(ctx context.Context, w http.ResponseWriter, err APIError, reqURL *url.URL) {
+ // Generate error response.
+ errorResponse := getAPIErrorResponse(ctx, err, reqURL.Path, w.Header().Get(xhttp.AmzRequestID), w.Header().Get(xhttp.AmzRequestHostID))
+ encodedErrorResponse := encodeResponseJSON(errorResponse)
+ writeResponse(w, err.HTTPStatusCode, encodedErrorResponse, mimeJSON)
+}
+
+// writeCustomErrorResponseJSON - similar to writeErrorResponseJSON,
+// but accepts the error message directly (this allows messages to be
+// dynamically generated.)
+func writeCustomErrorResponseJSON(ctx context.Context, w http.ResponseWriter, err APIError,
+ errBody string, reqURL *url.URL,
+) {
+ reqInfo := logger.GetReqInfo(ctx)
+ errorResponse := APIErrorResponse{
+ Code: err.Code,
+ Message: errBody,
+ Resource: reqURL.Path,
+ BucketName: reqInfo.BucketName,
+ Key: reqInfo.ObjectName,
+ RequestID: w.Header().Get(xhttp.AmzRequestID),
+ HostID: globalDeploymentID(),
+ }
+ encodedErrorResponse := encodeResponseJSON(errorResponse)
+ writeResponse(w, err.HTTPStatusCode, encodedErrorResponse, mimeJSON)
+}
diff --git a/cmd/api-response_test.go b/cmd/api-response_test.go
new file mode 100644
index 00000000..8f89b340
--- /dev/null
+++ b/cmd/api-response_test.go
@@ -0,0 +1,124 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "net/http"
+ "testing"
+)
+
+// Tests object location.
+func TestObjectLocation(t *testing.T) {
+ testCases := []struct {
+ request *http.Request
+ bucket, object string
+ domains []string
+ expectedLocation string
+ }{
+ // Server binding to localhost IP with https.
+ {
+ request: &http.Request{
+ Host: "127.0.0.1:9000",
+ Header: map[string][]string{
+ "X-Forwarded-Scheme": {httpScheme},
+ },
+ },
+ bucket: "testbucket1",
+ object: "test/1.txt",
+ expectedLocation: "http://127.0.0.1:9000/testbucket1/test/1.txt",
+ },
+ {
+ request: &http.Request{
+ Host: "127.0.0.1:9000",
+ Header: map[string][]string{
+ "X-Forwarded-Scheme": {httpsScheme},
+ },
+ },
+ bucket: "testbucket1",
+ object: "test/1.txt",
+ expectedLocation: "https://127.0.0.1:9000/testbucket1/test/1.txt",
+ },
+ // Server binding to fqdn.
+ {
+ request: &http.Request{
+ Host: "s3.mybucket.org",
+ Header: map[string][]string{
+ "X-Forwarded-Scheme": {httpScheme},
+ },
+ },
+ bucket: "mybucket",
+ object: "test/1.txt",
+ expectedLocation: "http://s3.mybucket.org/mybucket/test/1.txt",
+ },
+ // Server binding to fqdn.
+ {
+ request: &http.Request{
+ Host: "mys3.mybucket.org",
+ Header: map[string][]string{},
+ },
+ bucket: "mybucket",
+ object: "test/1.txt",
+ expectedLocation: "http://mys3.mybucket.org/mybucket/test/1.txt",
+ },
+ // Server with virtual domain name.
+ {
+ request: &http.Request{
+ Host: "mybucket.mys3.bucket.org",
+ Header: map[string][]string{},
+ },
+ domains: []string{"mys3.bucket.org"},
+ bucket: "mybucket",
+ object: "test/1.txt",
+ expectedLocation: "http://mybucket.mys3.bucket.org/test/1.txt",
+ },
+ {
+ request: &http.Request{
+ Host: "mybucket.mys3.bucket.org",
+ Header: map[string][]string{
+ "X-Forwarded-Scheme": {httpsScheme},
+ },
+ },
+ domains: []string{"mys3.bucket.org"},
+ bucket: "mybucket",
+ object: "test/1.txt",
+ expectedLocation: "https://mybucket.mys3.bucket.org/test/1.txt",
+ },
+ }
+ for _, testCase := range testCases {
+ t.Run("", func(t *testing.T) {
+ gotLocation := getObjectLocation(testCase.request, testCase.domains, testCase.bucket, testCase.object)
+ if testCase.expectedLocation != gotLocation {
+ t.Errorf("expected %s, got %s", testCase.expectedLocation, gotLocation)
+ }
+ })
+ }
+}
+
+// Tests getURLScheme function behavior.
+func TestGetURLScheme(t *testing.T) {
+ tls := false
+ gotScheme := getURLScheme(tls)
+ if gotScheme != httpScheme {
+ t.Errorf("Expected %s, got %s", httpScheme, gotScheme)
+ }
+ tls = true
+ gotScheme = getURLScheme(tls)
+ if gotScheme != httpsScheme {
+ t.Errorf("Expected %s, got %s", httpsScheme, gotScheme)
+ }
+}
diff --git a/cmd/api-router.go b/cmd/api-router.go
new file mode 100644
index 00000000..896ca48b
--- /dev/null
+++ b/cmd/api-router.go
@@ -0,0 +1,695 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "net"
+ "net/http"
+
+ consoleapi "github.com/minio/console/api"
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/mux"
+ "github.com/minio/pkg/v3/wildcard"
+ "github.com/rs/cors"
+)
+
+func newHTTPServerFn() *xhttp.Server {
+ globalObjLayerMutex.RLock()
+ defer globalObjLayerMutex.RUnlock()
+ return globalHTTPServer
+}
+
+func setHTTPServer(h *xhttp.Server) {
+ globalObjLayerMutex.Lock()
+ globalHTTPServer = h
+ globalObjLayerMutex.Unlock()
+}
+
+func newConsoleServerFn() *consoleapi.Server {
+ globalObjLayerMutex.RLock()
+ defer globalObjLayerMutex.RUnlock()
+ return globalConsoleSrv
+}
+
+func setConsoleSrv(srv *consoleapi.Server) {
+ globalObjLayerMutex.Lock()
+ globalConsoleSrv = srv
+ globalObjLayerMutex.Unlock()
+}
+
+func newObjectLayerFn() ObjectLayer {
+ globalObjLayerMutex.RLock()
+ defer globalObjLayerMutex.RUnlock()
+ return globalObjectAPI
+}
+
+func setObjectLayer(o ObjectLayer) {
+ globalObjLayerMutex.Lock()
+ globalObjectAPI = o
+ globalObjLayerMutex.Unlock()
+}
+
+// objectAPIHandlers implements and provides http handlers for S3 API.
+type objectAPIHandlers struct {
+ ObjectAPI func() ObjectLayer
+}
+
+// getHost tries its best to return the request host.
+// According to section 14.23 of RFC 2616 the Host header
+// can include the port number if the default value of 80 is not used.
+func getHost(r *http.Request) string {
+ if r.URL.IsAbs() {
+ return r.URL.Host
+ }
+ return r.Host
+}
+
+func notImplementedHandler(w http.ResponseWriter, r *http.Request) {
+ writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+}
+
+type rejectedAPI struct {
+ api string
+ methods []string
+ queries []string
+ path string
+}
+
+var rejectedObjAPIs = []rejectedAPI{
+ {
+ api: "torrent",
+ methods: []string{http.MethodPut, http.MethodDelete, http.MethodGet},
+ queries: []string{"torrent", ""},
+ path: "/{object:.+}",
+ },
+ {
+ api: "acl",
+ methods: []string{http.MethodDelete},
+ queries: []string{"acl", ""},
+ path: "/{object:.+}",
+ },
+}
+
+var rejectedBucketAPIs = []rejectedAPI{
+ {
+ api: "inventory",
+ methods: []string{http.MethodGet, http.MethodPut, http.MethodDelete},
+ queries: []string{"inventory", ""},
+ },
+ {
+ api: "cors",
+ methods: []string{http.MethodPut, http.MethodDelete},
+ queries: []string{"cors", ""},
+ },
+ {
+ api: "metrics",
+ methods: []string{http.MethodGet, http.MethodPut, http.MethodDelete},
+ queries: []string{"metrics", ""},
+ },
+ {
+ api: "website",
+ methods: []string{http.MethodPut},
+ queries: []string{"website", ""},
+ },
+ {
+ api: "logging",
+ methods: []string{http.MethodPut, http.MethodDelete},
+ queries: []string{"logging", ""},
+ },
+ {
+ api: "accelerate",
+ methods: []string{http.MethodPut, http.MethodDelete},
+ queries: []string{"accelerate", ""},
+ },
+ {
+ api: "requestPayment",
+ methods: []string{http.MethodPut, http.MethodDelete},
+ queries: []string{"requestPayment", ""},
+ },
+ {
+ api: "acl",
+ methods: []string{http.MethodDelete, http.MethodPut, http.MethodHead},
+ queries: []string{"acl", ""},
+ },
+ {
+ api: "publicAccessBlock",
+ methods: []string{http.MethodDelete, http.MethodPut, http.MethodGet},
+ queries: []string{"publicAccessBlock", ""},
+ },
+ {
+ api: "ownershipControls",
+ methods: []string{http.MethodDelete, http.MethodPut, http.MethodGet},
+ queries: []string{"ownershipControls", ""},
+ },
+ {
+ api: "intelligent-tiering",
+ methods: []string{http.MethodDelete, http.MethodPut, http.MethodGet},
+ queries: []string{"intelligent-tiering", ""},
+ },
+ {
+ api: "analytics",
+ methods: []string{http.MethodDelete, http.MethodPut, http.MethodGet},
+ queries: []string{"analytics", ""},
+ },
+}
+
+// Set of s3 handler options as bit flags.
+type s3HFlag uint8
+
+const (
+ // when provided, disables Gzip compression.
+ noGZS3HFlag = 1 << iota
+
+ // when provided, enables only tracing of headers. Otherwise, both headers
+ // and body are traced.
+ traceHdrsS3HFlag
+
+ // when provided, disables throttling via the `maxClients` middleware.
+ noThrottleS3HFlag
+)
+
+func (h s3HFlag) has(flag s3HFlag) bool {
+ // Use bitwise-AND and check if the result is non-zero.
+ return h&flag != 0
+}
+
+// s3APIMiddleware - performs some common handler functionality for S3 API
+// handlers.
+//
+// It is set per-"handler function registration" in the router to allow for
+// behavior modification via flags.
+//
+// This middleware always calls `collectAPIStats` to collect API stats.
+//
+// The passed in handler function must be a method of `objectAPIHandlers` for
+// the name displayed in logs and trace to be accurate. The name is extracted
+// via reflection.
+//
+// When **no** flags are passed, the behavior is to trace both headers and body,
+// gzip the response and throttle the handler via `maxClients`. Each of these
+// can be disabled via the corresponding `s3HFlag`.
+//
+// CAUTION: for requests involving large req/resp bodies ensure to pass the
+// `traceHdrsS3HFlag`, otherwise both headers and body will be traced, causing
+// high memory usage!
+func s3APIMiddleware(f http.HandlerFunc, flags ...s3HFlag) http.HandlerFunc {
+ // Collect all flags with bitwise-OR and assign operator
+ var handlerFlags s3HFlag
+ for _, flag := range flags {
+ handlerFlags |= flag
+ }
+
+ // Get name of the handler using reflection.
+ handlerName := getHandlerName(f, "objectAPIHandlers")
+
+ var handler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
+ // Wrap the actual handler with the appropriate tracing middleware.
+ var tracedHandler http.HandlerFunc
+ if handlerFlags.has(traceHdrsS3HFlag) {
+ tracedHandler = httpTraceHdrs(f)
+ } else {
+ tracedHandler = httpTraceAll(f)
+ }
+
+ // Skip wrapping with the gzip middleware if specified.
+ gzippedHandler := tracedHandler
+ if !handlerFlags.has(noGZS3HFlag) {
+ gzippedHandler = gzipHandler(gzippedHandler)
+ }
+
+ // Skip wrapping with throttling middleware if specified.
+ throttledHandler := gzippedHandler
+ if !handlerFlags.has(noThrottleS3HFlag) {
+ throttledHandler = maxClients(throttledHandler)
+ }
+
+ // Collect API stats using the API name got from reflection in
+ // `getHandlerName`.
+ statsCollectedHandler := collectAPIStats(handlerName, throttledHandler)
+
+ // Call the final handler.
+ statsCollectedHandler(w, r)
+ }
+
+ return handler
+}
+
+// registerAPIRouter - registers S3 compatible APIs.
+func registerAPIRouter(router *mux.Router) {
+ // Initialize API.
+ api := objectAPIHandlers{
+ ObjectAPI: newObjectLayerFn,
+ }
+
+ // API Router
+ apiRouter := router.PathPrefix(SlashSeparator).Subrouter()
+
+ var routers []*mux.Router
+ for _, domainName := range globalDomainNames {
+ if IsKubernetes() {
+ routers = append(routers, apiRouter.MatcherFunc(func(r *http.Request, match *mux.RouteMatch) bool {
+ host, _, err := net.SplitHostPort(getHost(r))
+ if err != nil {
+ host = r.Host
+ }
+ // Make sure to skip matching minio.` this is
+ // specifically meant for operator/k8s deployment
+ // The reason we need to skip this is for a special
+ // usecase where we need to make sure that
+ // minio..svc. is ignored
+ // by the bucketDNS style to ensure that path style
+ // is available and honored at this domain.
+ //
+ // All other `..svc.`
+ // makes sure that buckets are routed through this matcher
+ // to match for ``
+ return host != minioReservedBucket+"."+domainName
+ }).Host("{bucket:.+}."+domainName).Subrouter())
+ } else {
+ routers = append(routers, apiRouter.Host("{bucket:.+}."+domainName).Subrouter())
+ }
+ }
+ routers = append(routers, apiRouter.PathPrefix("/{bucket}").Subrouter())
+
+ for _, router := range routers {
+ // Register all rejected object APIs
+ for _, r := range rejectedObjAPIs {
+ t := router.Methods(r.methods...).
+ HandlerFunc(collectAPIStats(r.api, httpTraceAll(notImplementedHandler))).
+ Queries(r.queries...)
+ t.Path(r.path)
+ }
+
+ // Object operations
+ // HeadObject
+ router.Methods(http.MethodHead).Path("/{object:.+}").
+ HandlerFunc(s3APIMiddleware(api.HeadObjectHandler))
+
+ // GetObjectAttributes
+ router.Methods(http.MethodGet).Path("/{object:.+}").
+ HandlerFunc(s3APIMiddleware(api.GetObjectAttributesHandler, traceHdrsS3HFlag)).
+ Queries("attributes", "")
+
+ // CopyObjectPart
+ router.Methods(http.MethodPut).Path("/{object:.+}").
+ HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").
+ HandlerFunc(s3APIMiddleware(api.CopyObjectPartHandler)).
+ Queries("partNumber", "{partNumber:.*}", "uploadId", "{uploadId:.*}")
+ // PutObjectPart
+ router.Methods(http.MethodPut).Path("/{object:.+}").
+ HandlerFunc(s3APIMiddleware(api.PutObjectPartHandler, traceHdrsS3HFlag)).
+ Queries("partNumber", "{partNumber:.*}", "uploadId", "{uploadId:.*}")
+ // ListObjectParts
+ router.Methods(http.MethodGet).Path("/{object:.+}").
+ HandlerFunc(s3APIMiddleware(api.ListObjectPartsHandler)).
+ Queries("uploadId", "{uploadId:.*}")
+ // CompleteMultipartUpload
+ router.Methods(http.MethodPost).Path("/{object:.+}").
+ HandlerFunc(s3APIMiddleware(api.CompleteMultipartUploadHandler)).
+ Queries("uploadId", "{uploadId:.*}")
+ // NewMultipartUpload
+ router.Methods(http.MethodPost).Path("/{object:.+}").
+ HandlerFunc(s3APIMiddleware(api.NewMultipartUploadHandler)).
+ Queries("uploads", "")
+ // AbortMultipartUpload
+ router.Methods(http.MethodDelete).Path("/{object:.+}").
+ HandlerFunc(s3APIMiddleware(api.AbortMultipartUploadHandler)).
+ Queries("uploadId", "{uploadId:.*}")
+ // GetObjectACL - this is a dummy call.
+ router.Methods(http.MethodGet).Path("/{object:.+}").
+ HandlerFunc(s3APIMiddleware(api.GetObjectACLHandler, traceHdrsS3HFlag)).
+ Queries("acl", "")
+ // PutObjectACL - this is a dummy call.
+ router.Methods(http.MethodPut).Path("/{object:.+}").
+ HandlerFunc(s3APIMiddleware(api.PutObjectACLHandler, traceHdrsS3HFlag)).
+ Queries("acl", "")
+ // GetObjectTagging
+ router.Methods(http.MethodGet).Path("/{object:.+}").
+ HandlerFunc(s3APIMiddleware(api.GetObjectTaggingHandler, traceHdrsS3HFlag)).
+ Queries("tagging", "")
+ // PutObjectTagging
+ router.Methods(http.MethodPut).Path("/{object:.+}").
+ HandlerFunc(s3APIMiddleware(api.PutObjectTaggingHandler, traceHdrsS3HFlag)).
+ Queries("tagging", "")
+ // DeleteObjectTagging
+ router.Methods(http.MethodDelete).Path("/{object:.+}").
+ HandlerFunc(s3APIMiddleware(api.DeleteObjectTaggingHandler, traceHdrsS3HFlag)).
+ Queries("tagging", "")
+ // SelectObjectContent
+ router.Methods(http.MethodPost).Path("/{object:.+}").
+ HandlerFunc(s3APIMiddleware(api.SelectObjectContentHandler, traceHdrsS3HFlag)).
+ Queries("select", "").Queries("select-type", "2")
+ // GetObjectRetention
+ router.Methods(http.MethodGet).Path("/{object:.+}").
+ HandlerFunc(s3APIMiddleware(api.GetObjectRetentionHandler)).
+ Queries("retention", "")
+ // GetObjectLegalHold
+ router.Methods(http.MethodGet).Path("/{object:.+}").
+ HandlerFunc(s3APIMiddleware(api.GetObjectLegalHoldHandler)).
+ Queries("legal-hold", "")
+ // GetObject with lambda ARNs
+ router.Methods(http.MethodGet).Path("/{object:.+}").
+ HandlerFunc(s3APIMiddleware(api.GetObjectLambdaHandler, traceHdrsS3HFlag)).
+ Queries("lambdaArn", "{lambdaArn:.*}")
+ // GetObject
+ router.Methods(http.MethodGet).Path("/{object:.+}").
+ HandlerFunc(s3APIMiddleware(api.GetObjectHandler, traceHdrsS3HFlag))
+ // CopyObject
+ router.Methods(http.MethodPut).Path("/{object:.+}").
+ HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").
+ HandlerFunc(s3APIMiddleware(api.CopyObjectHandler))
+ // PutObjectRetention
+ router.Methods(http.MethodPut).Path("/{object:.+}").
+ HandlerFunc(s3APIMiddleware(api.PutObjectRetentionHandler)).
+ Queries("retention", "")
+ // PutObjectLegalHold
+ router.Methods(http.MethodPut).Path("/{object:.+}").
+ HandlerFunc(s3APIMiddleware(api.PutObjectLegalHoldHandler)).
+ Queries("legal-hold", "")
+
+ // PutObject with auto-extract support for zip
+ router.Methods(http.MethodPut).Path("/{object:.+}").
+ HeadersRegexp(xhttp.AmzSnowballExtract, "true").
+ HandlerFunc(s3APIMiddleware(api.PutObjectExtractHandler, traceHdrsS3HFlag))
+
+ // AppendObject to be rejected
+ router.Methods(http.MethodPut).Path("/{object:.+}").
+ HeadersRegexp(xhttp.AmzWriteOffsetBytes, "").
+ HandlerFunc(s3APIMiddleware(errorResponseHandler))
+
+ // PutObject
+ router.Methods(http.MethodPut).Path("/{object:.+}").
+ HandlerFunc(s3APIMiddleware(api.PutObjectHandler, traceHdrsS3HFlag))
+
+ // DeleteObject
+ router.Methods(http.MethodDelete).Path("/{object:.+}").
+ HandlerFunc(s3APIMiddleware(api.DeleteObjectHandler))
+
+ // PostRestoreObject
+ router.Methods(http.MethodPost).Path("/{object:.+}").
+ HandlerFunc(s3APIMiddleware(api.PostRestoreObjectHandler)).
+ Queries("restore", "")
+
+ // Bucket operations
+
+ // GetBucketLocation
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.GetBucketLocationHandler)).
+ Queries("location", "")
+ // GetBucketPolicy
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.GetBucketPolicyHandler)).
+ Queries("policy", "")
+ // GetBucketLifecycle
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.GetBucketLifecycleHandler)).
+ Queries("lifecycle", "")
+ // GetBucketEncryption
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.GetBucketEncryptionHandler)).
+ Queries("encryption", "")
+ // GetBucketObjectLockConfig
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.GetBucketObjectLockConfigHandler)).
+ Queries("object-lock", "")
+ // GetBucketReplicationConfig
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.GetBucketReplicationConfigHandler)).
+ Queries("replication", "")
+ // GetBucketVersioning
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.GetBucketVersioningHandler)).
+ Queries("versioning", "")
+ // GetBucketNotification
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.GetBucketNotificationHandler)).
+ Queries("notification", "")
+ // ListenNotification
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.ListenNotificationHandler, noThrottleS3HFlag, traceHdrsS3HFlag)).
+ Queries("events", "{events:.*}")
+ // ResetBucketReplicationStatus - MinIO extension API
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.ResetBucketReplicationStatusHandler)).
+ Queries("replication-reset-status", "")
+
+ // Dummy Bucket Calls
+ // GetBucketACL -- this is a dummy call.
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.GetBucketACLHandler)).
+ Queries("acl", "")
+ // PutBucketACL -- this is a dummy call.
+ router.Methods(http.MethodPut).
+ HandlerFunc(s3APIMiddleware(api.PutBucketACLHandler)).
+ Queries("acl", "")
+ // GetBucketCors - this is a dummy call.
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.GetBucketCorsHandler)).
+ Queries("cors", "")
+ // PutBucketCors - this is a dummy call.
+ router.Methods(http.MethodPut).
+ HandlerFunc(s3APIMiddleware(api.PutBucketCorsHandler)).
+ Queries("cors", "")
+ // DeleteBucketCors - this is a dummy call.
+ router.Methods(http.MethodDelete).
+ HandlerFunc(s3APIMiddleware(api.DeleteBucketCorsHandler)).
+ Queries("cors", "")
+ // GetBucketWebsiteHandler - this is a dummy call.
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.GetBucketWebsiteHandler)).
+ Queries("website", "")
+ // GetBucketAccelerateHandler - this is a dummy call.
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.GetBucketAccelerateHandler)).
+ Queries("accelerate", "")
+ // GetBucketRequestPaymentHandler - this is a dummy call.
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.GetBucketRequestPaymentHandler)).
+ Queries("requestPayment", "")
+ // GetBucketLoggingHandler - this is a dummy call.
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.GetBucketLoggingHandler)).
+ Queries("logging", "")
+
+ // GetBucketTaggingHandler
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.GetBucketTaggingHandler)).
+ Queries("tagging", "")
+ // DeleteBucketWebsiteHandler
+ router.Methods(http.MethodDelete).
+ HandlerFunc(s3APIMiddleware(api.DeleteBucketWebsiteHandler)).
+ Queries("website", "")
+ // DeleteBucketTaggingHandler
+ router.Methods(http.MethodDelete).
+ HandlerFunc(s3APIMiddleware(api.DeleteBucketTaggingHandler)).
+ Queries("tagging", "")
+
+ // ListMultipartUploads
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.ListMultipartUploadsHandler)).
+ Queries("uploads", "")
+ // ListObjectsV2M
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.ListObjectsV2MHandler)).
+ Queries("list-type", "2", "metadata", "true")
+ // ListObjectsV2
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.ListObjectsV2Handler)).
+ Queries("list-type", "2")
+ // ListObjectVersions
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.ListObjectVersionsMHandler)).
+ Queries("versions", "", "metadata", "true")
+ // ListObjectVersions
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.ListObjectVersionsHandler)).
+ Queries("versions", "")
+ // GetBucketPolicyStatus
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.GetBucketPolicyStatusHandler)).
+ Queries("policyStatus", "")
+ // PutBucketLifecycle
+ router.Methods(http.MethodPut).
+ HandlerFunc(s3APIMiddleware(api.PutBucketLifecycleHandler)).
+ Queries("lifecycle", "")
+ // PutBucketReplicationConfig
+ router.Methods(http.MethodPut).
+ HandlerFunc(s3APIMiddleware(api.PutBucketReplicationConfigHandler)).
+ Queries("replication", "")
+ // PutBucketEncryption
+ router.Methods(http.MethodPut).
+ HandlerFunc(s3APIMiddleware(api.PutBucketEncryptionHandler)).
+ Queries("encryption", "")
+
+ // PutBucketPolicy
+ router.Methods(http.MethodPut).
+ HandlerFunc(s3APIMiddleware(api.PutBucketPolicyHandler)).
+ Queries("policy", "")
+
+ // PutBucketObjectLockConfig
+ router.Methods(http.MethodPut).
+ HandlerFunc(s3APIMiddleware(api.PutBucketObjectLockConfigHandler)).
+ Queries("object-lock", "")
+ // PutBucketTaggingHandler
+ router.Methods(http.MethodPut).
+ HandlerFunc(s3APIMiddleware(api.PutBucketTaggingHandler)).
+ Queries("tagging", "")
+ // PutBucketVersioning
+ router.Methods(http.MethodPut).
+ HandlerFunc(s3APIMiddleware(api.PutBucketVersioningHandler)).
+ Queries("versioning", "")
+ // PutBucketNotification
+ router.Methods(http.MethodPut).
+ HandlerFunc(s3APIMiddleware(api.PutBucketNotificationHandler)).
+ Queries("notification", "")
+ // ResetBucketReplicationStart - MinIO extension API
+ router.Methods(http.MethodPut).
+ HandlerFunc(s3APIMiddleware(api.ResetBucketReplicationStartHandler)).
+ Queries("replication-reset", "")
+
+ // PutBucket
+ router.Methods(http.MethodPut).
+ HandlerFunc(s3APIMiddleware(api.PutBucketHandler))
+ // HeadBucket
+ router.Methods(http.MethodHead).
+ HandlerFunc(s3APIMiddleware(api.HeadBucketHandler))
+ // PostPolicy
+ router.Methods(http.MethodPost).
+ MatcherFunc(func(r *http.Request, _ *mux.RouteMatch) bool {
+ return isRequestPostPolicySignatureV4(r)
+ }).
+ HandlerFunc(s3APIMiddleware(api.PostPolicyBucketHandler, traceHdrsS3HFlag))
+ // DeleteMultipleObjects
+ router.Methods(http.MethodPost).
+ HandlerFunc(s3APIMiddleware(api.DeleteMultipleObjectsHandler)).
+ Queries("delete", "")
+ // DeleteBucketPolicy
+ router.Methods(http.MethodDelete).
+ HandlerFunc(s3APIMiddleware(api.DeleteBucketPolicyHandler)).
+ Queries("policy", "")
+ // DeleteBucketReplication
+ router.Methods(http.MethodDelete).
+ HandlerFunc(s3APIMiddleware(api.DeleteBucketReplicationConfigHandler)).
+ Queries("replication", "")
+ // DeleteBucketLifecycle
+ router.Methods(http.MethodDelete).
+ HandlerFunc(s3APIMiddleware(api.DeleteBucketLifecycleHandler)).
+ Queries("lifecycle", "")
+ // DeleteBucketEncryption
+ router.Methods(http.MethodDelete).
+ HandlerFunc(s3APIMiddleware(api.DeleteBucketEncryptionHandler)).
+ Queries("encryption", "")
+ // DeleteBucket
+ router.Methods(http.MethodDelete).
+ HandlerFunc(s3APIMiddleware(api.DeleteBucketHandler))
+
+ // MinIO extension API for replication.
+ //
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.GetBucketReplicationMetricsV2Handler)).
+ Queries("replication-metrics", "2")
+ // deprecated handler
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.GetBucketReplicationMetricsHandler)).
+ Queries("replication-metrics", "")
+
+ // ValidateBucketReplicationCreds
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.ValidateBucketReplicationCredsHandler)).
+ Queries("replication-check", "")
+
+ // Register rejected bucket APIs
+ for _, r := range rejectedBucketAPIs {
+ router.Methods(r.methods...).
+ HandlerFunc(collectAPIStats(r.api, httpTraceAll(notImplementedHandler))).
+ Queries(r.queries...)
+ }
+
+ // S3 ListObjectsV1 (Legacy)
+ router.Methods(http.MethodGet).
+ HandlerFunc(s3APIMiddleware(api.ListObjectsV1Handler))
+ }
+
+ // Root operation
+
+ // ListenNotification
+ apiRouter.Methods(http.MethodGet).Path(SlashSeparator).
+ HandlerFunc(s3APIMiddleware(api.ListenNotificationHandler, noThrottleS3HFlag, traceHdrsS3HFlag)).
+ Queries("events", "{events:.*}")
+
+ // ListBuckets
+ apiRouter.Methods(http.MethodGet).Path(SlashSeparator).
+ HandlerFunc(s3APIMiddleware(api.ListBucketsHandler))
+
+ // S3 browser with signature v4 adds '//' for ListBuckets request, so rather
+ // than failing with UnknownAPIRequest we simply handle it for now.
+ apiRouter.Methods(http.MethodGet).Path(SlashSeparator + SlashSeparator).
+ HandlerFunc(s3APIMiddleware(api.ListBucketsHandler))
+
+ // If none of the routes match add default error handler routes
+ apiRouter.NotFoundHandler = collectAPIStats("notfound", httpTraceAll(errorResponseHandler))
+ apiRouter.MethodNotAllowedHandler = collectAPIStats("methodnotallowed", httpTraceAll(methodNotAllowedHandler("S3")))
+}
+
+// corsHandler handler for CORS (Cross Origin Resource Sharing)
+func corsHandler(handler http.Handler) http.Handler {
+ commonS3Headers := []string{
+ xhttp.Date,
+ xhttp.ETag,
+ xhttp.ServerInfo,
+ xhttp.Connection,
+ xhttp.AcceptRanges,
+ xhttp.ContentRange,
+ xhttp.ContentEncoding,
+ xhttp.ContentLength,
+ xhttp.ContentType,
+ xhttp.ContentDisposition,
+ xhttp.LastModified,
+ xhttp.ContentLanguage,
+ xhttp.CacheControl,
+ xhttp.RetryAfter,
+ xhttp.AmzBucketRegion,
+ xhttp.Expires,
+ "X-Amz*",
+ "x-amz*",
+ "*",
+ }
+ opts := cors.Options{
+ AllowOriginFunc: func(origin string) bool {
+ for _, allowedOrigin := range globalAPIConfig.getCorsAllowOrigins() {
+ if wildcard.MatchSimple(allowedOrigin, origin) {
+ return true
+ }
+ }
+ return false
+ },
+ AllowedMethods: []string{
+ http.MethodGet,
+ http.MethodPut,
+ http.MethodHead,
+ http.MethodPost,
+ http.MethodDelete,
+ http.MethodOptions,
+ http.MethodPatch,
+ },
+ AllowedHeaders: commonS3Headers,
+ ExposedHeaders: commonS3Headers,
+ AllowCredentials: true,
+ }
+ return cors.New(opts).Handler(handler)
+}
diff --git a/cmd/api-utils.go b/cmd/api-utils.go
new file mode 100644
index 00000000..ee8fa533
--- /dev/null
+++ b/cmd/api-utils.go
@@ -0,0 +1,119 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "fmt"
+ "net/http"
+ "reflect"
+ "runtime"
+ "strings"
+)
+
+func shouldEscape(c byte) bool {
+ if 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' {
+ return false
+ }
+
+ switch c {
+ case '-', '_', '.', '/', '*':
+ return false
+ }
+ return true
+}
+
+// s3URLEncode is based on Golang's url.QueryEscape() code,
+// while considering some S3 exceptions:
+// - Avoid encoding '/' and '*'
+// - Force encoding of '~'
+func s3URLEncode(s string) string {
+ spaceCount, hexCount := 0, 0
+ for i := range len(s) {
+ c := s[i]
+ if shouldEscape(c) {
+ if c == ' ' {
+ spaceCount++
+ } else {
+ hexCount++
+ }
+ }
+ }
+
+ if spaceCount == 0 && hexCount == 0 {
+ return s
+ }
+
+ var buf [64]byte
+ var t []byte
+
+ required := len(s) + 2*hexCount
+ if required <= len(buf) {
+ t = buf[:required]
+ } else {
+ t = make([]byte, required)
+ }
+
+ if hexCount == 0 {
+ copy(t, s)
+ for i := range len(s) {
+ if s[i] == ' ' {
+ t[i] = '+'
+ }
+ }
+ return string(t)
+ }
+
+ j := 0
+ for i := range len(s) {
+ switch c := s[i]; {
+ case c == ' ':
+ t[j] = '+'
+ j++
+ case shouldEscape(c):
+ t[j] = '%'
+ t[j+1] = "0123456789ABCDEF"[c>>4]
+ t[j+2] = "0123456789ABCDEF"[c&15]
+ j += 3
+ default:
+ t[j] = s[i]
+ j++
+ }
+ }
+ return string(t)
+}
+
+// s3EncodeName encodes string in response when encodingType is specified in AWS S3 requests.
+func s3EncodeName(name, encodingType string) string {
+ if strings.ToLower(encodingType) == "url" {
+ return s3URLEncode(name)
+ }
+ return name
+}
+
+// getHandlerName returns the name of the handler function. It takes the type
+// name as a string to clean up the name retrieved via reflection. This function
+// only works correctly when the type is present in the cmd package.
+func getHandlerName(f http.HandlerFunc, cmdType string) string {
+ name := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
+
+ packageName := fmt.Sprintf("github.com/minio/minio/cmd.%s.", cmdType)
+ name = strings.TrimPrefix(name, packageName)
+ name = strings.TrimSuffix(name, "Handler-fm")
+ name = strings.TrimSuffix(name, "-fm")
+ return name
+}
diff --git a/cmd/api-utils_test.go b/cmd/api-utils_test.go
new file mode 100644
index 00000000..b8bc050f
--- /dev/null
+++ b/cmd/api-utils_test.go
@@ -0,0 +1,49 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestS3EncodeName(t *testing.T) {
+ testCases := []struct {
+ inputText, encodingType, expectedOutput string
+ }{
+ {"a b", "", "a b"},
+ {"a b", "url", "a+b"},
+ {"p- ", "url", "p-+"},
+ {"p-%", "url", "p-%25"},
+ {"p/", "url", "p/"},
+ {"p/", "url", "p/"},
+ {"~user", "url", "%7Euser"},
+ {"*user", "url", "*user"},
+ {"user+password", "url", "user%2Bpassword"},
+ {"_user", "url", "_user"},
+ {"firstname.lastname", "url", "firstname.lastname"},
+ }
+ for i, testCase := range testCases {
+ t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
+ outputText := s3EncodeName(testCase.inputText, testCase.encodingType)
+ if testCase.expectedOutput != outputText {
+ t.Errorf("Expected `%s`, got `%s`", testCase.expectedOutput, outputText)
+ }
+ })
+ }
+}
diff --git a/cmd/apierrorcode_string.go b/cmd/apierrorcode_string.go
new file mode 100644
index 00000000..d9973d88
--- /dev/null
+++ b/cmd/apierrorcode_string.go
@@ -0,0 +1,354 @@
+// Code generated by "stringer -type=APIErrorCode -trimprefix=Err api-errors.go"; DO NOT EDIT.
+
+package cmd
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[ErrNone-0]
+ _ = x[ErrAccessDenied-1]
+ _ = x[ErrBadDigest-2]
+ _ = x[ErrEntityTooSmall-3]
+ _ = x[ErrEntityTooLarge-4]
+ _ = x[ErrPolicyTooLarge-5]
+ _ = x[ErrIncompleteBody-6]
+ _ = x[ErrInternalError-7]
+ _ = x[ErrInvalidAccessKeyID-8]
+ _ = x[ErrAccessKeyDisabled-9]
+ _ = x[ErrInvalidArgument-10]
+ _ = x[ErrInvalidBucketName-11]
+ _ = x[ErrInvalidDigest-12]
+ _ = x[ErrInvalidRange-13]
+ _ = x[ErrInvalidRangePartNumber-14]
+ _ = x[ErrInvalidCopyPartRange-15]
+ _ = x[ErrInvalidCopyPartRangeSource-16]
+ _ = x[ErrInvalidMaxKeys-17]
+ _ = x[ErrInvalidEncodingMethod-18]
+ _ = x[ErrInvalidMaxUploads-19]
+ _ = x[ErrInvalidMaxParts-20]
+ _ = x[ErrInvalidPartNumberMarker-21]
+ _ = x[ErrInvalidPartNumber-22]
+ _ = x[ErrInvalidRequestBody-23]
+ _ = x[ErrInvalidCopySource-24]
+ _ = x[ErrInvalidMetadataDirective-25]
+ _ = x[ErrInvalidCopyDest-26]
+ _ = x[ErrInvalidPolicyDocument-27]
+ _ = x[ErrInvalidObjectState-28]
+ _ = x[ErrMalformedXML-29]
+ _ = x[ErrMissingContentLength-30]
+ _ = x[ErrMissingContentMD5-31]
+ _ = x[ErrMissingRequestBodyError-32]
+ _ = x[ErrMissingSecurityHeader-33]
+ _ = x[ErrNoSuchBucket-34]
+ _ = x[ErrNoSuchBucketPolicy-35]
+ _ = x[ErrNoSuchBucketLifecycle-36]
+ _ = x[ErrNoSuchLifecycleConfiguration-37]
+ _ = x[ErrInvalidLifecycleWithObjectLock-38]
+ _ = x[ErrNoSuchBucketSSEConfig-39]
+ _ = x[ErrNoSuchCORSConfiguration-40]
+ _ = x[ErrNoSuchWebsiteConfiguration-41]
+ _ = x[ErrReplicationConfigurationNotFoundError-42]
+ _ = x[ErrRemoteDestinationNotFoundError-43]
+ _ = x[ErrReplicationDestinationMissingLock-44]
+ _ = x[ErrRemoteTargetNotFoundError-45]
+ _ = x[ErrReplicationRemoteConnectionError-46]
+ _ = x[ErrReplicationBandwidthLimitError-47]
+ _ = x[ErrBucketRemoteIdenticalToSource-48]
+ _ = x[ErrBucketRemoteAlreadyExists-49]
+ _ = x[ErrBucketRemoteLabelInUse-50]
+ _ = x[ErrBucketRemoteArnTypeInvalid-51]
+ _ = x[ErrBucketRemoteArnInvalid-52]
+ _ = x[ErrBucketRemoteRemoveDisallowed-53]
+ _ = x[ErrRemoteTargetNotVersionedError-54]
+ _ = x[ErrReplicationSourceNotVersionedError-55]
+ _ = x[ErrReplicationNeedsVersioningError-56]
+ _ = x[ErrReplicationBucketNeedsVersioningError-57]
+ _ = x[ErrReplicationDenyEditError-58]
+ _ = x[ErrRemoteTargetDenyAddError-59]
+ _ = x[ErrReplicationNoExistingObjects-60]
+ _ = x[ErrReplicationValidationError-61]
+ _ = x[ErrReplicationPermissionCheckError-62]
+ _ = x[ErrObjectRestoreAlreadyInProgress-63]
+ _ = x[ErrNoSuchKey-64]
+ _ = x[ErrNoSuchUpload-65]
+ _ = x[ErrInvalidVersionID-66]
+ _ = x[ErrNoSuchVersion-67]
+ _ = x[ErrNotImplemented-68]
+ _ = x[ErrPreconditionFailed-69]
+ _ = x[ErrRequestTimeTooSkewed-70]
+ _ = x[ErrSignatureDoesNotMatch-71]
+ _ = x[ErrMethodNotAllowed-72]
+ _ = x[ErrInvalidPart-73]
+ _ = x[ErrInvalidPartOrder-74]
+ _ = x[ErrMissingPart-75]
+ _ = x[ErrAuthorizationHeaderMalformed-76]
+ _ = x[ErrMalformedPOSTRequest-77]
+ _ = x[ErrPOSTFileRequired-78]
+ _ = x[ErrSignatureVersionNotSupported-79]
+ _ = x[ErrBucketNotEmpty-80]
+ _ = x[ErrAllAccessDisabled-81]
+ _ = x[ErrPolicyInvalidVersion-82]
+ _ = x[ErrMissingFields-83]
+ _ = x[ErrMissingCredTag-84]
+ _ = x[ErrCredMalformed-85]
+ _ = x[ErrInvalidRegion-86]
+ _ = x[ErrInvalidServiceS3-87]
+ _ = x[ErrInvalidServiceSTS-88]
+ _ = x[ErrInvalidRequestVersion-89]
+ _ = x[ErrMissingSignTag-90]
+ _ = x[ErrMissingSignHeadersTag-91]
+ _ = x[ErrMalformedDate-92]
+ _ = x[ErrMalformedPresignedDate-93]
+ _ = x[ErrMalformedCredentialDate-94]
+ _ = x[ErrMalformedExpires-95]
+ _ = x[ErrNegativeExpires-96]
+ _ = x[ErrAuthHeaderEmpty-97]
+ _ = x[ErrExpiredPresignRequest-98]
+ _ = x[ErrRequestNotReadyYet-99]
+ _ = x[ErrUnsignedHeaders-100]
+ _ = x[ErrMissingDateHeader-101]
+ _ = x[ErrInvalidQuerySignatureAlgo-102]
+ _ = x[ErrInvalidQueryParams-103]
+ _ = x[ErrBucketAlreadyOwnedByYou-104]
+ _ = x[ErrInvalidDuration-105]
+ _ = x[ErrBucketAlreadyExists-106]
+ _ = x[ErrMetadataTooLarge-107]
+ _ = x[ErrUnsupportedMetadata-108]
+ _ = x[ErrUnsupportedHostHeader-109]
+ _ = x[ErrMaximumExpires-110]
+ _ = x[ErrSlowDownRead-111]
+ _ = x[ErrSlowDownWrite-112]
+ _ = x[ErrMaxVersionsExceeded-113]
+ _ = x[ErrInvalidPrefixMarker-114]
+ _ = x[ErrBadRequest-115]
+ _ = x[ErrKeyTooLongError-116]
+ _ = x[ErrInvalidBucketObjectLockConfiguration-117]
+ _ = x[ErrObjectLockConfigurationNotFound-118]
+ _ = x[ErrObjectLockConfigurationNotAllowed-119]
+ _ = x[ErrNoSuchObjectLockConfiguration-120]
+ _ = x[ErrObjectLocked-121]
+ _ = x[ErrInvalidRetentionDate-122]
+ _ = x[ErrPastObjectLockRetainDate-123]
+ _ = x[ErrUnknownWORMModeDirective-124]
+ _ = x[ErrBucketTaggingNotFound-125]
+ _ = x[ErrObjectLockInvalidHeaders-126]
+ _ = x[ErrInvalidTagDirective-127]
+ _ = x[ErrPolicyAlreadyAttached-128]
+ _ = x[ErrPolicyNotAttached-129]
+ _ = x[ErrExcessData-130]
+ _ = x[ErrPolicyInvalidName-131]
+ _ = x[ErrNoTokenRevokeType-132]
+ _ = x[ErrAdminOpenIDNotEnabled-133]
+ _ = x[ErrAdminNoSuchAccessKey-134]
+ _ = x[ErrInvalidEncryptionMethod-135]
+ _ = x[ErrInvalidEncryptionKeyID-136]
+ _ = x[ErrInsecureSSECustomerRequest-137]
+ _ = x[ErrSSEMultipartEncrypted-138]
+ _ = x[ErrSSEEncryptedObject-139]
+ _ = x[ErrInvalidEncryptionParameters-140]
+ _ = x[ErrInvalidEncryptionParametersSSEC-141]
+ _ = x[ErrInvalidSSECustomerAlgorithm-142]
+ _ = x[ErrInvalidSSECustomerKey-143]
+ _ = x[ErrMissingSSECustomerKey-144]
+ _ = x[ErrMissingSSECustomerKeyMD5-145]
+ _ = x[ErrSSECustomerKeyMD5Mismatch-146]
+ _ = x[ErrInvalidSSECustomerParameters-147]
+ _ = x[ErrIncompatibleEncryptionMethod-148]
+ _ = x[ErrKMSNotConfigured-149]
+ _ = x[ErrKMSKeyNotFoundException-150]
+ _ = x[ErrKMSDefaultKeyAlreadyConfigured-151]
+ _ = x[ErrNoAccessKey-152]
+ _ = x[ErrInvalidToken-153]
+ _ = x[ErrEventNotification-154]
+ _ = x[ErrARNNotification-155]
+ _ = x[ErrRegionNotification-156]
+ _ = x[ErrOverlappingFilterNotification-157]
+ _ = x[ErrFilterNameInvalid-158]
+ _ = x[ErrFilterNamePrefix-159]
+ _ = x[ErrFilterNameSuffix-160]
+ _ = x[ErrFilterValueInvalid-161]
+ _ = x[ErrOverlappingConfigs-162]
+ _ = x[ErrUnsupportedNotification-163]
+ _ = x[ErrContentSHA256Mismatch-164]
+ _ = x[ErrContentChecksumMismatch-165]
+ _ = x[ErrStorageFull-166]
+ _ = x[ErrRequestBodyParse-167]
+ _ = x[ErrObjectExistsAsDirectory-168]
+ _ = x[ErrInvalidObjectName-169]
+ _ = x[ErrInvalidObjectNamePrefixSlash-170]
+ _ = x[ErrInvalidResourceName-171]
+ _ = x[ErrInvalidLifecycleQueryParameter-172]
+ _ = x[ErrServerNotInitialized-173]
+ _ = x[ErrBucketMetadataNotInitialized-174]
+ _ = x[ErrRequestTimedout-175]
+ _ = x[ErrClientDisconnected-176]
+ _ = x[ErrTooManyRequests-177]
+ _ = x[ErrInvalidRequest-178]
+ _ = x[ErrTransitionStorageClassNotFoundError-179]
+ _ = x[ErrInvalidStorageClass-180]
+ _ = x[ErrBackendDown-181]
+ _ = x[ErrMalformedJSON-182]
+ _ = x[ErrAdminNoSuchUser-183]
+ _ = x[ErrAdminNoSuchUserLDAPWarn-184]
+ _ = x[ErrAdminLDAPExpectedLoginName-185]
+ _ = x[ErrAdminNoSuchGroup-186]
+ _ = x[ErrAdminGroupNotEmpty-187]
+ _ = x[ErrAdminGroupDisabled-188]
+ _ = x[ErrAdminInvalidGroupName-189]
+ _ = x[ErrAdminNoSuchJob-190]
+ _ = x[ErrAdminNoSuchPolicy-191]
+ _ = x[ErrAdminPolicyChangeAlreadyApplied-192]
+ _ = x[ErrAdminInvalidArgument-193]
+ _ = x[ErrAdminInvalidAccessKey-194]
+ _ = x[ErrAdminInvalidSecretKey-195]
+ _ = x[ErrAdminConfigNoQuorum-196]
+ _ = x[ErrAdminConfigTooLarge-197]
+ _ = x[ErrAdminConfigBadJSON-198]
+ _ = x[ErrAdminNoSuchConfigTarget-199]
+ _ = x[ErrAdminConfigEnvOverridden-200]
+ _ = x[ErrAdminConfigDuplicateKeys-201]
+ _ = x[ErrAdminConfigInvalidIDPType-202]
+ _ = x[ErrAdminConfigLDAPNonDefaultConfigName-203]
+ _ = x[ErrAdminConfigLDAPValidation-204]
+ _ = x[ErrAdminConfigIDPCfgNameAlreadyExists-205]
+ _ = x[ErrAdminConfigIDPCfgNameDoesNotExist-206]
+ _ = x[ErrInsecureClientRequest-207]
+ _ = x[ErrObjectTampered-208]
+ _ = x[ErrAdminLDAPNotEnabled-209]
+ _ = x[ErrSiteReplicationInvalidRequest-210]
+ _ = x[ErrSiteReplicationPeerResp-211]
+ _ = x[ErrSiteReplicationBackendIssue-212]
+ _ = x[ErrSiteReplicationServiceAccountError-213]
+ _ = x[ErrSiteReplicationBucketConfigError-214]
+ _ = x[ErrSiteReplicationBucketMetaError-215]
+ _ = x[ErrSiteReplicationIAMError-216]
+ _ = x[ErrSiteReplicationConfigMissing-217]
+ _ = x[ErrSiteReplicationIAMConfigMismatch-218]
+ _ = x[ErrAdminRebalanceAlreadyStarted-219]
+ _ = x[ErrAdminRebalanceNotStarted-220]
+ _ = x[ErrAdminBucketQuotaExceeded-221]
+ _ = x[ErrAdminNoSuchQuotaConfiguration-222]
+ _ = x[ErrHealNotImplemented-223]
+ _ = x[ErrHealNoSuchProcess-224]
+ _ = x[ErrHealInvalidClientToken-225]
+ _ = x[ErrHealMissingBucket-226]
+ _ = x[ErrHealAlreadyRunning-227]
+ _ = x[ErrHealOverlappingPaths-228]
+ _ = x[ErrIncorrectContinuationToken-229]
+ _ = x[ErrEmptyRequestBody-230]
+ _ = x[ErrUnsupportedFunction-231]
+ _ = x[ErrInvalidExpressionType-232]
+ _ = x[ErrBusy-233]
+ _ = x[ErrUnauthorizedAccess-234]
+ _ = x[ErrExpressionTooLong-235]
+ _ = x[ErrIllegalSQLFunctionArgument-236]
+ _ = x[ErrInvalidKeyPath-237]
+ _ = x[ErrInvalidCompressionFormat-238]
+ _ = x[ErrInvalidFileHeaderInfo-239]
+ _ = x[ErrInvalidJSONType-240]
+ _ = x[ErrInvalidQuoteFields-241]
+ _ = x[ErrInvalidRequestParameter-242]
+ _ = x[ErrInvalidDataType-243]
+ _ = x[ErrInvalidTextEncoding-244]
+ _ = x[ErrInvalidDataSource-245]
+ _ = x[ErrInvalidTableAlias-246]
+ _ = x[ErrMissingRequiredParameter-247]
+ _ = x[ErrObjectSerializationConflict-248]
+ _ = x[ErrUnsupportedSQLOperation-249]
+ _ = x[ErrUnsupportedSQLStructure-250]
+ _ = x[ErrUnsupportedSyntax-251]
+ _ = x[ErrUnsupportedRangeHeader-252]
+ _ = x[ErrLexerInvalidChar-253]
+ _ = x[ErrLexerInvalidOperator-254]
+ _ = x[ErrLexerInvalidLiteral-255]
+ _ = x[ErrLexerInvalidIONLiteral-256]
+ _ = x[ErrParseExpectedDatePart-257]
+ _ = x[ErrParseExpectedKeyword-258]
+ _ = x[ErrParseExpectedTokenType-259]
+ _ = x[ErrParseExpected2TokenTypes-260]
+ _ = x[ErrParseExpectedNumber-261]
+ _ = x[ErrParseExpectedRightParenBuiltinFunctionCall-262]
+ _ = x[ErrParseExpectedTypeName-263]
+ _ = x[ErrParseExpectedWhenClause-264]
+ _ = x[ErrParseUnsupportedToken-265]
+ _ = x[ErrParseUnsupportedLiteralsGroupBy-266]
+ _ = x[ErrParseExpectedMember-267]
+ _ = x[ErrParseUnsupportedSelect-268]
+ _ = x[ErrParseUnsupportedCase-269]
+ _ = x[ErrParseUnsupportedCaseClause-270]
+ _ = x[ErrParseUnsupportedAlias-271]
+ _ = x[ErrParseUnsupportedSyntax-272]
+ _ = x[ErrParseUnknownOperator-273]
+ _ = x[ErrParseMissingIdentAfterAt-274]
+ _ = x[ErrParseUnexpectedOperator-275]
+ _ = x[ErrParseUnexpectedTerm-276]
+ _ = x[ErrParseUnexpectedToken-277]
+ _ = x[ErrParseUnexpectedKeyword-278]
+ _ = x[ErrParseExpectedExpression-279]
+ _ = x[ErrParseExpectedLeftParenAfterCast-280]
+ _ = x[ErrParseExpectedLeftParenValueConstructor-281]
+ _ = x[ErrParseExpectedLeftParenBuiltinFunctionCall-282]
+ _ = x[ErrParseExpectedArgumentDelimiter-283]
+ _ = x[ErrParseCastArity-284]
+ _ = x[ErrParseInvalidTypeParam-285]
+ _ = x[ErrParseEmptySelect-286]
+ _ = x[ErrParseSelectMissingFrom-287]
+ _ = x[ErrParseExpectedIdentForGroupName-288]
+ _ = x[ErrParseExpectedIdentForAlias-289]
+ _ = x[ErrParseUnsupportedCallWithStar-290]
+ _ = x[ErrParseNonUnaryAggregateFunctionCall-291]
+ _ = x[ErrParseMalformedJoin-292]
+ _ = x[ErrParseExpectedIdentForAt-293]
+ _ = x[ErrParseAsteriskIsNotAloneInSelectList-294]
+ _ = x[ErrParseCannotMixSqbAndWildcardInSelectList-295]
+ _ = x[ErrParseInvalidContextForWildcardInSelectList-296]
+ _ = x[ErrIncorrectSQLFunctionArgumentType-297]
+ _ = x[ErrValueParseFailure-298]
+ _ = x[ErrEvaluatorInvalidArguments-299]
+ _ = x[ErrIntegerOverflow-300]
+ _ = x[ErrLikeInvalidInputs-301]
+ _ = x[ErrCastFailed-302]
+ _ = x[ErrInvalidCast-303]
+ _ = x[ErrEvaluatorInvalidTimestampFormatPattern-304]
+ _ = x[ErrEvaluatorInvalidTimestampFormatPatternSymbolForParsing-305]
+ _ = x[ErrEvaluatorTimestampFormatPatternDuplicateFields-306]
+ _ = x[ErrEvaluatorTimestampFormatPatternHourClockAmPmMismatch-307]
+ _ = x[ErrEvaluatorUnterminatedTimestampFormatPatternToken-308]
+ _ = x[ErrEvaluatorInvalidTimestampFormatPatternToken-309]
+ _ = x[ErrEvaluatorInvalidTimestampFormatPatternSymbol-310]
+ _ = x[ErrEvaluatorBindingDoesNotExist-311]
+ _ = x[ErrMissingHeaders-312]
+ _ = x[ErrInvalidColumnIndex-313]
+ _ = x[ErrAdminConfigNotificationTargetsFailed-314]
+ _ = x[ErrAdminProfilerNotEnabled-315]
+ _ = x[ErrInvalidDecompressedSize-316]
+ _ = x[ErrAddUserInvalidArgument-317]
+ _ = x[ErrAddUserValidUTF-318]
+ _ = x[ErrAdminResourceInvalidArgument-319]
+ _ = x[ErrAdminAccountNotEligible-320]
+ _ = x[ErrAccountNotEligible-321]
+ _ = x[ErrAdminServiceAccountNotFound-322]
+ _ = x[ErrPostPolicyConditionInvalidFormat-323]
+ _ = x[ErrInvalidChecksum-324]
+ _ = x[ErrLambdaARNInvalid-325]
+ _ = x[ErrLambdaARNNotFound-326]
+ _ = x[ErrInvalidAttributeName-327]
+ _ = x[ErrAdminNoAccessKey-328]
+ _ = x[ErrAdminNoSecretKey-329]
+ _ = x[ErrIAMNotInitialized-330]
+ _ = x[apiErrCodeEnd-331]
+}
+
+const _APIErrorCode_name = "NoneAccessDeniedBadDigestEntityTooSmallEntityTooLargePolicyTooLargeIncompleteBodyInternalErrorInvalidAccessKeyIDAccessKeyDisabledInvalidArgumentInvalidBucketNameInvalidDigestInvalidRangeInvalidRangePartNumberInvalidCopyPartRangeInvalidCopyPartRangeSourceInvalidMaxKeysInvalidEncodingMethodInvalidMaxUploadsInvalidMaxPartsInvalidPartNumberMarkerInvalidPartNumberInvalidRequestBodyInvalidCopySourceInvalidMetadataDirectiveInvalidCopyDestInvalidPolicyDocumentInvalidObjectStateMalformedXMLMissingContentLengthMissingContentMD5MissingRequestBodyErrorMissingSecurityHeaderNoSuchBucketNoSuchBucketPolicyNoSuchBucketLifecycleNoSuchLifecycleConfigurationInvalidLifecycleWithObjectLockNoSuchBucketSSEConfigNoSuchCORSConfigurationNoSuchWebsiteConfigurationReplicationConfigurationNotFoundErrorRemoteDestinationNotFoundErrorReplicationDestinationMissingLockRemoteTargetNotFoundErrorReplicationRemoteConnectionErrorReplicationBandwidthLimitErrorBucketRemoteIdenticalToSourceBucketRemoteAlreadyExistsBucketRemoteLabelInUseBucketRemoteArnTypeInvalidBucketRemoteArnInvalidBucketRemoteRemoveDisallowedRemoteTargetNotVersionedErrorReplicationSourceNotVersionedErrorReplicationNeedsVersioningErrorReplicationBucketNeedsVersioningErrorReplicationDenyEditErrorRemoteTargetDenyAddErrorReplicationNoExistingObjectsReplicationValidationErrorReplicationPermissionCheckErrorObjectRestoreAlreadyInProgressNoSuchKeyNoSuchUploadInvalidVersionIDNoSuchVersionNotImplementedPreconditionFailedRequestTimeTooSkewedSignatureDoesNotMatchMethodNotAllowedInvalidPartInvalidPartOrderMissingPartAuthorizationHeaderMalformedMalformedPOSTRequestPOSTFileRequiredSignatureVersionNotSupportedBucketNotEmptyAllAccessDisabledPolicyInvalidVersionMissingFieldsMissingCredTagCredMalformedInvalidRegionInvalidServiceS3InvalidServiceSTSInvalidRequestVersionMissingSignTagMissingSignHeadersTagMalformedDateMalformedPresignedDateMalformedCredentialDateMalformedExpiresNegativeExpiresAuthHeaderEmptyExpiredPresignRequestRequestNotReadyYetUnsignedHeadersMissingDateHeaderInvalidQuerySignatureAlgoInvalidQueryParamsBucketAlreadyOwnedByYouInvalidDurationBucketAlreadyExistsMetadataTooLargeUnsupportedMetadataUnsupportedHostHeaderMaximumExpiresSlowDownReadSlowDownWriteMaxVersionsExceededInvalidPrefixMarkerBadRequestKeyTooLongErrorInvalidBucketObjectLockConfigurationObjectLockConfigurationNotFoundObjectLockConfigurationNotAllowedNoSuchObjectLockConfigurationObjectLockedInvalidRetentionDatePastObjectLockRetainDateUnknownWORMModeDirectiveBucketTaggingNotFoundObjectLockInvalidHeadersInvalidTagDirectivePolicyAlreadyAttachedPolicyNotAttachedExcessDataPolicyInvalidNameNoTokenRevokeTypeAdminOpenIDNotEnabledAdminNoSuchAccessKeyInvalidEncryptionMethodInvalidEncryptionKeyIDInsecureSSECustomerRequestSSEMultipartEncryptedSSEEncryptedObjectInvalidEncryptionParametersInvalidEncryptionParametersSSECInvalidSSECustomerAlgorithmInvalidSSECustomerKeyMissingSSECustomerKeyMissingSSECustomerKeyMD5SSECustomerKeyMD5MismatchInvalidSSECustomerParametersIncompatibleEncryptionMethodKMSNotConfiguredKMSKeyNotFoundExceptionKMSDefaultKeyAlreadyConfiguredNoAccessKeyInvalidTokenEventNotificationARNNotificationRegionNotificationOverlappingFilterNotificationFilterNameInvalidFilterNamePrefixFilterNameSuffixFilterValueInvalidOverlappingConfigsUnsupportedNotificationContentSHA256MismatchContentChecksumMismatchStorageFullRequestBodyParseObjectExistsAsDirectoryInvalidObjectNameInvalidObjectNamePrefixSlashInvalidResourceNameInvalidLifecycleQueryParameterServerNotInitializedBucketMetadataNotInitializedRequestTimedoutClientDisconnectedTooManyRequestsInvalidRequestTransitionStorageClassNotFoundErrorInvalidStorageClassBackendDownMalformedJSONAdminNoSuchUserAdminNoSuchUserLDAPWarnAdminLDAPExpectedLoginNameAdminNoSuchGroupAdminGroupNotEmptyAdminGroupDisabledAdminInvalidGroupNameAdminNoSuchJobAdminNoSuchPolicyAdminPolicyChangeAlreadyAppliedAdminInvalidArgumentAdminInvalidAccessKeyAdminInvalidSecretKeyAdminConfigNoQuorumAdminConfigTooLargeAdminConfigBadJSONAdminNoSuchConfigTargetAdminConfigEnvOverriddenAdminConfigDuplicateKeysAdminConfigInvalidIDPTypeAdminConfigLDAPNonDefaultConfigNameAdminConfigLDAPValidationAdminConfigIDPCfgNameAlreadyExistsAdminConfigIDPCfgNameDoesNotExistInsecureClientRequestObjectTamperedAdminLDAPNotEnabledSiteReplicationInvalidRequestSiteReplicationPeerRespSiteReplicationBackendIssueSiteReplicationServiceAccountErrorSiteReplicationBucketConfigErrorSiteReplicationBucketMetaErrorSiteReplicationIAMErrorSiteReplicationConfigMissingSiteReplicationIAMConfigMismatchAdminRebalanceAlreadyStartedAdminRebalanceNotStartedAdminBucketQuotaExceededAdminNoSuchQuotaConfigurationHealNotImplementedHealNoSuchProcessHealInvalidClientTokenHealMissingBucketHealAlreadyRunningHealOverlappingPathsIncorrectContinuationTokenEmptyRequestBodyUnsupportedFunctionInvalidExpressionTypeBusyUnauthorizedAccessExpressionTooLongIllegalSQLFunctionArgumentInvalidKeyPathInvalidCompressionFormatInvalidFileHeaderInfoInvalidJSONTypeInvalidQuoteFieldsInvalidRequestParameterInvalidDataTypeInvalidTextEncodingInvalidDataSourceInvalidTableAliasMissingRequiredParameterObjectSerializationConflictUnsupportedSQLOperationUnsupportedSQLStructureUnsupportedSyntaxUnsupportedRangeHeaderLexerInvalidCharLexerInvalidOperatorLexerInvalidLiteralLexerInvalidIONLiteralParseExpectedDatePartParseExpectedKeywordParseExpectedTokenTypeParseExpected2TokenTypesParseExpectedNumberParseExpectedRightParenBuiltinFunctionCallParseExpectedTypeNameParseExpectedWhenClauseParseUnsupportedTokenParseUnsupportedLiteralsGroupByParseExpectedMemberParseUnsupportedSelectParseUnsupportedCaseParseUnsupportedCaseClauseParseUnsupportedAliasParseUnsupportedSyntaxParseUnknownOperatorParseMissingIdentAfterAtParseUnexpectedOperatorParseUnexpectedTermParseUnexpectedTokenParseUnexpectedKeywordParseExpectedExpressionParseExpectedLeftParenAfterCastParseExpectedLeftParenValueConstructorParseExpectedLeftParenBuiltinFunctionCallParseExpectedArgumentDelimiterParseCastArityParseInvalidTypeParamParseEmptySelectParseSelectMissingFromParseExpectedIdentForGroupNameParseExpectedIdentForAliasParseUnsupportedCallWithStarParseNonUnaryAggregateFunctionCallParseMalformedJoinParseExpectedIdentForAtParseAsteriskIsNotAloneInSelectListParseCannotMixSqbAndWildcardInSelectListParseInvalidContextForWildcardInSelectListIncorrectSQLFunctionArgumentTypeValueParseFailureEvaluatorInvalidArgumentsIntegerOverflowLikeInvalidInputsCastFailedInvalidCastEvaluatorInvalidTimestampFormatPatternEvaluatorInvalidTimestampFormatPatternSymbolForParsingEvaluatorTimestampFormatPatternDuplicateFieldsEvaluatorTimestampFormatPatternHourClockAmPmMismatchEvaluatorUnterminatedTimestampFormatPatternTokenEvaluatorInvalidTimestampFormatPatternTokenEvaluatorInvalidTimestampFormatPatternSymbolEvaluatorBindingDoesNotExistMissingHeadersInvalidColumnIndexAdminConfigNotificationTargetsFailedAdminProfilerNotEnabledInvalidDecompressedSizeAddUserInvalidArgumentAddUserValidUTFAdminResourceInvalidArgumentAdminAccountNotEligibleAccountNotEligibleAdminServiceAccountNotFoundPostPolicyConditionInvalidFormatInvalidChecksumLambdaARNInvalidLambdaARNNotFoundInvalidAttributeNameAdminNoAccessKeyAdminNoSecretKeyIAMNotInitializedapiErrCodeEnd"
+
+var _APIErrorCode_index = [...]uint16{0, 4, 16, 25, 39, 53, 67, 81, 94, 112, 129, 144, 161, 174, 186, 208, 228, 254, 268, 289, 306, 321, 344, 361, 379, 396, 420, 435, 456, 474, 486, 506, 523, 546, 567, 579, 597, 618, 646, 676, 697, 720, 746, 783, 813, 846, 871, 903, 933, 962, 987, 1009, 1035, 1057, 1085, 1114, 1148, 1179, 1216, 1240, 1264, 1292, 1318, 1349, 1379, 1388, 1400, 1416, 1429, 1443, 1461, 1481, 1502, 1518, 1529, 1545, 1556, 1584, 1604, 1620, 1648, 1662, 1679, 1699, 1712, 1726, 1739, 1752, 1768, 1785, 1806, 1820, 1841, 1854, 1876, 1899, 1915, 1930, 1945, 1966, 1984, 1999, 2016, 2041, 2059, 2082, 2097, 2116, 2132, 2151, 2172, 2186, 2198, 2211, 2230, 2249, 2259, 2274, 2310, 2341, 2374, 2403, 2415, 2435, 2459, 2483, 2504, 2528, 2547, 2568, 2585, 2595, 2612, 2629, 2650, 2670, 2693, 2715, 2741, 2762, 2780, 2807, 2838, 2865, 2886, 2907, 2931, 2956, 2984, 3012, 3028, 3051, 3081, 3092, 3104, 3121, 3136, 3154, 3183, 3200, 3216, 3232, 3250, 3268, 3291, 3312, 3335, 3346, 3362, 3385, 3402, 3430, 3449, 3479, 3499, 3527, 3542, 3560, 3575, 3589, 3624, 3643, 3654, 3667, 3682, 3705, 3731, 3747, 3765, 3783, 3804, 3818, 3835, 3866, 3886, 3907, 3928, 3947, 3966, 3984, 4007, 4031, 4055, 4080, 4115, 4140, 4174, 4207, 4228, 4242, 4261, 4290, 4313, 4340, 4374, 4406, 4436, 4459, 4487, 4519, 4547, 4571, 4595, 4624, 4642, 4659, 4681, 4698, 4716, 4736, 4762, 4778, 4797, 4818, 4822, 4840, 4857, 4883, 4897, 4921, 4942, 4957, 4975, 4998, 5013, 5032, 5049, 5066, 5090, 5117, 5140, 5163, 5180, 5202, 5218, 5238, 5257, 5279, 5300, 5320, 5342, 5366, 5385, 5427, 5448, 5471, 5492, 5523, 5542, 5564, 5584, 5610, 5631, 5653, 5673, 5697, 5720, 5739, 5759, 5781, 5804, 5835, 5873, 5914, 5944, 5958, 5979, 5995, 6017, 6047, 6073, 6101, 6135, 6153, 6176, 6211, 6251, 6293, 6325, 6342, 6367, 6382, 6399, 6409, 6420, 6458, 6512, 6558, 6610, 6658, 6701, 6745, 6773, 6787, 6805, 6841, 6864, 6887, 6909, 6924, 6952, 6975, 6993, 7020, 7052, 7067, 7083, 7100, 7120, 7136, 7152, 7169, 7182}
+
+func (i APIErrorCode) String() string {
+ if i < 0 || i >= APIErrorCode(len(_APIErrorCode_index)-1) {
+ return "APIErrorCode(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _APIErrorCode_name[_APIErrorCode_index[i]:_APIErrorCode_index[i+1]]
+}
diff --git a/cmd/auth-handler.go b/cmd/auth-handler.go
new file mode 100644
index 00000000..6e824e31
--- /dev/null
+++ b/cmd/auth-handler.go
@@ -0,0 +1,785 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "crypto/subtle"
+ "encoding/base64"
+ "encoding/hex"
+ "errors"
+ "io"
+ "mime"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "sync/atomic"
+ "time"
+
+ "github.com/minio/minio/internal/auth"
+ objectlock "github.com/minio/minio/internal/bucket/object/lock"
+ "github.com/minio/minio/internal/etag"
+ "github.com/minio/minio/internal/hash"
+ xhttp "github.com/minio/minio/internal/http"
+ xjwt "github.com/minio/minio/internal/jwt"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/minio/internal/mcontext"
+ "github.com/minio/pkg/v3/policy"
+)
+
+// Verify if request has JWT.
+func isRequestJWT(r *http.Request) bool {
+ return strings.HasPrefix(r.Header.Get(xhttp.Authorization), jwtAlgorithm)
+}
+
+// Verify if request has AWS Signature Version '4'.
+func isRequestSignatureV4(r *http.Request) bool {
+ return strings.HasPrefix(r.Header.Get(xhttp.Authorization), signV4Algorithm)
+}
+
+// Verify if request has AWS Signature Version '2'.
+func isRequestSignatureV2(r *http.Request) bool {
+ return (!strings.HasPrefix(r.Header.Get(xhttp.Authorization), signV4Algorithm) &&
+ strings.HasPrefix(r.Header.Get(xhttp.Authorization), signV2Algorithm))
+}
+
+// Verify if request has AWS PreSign Version '4'.
+func isRequestPresignedSignatureV4(r *http.Request) bool {
+ _, ok := r.Form[xhttp.AmzCredential]
+ return ok
+}
+
+// Verify request has AWS PreSign Version '2'.
+func isRequestPresignedSignatureV2(r *http.Request) bool {
+ _, ok := r.Form[xhttp.AmzAccessKeyID]
+ return ok
+}
+
+// Verify if request has AWS Post policy Signature Version '4'.
+func isRequestPostPolicySignatureV4(r *http.Request) bool {
+ mediaType, _, err := mime.ParseMediaType(r.Header.Get(xhttp.ContentType))
+ if err != nil {
+ return false
+ }
+ return mediaType == "multipart/form-data" && r.Method == http.MethodPost
+}
+
+// Verify if the request has AWS Streaming Signature Version '4'. This is only valid for 'PUT' operation.
+func isRequestSignStreamingV4(r *http.Request) bool {
+ return r.Header.Get(xhttp.AmzContentSha256) == streamingContentSHA256 &&
+ r.Method == http.MethodPut
+}
+
+// Verify if the request has AWS Streaming Signature Version '4'. This is only valid for 'PUT' operation.
+func isRequestSignStreamingTrailerV4(r *http.Request) bool {
+ return r.Header.Get(xhttp.AmzContentSha256) == streamingContentSHA256Trailer &&
+ r.Method == http.MethodPut
+}
+
+// Verify if the request has AWS Streaming Signature Version '4', with unsigned content and trailer.
+func isRequestUnsignedTrailerV4(r *http.Request) bool {
+ return r.Header.Get(xhttp.AmzContentSha256) == unsignedPayloadTrailer &&
+ r.Method == http.MethodPut
+}
+
+// Authorization type.
+//
+//go:generate stringer -type=authType -trimprefix=authType $GOFILE
+type authType int
+
+// List of all supported auth types.
+const (
+ authTypeUnknown authType = iota
+ authTypeAnonymous
+ authTypePresigned
+ authTypePresignedV2
+ authTypePostPolicy
+ authTypeStreamingSigned
+ authTypeSigned
+ authTypeSignedV2
+ authTypeJWT
+ authTypeSTS
+ authTypeStreamingSignedTrailer
+ authTypeStreamingUnsignedTrailer
+)
+
+// Get request authentication type.
+func getRequestAuthType(r *http.Request) (at authType) {
+ if r.URL != nil {
+ var err error
+ r.Form, err = url.ParseQuery(r.URL.RawQuery)
+ if err != nil {
+ authNLogIf(r.Context(), err)
+ return authTypeUnknown
+ }
+ }
+ if isRequestSignatureV2(r) {
+ return authTypeSignedV2
+ } else if isRequestPresignedSignatureV2(r) {
+ return authTypePresignedV2
+ } else if isRequestSignStreamingV4(r) {
+ return authTypeStreamingSigned
+ } else if isRequestSignStreamingTrailerV4(r) {
+ return authTypeStreamingSignedTrailer
+ } else if isRequestUnsignedTrailerV4(r) {
+ return authTypeStreamingUnsignedTrailer
+ } else if isRequestSignatureV4(r) {
+ return authTypeSigned
+ } else if isRequestPresignedSignatureV4(r) {
+ return authTypePresigned
+ } else if isRequestJWT(r) {
+ return authTypeJWT
+ } else if isRequestPostPolicySignatureV4(r) {
+ return authTypePostPolicy
+ } else if _, ok := r.Form[xhttp.Action]; ok {
+ return authTypeSTS
+ } else if _, ok := r.Header[xhttp.Authorization]; !ok {
+ return authTypeAnonymous
+ }
+ return authTypeUnknown
+}
+
+func validateAdminSignature(ctx context.Context, r *http.Request, region string) (auth.Credentials, bool, APIErrorCode) {
+ var cred auth.Credentials
+ var owner bool
+ s3Err := ErrAccessDenied
+ if _, ok := r.Header[xhttp.AmzContentSha256]; ok &&
+ getRequestAuthType(r) == authTypeSigned {
+ // Get credential information from the request.
+ cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
+ if s3Err != ErrNone {
+ return cred, owner, s3Err
+ }
+
+ // we only support V4 (no presign) with auth body
+ s3Err = isReqAuthenticated(ctx, r, region, serviceS3)
+ }
+ if s3Err != ErrNone {
+ return cred, owner, s3Err
+ }
+
+ logger.GetReqInfo(ctx).Cred = cred
+ logger.GetReqInfo(ctx).Owner = owner
+ logger.GetReqInfo(ctx).Region = globalSite.Region()
+
+ return cred, owner, ErrNone
+}
+
+// checkAdminRequestAuth checks for authentication and authorization for the incoming
+// request. It only accepts V2 and V4 requests. Presigned, JWT and anonymous requests
+// are automatically rejected.
+func checkAdminRequestAuth(ctx context.Context, r *http.Request, action policy.AdminAction, region string) (auth.Credentials, APIErrorCode) {
+ cred, owner, s3Err := validateAdminSignature(ctx, r, region)
+ if s3Err != ErrNone {
+ return cred, s3Err
+ }
+ if globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.Action(action),
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ Claims: cred.Claims,
+ }) {
+ // Request is allowed return the appropriate access key.
+ return cred, ErrNone
+ }
+
+ return cred, ErrAccessDenied
+}
+
+// Fetch the security token set by the client.
+func getSessionToken(r *http.Request) (token string) {
+ token = r.Header.Get(xhttp.AmzSecurityToken)
+ if token != "" {
+ return token
+ }
+ return r.Form.Get(xhttp.AmzSecurityToken)
+}
+
+// Fetch claims in the security token returned by the client, doesn't return
+// errors - upon errors the returned claims map will be empty.
+func mustGetClaimsFromToken(r *http.Request) map[string]any {
+ claims, _ := getClaimsFromToken(getSessionToken(r))
+ return claims
+}
+
+func getClaimsFromTokenWithSecret(token, secret string) (*xjwt.MapClaims, error) {
+ // JWT token for x-amz-security-token is signed with admin
+ // secret key, temporary credentials become invalid if
+ // server admin credentials change. This is done to ensure
+ // that clients cannot decode the token using the temp
+ // secret keys and generate an entirely new claim by essentially
+ // hijacking the policies. We need to make sure that this is
+ // based on admin credential such that token cannot be decoded
+ // on the client side and is treated like an opaque value.
+ claims, err := auth.ExtractClaims(token, secret)
+ if err != nil {
+ if subtle.ConstantTimeCompare([]byte(secret), []byte(globalActiveCred.SecretKey)) == 1 {
+ return nil, errAuthentication
+ }
+ claims, err = auth.ExtractClaims(token, globalActiveCred.SecretKey)
+ if err != nil {
+ return nil, errAuthentication
+ }
+ }
+
+ // If AuthZPlugin is set, return without any further checks.
+ if newGlobalAuthZPluginFn() != nil {
+ return claims, nil
+ }
+
+ // Check if a session policy is set. If so, decode it here.
+ sp, spok := claims.Lookup(policy.SessionPolicyName)
+ if spok {
+ // Looks like subpolicy is set and is a string, if set then its
+ // base64 encoded, decode it. Decoding fails reject such
+ // requests.
+ spBytes, err := base64.StdEncoding.DecodeString(sp)
+ if err != nil {
+ // Base64 decoding fails, we should log to indicate
+ // something is malforming the request sent by client.
+ authNLogIf(GlobalContext, err, logger.ErrorKind)
+ return nil, errAuthentication
+ }
+ claims.MapClaims[sessionPolicyNameExtracted] = string(spBytes)
+ }
+
+ return claims, nil
+}
+
+// Fetch claims in the security token returned by the client.
+func getClaimsFromToken(token string) (map[string]any, error) {
+ jwtClaims, err := getClaimsFromTokenWithSecret(token, globalActiveCred.SecretKey)
+ if err != nil {
+ return nil, err
+ }
+ return jwtClaims.Map(), nil
+}
+
+// Fetch claims in the security token returned by the client and validate the token.
+func checkClaimsFromToken(r *http.Request, cred auth.Credentials) (map[string]any, APIErrorCode) {
+ token := getSessionToken(r)
+ if token != "" && cred.AccessKey == "" {
+ // x-amz-security-token is not allowed for anonymous access.
+ return nil, ErrNoAccessKey
+ }
+
+ if token == "" && cred.IsTemp() && !cred.IsServiceAccount() {
+ // Temporary credentials should always have x-amz-security-token
+ return nil, ErrInvalidToken
+ }
+
+ if token != "" && !cred.IsTemp() {
+ // x-amz-security-token should not present for static credentials.
+ return nil, ErrInvalidToken
+ }
+
+ if !cred.IsServiceAccount() && cred.IsTemp() && subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
+ // validate token for temporary credentials only.
+ return nil, ErrInvalidToken
+ }
+
+ // Expired credentials must return error right away.
+ if cred.IsTemp() && cred.IsExpired() {
+ return nil, toAPIErrorCode(r.Context(), errInvalidAccessKeyID)
+ }
+ secret := globalActiveCred.SecretKey
+ if globalSiteReplicationSys.isEnabled() && cred.AccessKey != siteReplicatorSvcAcc {
+ nsecret, err := getTokenSigningKey()
+ if err != nil {
+ return nil, toAPIErrorCode(r.Context(), err)
+ }
+ // sign root's temporary accounts also with site replicator creds
+ if cred.ParentUser != globalActiveCred.AccessKey || cred.IsTemp() {
+ secret = nsecret
+ }
+ }
+ if cred.IsServiceAccount() {
+ token = cred.SessionToken
+ secret = cred.SecretKey
+ }
+
+ if token != "" {
+ claims, err := getClaimsFromTokenWithSecret(token, secret)
+ if err != nil {
+ return nil, toAPIErrorCode(r.Context(), err)
+ }
+ return claims.Map(), ErrNone
+ }
+
+ claims := xjwt.NewMapClaims()
+ return claims.Map(), ErrNone
+}
+
+// Check request auth type verifies the incoming http request
+// - validates the request signature
+// - validates the policy action if anonymous tests bucket policies if any,
+// for authenticated requests validates IAM policies.
+//
+// returns APIErrorCode if any to be replied to the client.
+func checkRequestAuthType(ctx context.Context, r *http.Request, action policy.Action, bucketName, objectName string) (s3Err APIErrorCode) {
+ logger.GetReqInfo(ctx).BucketName = bucketName
+ logger.GetReqInfo(ctx).ObjectName = objectName
+
+ _, _, s3Err = checkRequestAuthTypeCredential(ctx, r, action)
+ return s3Err
+}
+
+// checkRequestAuthTypeWithVID is similar to checkRequestAuthType
+// passes versionID additionally.
+func checkRequestAuthTypeWithVID(ctx context.Context, r *http.Request, action policy.Action, bucketName, objectName, versionID string) (s3Err APIErrorCode) {
+ logger.GetReqInfo(ctx).BucketName = bucketName
+ logger.GetReqInfo(ctx).ObjectName = objectName
+ logger.GetReqInfo(ctx).VersionID = versionID
+
+ _, _, s3Err = checkRequestAuthTypeCredential(ctx, r, action)
+ return s3Err
+}
+
+func authenticateRequest(ctx context.Context, r *http.Request, action policy.Action) (s3Err APIErrorCode) {
+ if logger.GetReqInfo(ctx) == nil {
+ bugLogIf(ctx, errors.New("unexpected context.Context does not have a logger.ReqInfo"), logger.ErrorKind)
+ return ErrAccessDenied
+ }
+
+ var cred auth.Credentials
+ var owner bool
+ switch getRequestAuthType(r) {
+ case authTypeUnknown, authTypeStreamingSigned, authTypeStreamingSignedTrailer, authTypeStreamingUnsignedTrailer:
+ return ErrSignatureVersionNotSupported
+ case authTypePresignedV2, authTypeSignedV2:
+ if s3Err = isReqAuthenticatedV2(r); s3Err != ErrNone {
+ return s3Err
+ }
+ cred, owner, s3Err = getReqAccessKeyV2(r)
+ case authTypeSigned, authTypePresigned:
+ region := globalSite.Region()
+ switch action {
+ case policy.GetBucketLocationAction, policy.ListAllMyBucketsAction:
+ region = ""
+ }
+ if s3Err = isReqAuthenticated(ctx, r, region, serviceS3); s3Err != ErrNone {
+ return s3Err
+ }
+ cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
+ }
+ if s3Err != ErrNone {
+ return s3Err
+ }
+
+ logger.GetReqInfo(ctx).Cred = cred
+ logger.GetReqInfo(ctx).Owner = owner
+ logger.GetReqInfo(ctx).Region = globalSite.Region()
+
+ // region is valid only for CreateBucketAction.
+ var region string
+ if action == policy.CreateBucketAction {
+ // To extract region from XML in request body, get copy of request body.
+ payload, err := io.ReadAll(io.LimitReader(r.Body, maxLocationConstraintSize))
+ if err != nil {
+ authZLogIf(ctx, err, logger.ErrorKind)
+ return ErrMalformedXML
+ }
+
+ // Populate payload to extract location constraint.
+ r.Body = io.NopCloser(bytes.NewReader(payload))
+ region, s3Err = parseLocationConstraint(r)
+ if s3Err != ErrNone {
+ return s3Err
+ }
+
+ // Populate payload again to handle it in HTTP handler.
+ r.Body = io.NopCloser(bytes.NewReader(payload))
+ }
+
+ logger.GetReqInfo(ctx).Region = region
+
+ return s3Err
+}
+
+func authorizeRequest(ctx context.Context, r *http.Request, action policy.Action) (s3Err APIErrorCode) {
+ reqInfo := logger.GetReqInfo(ctx)
+ if reqInfo == nil {
+ return ErrAccessDenied
+ }
+
+ cred := reqInfo.Cred
+ owner := reqInfo.Owner
+ region := reqInfo.Region
+ bucket := reqInfo.BucketName
+ object := reqInfo.ObjectName
+ versionID := reqInfo.VersionID
+
+ if action != policy.ListAllMyBucketsAction && cred.AccessKey == "" {
+ // Anonymous checks are not meant for ListAllBuckets action
+ if globalPolicySys.IsAllowed(policy.BucketPolicyArgs{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: action,
+ BucketName: bucket,
+ ConditionValues: getConditionValues(r, region, auth.AnonymousCredentials),
+ IsOwner: false,
+ ObjectName: object,
+ }) {
+ // Request is allowed return the appropriate access key.
+ return ErrNone
+ }
+
+ if action == policy.ListBucketVersionsAction {
+ // In AWS S3 s3:ListBucket permission is same as s3:ListBucketVersions permission
+ // verify as a fallback.
+ if globalPolicySys.IsAllowed(policy.BucketPolicyArgs{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.ListBucketAction,
+ BucketName: bucket,
+ ConditionValues: getConditionValues(r, region, auth.AnonymousCredentials),
+ IsOwner: false,
+ ObjectName: object,
+ }) {
+ // Request is allowed return the appropriate access key.
+ return ErrNone
+ }
+ }
+
+ return ErrAccessDenied
+ }
+ if action == policy.DeleteObjectAction && versionID != "" {
+ if !globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.Action(policy.DeleteObjectVersionAction),
+ BucketName: bucket,
+ ConditionValues: getConditionValues(r, "", cred),
+ ObjectName: object,
+ IsOwner: owner,
+ Claims: cred.Claims,
+ DenyOnly: true,
+ }) { // Request is not allowed if Deny action on DeleteObjectVersionAction
+ return ErrAccessDenied
+ }
+ }
+ if globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: action,
+ BucketName: bucket,
+ ConditionValues: getConditionValues(r, "", cred),
+ ObjectName: object,
+ IsOwner: owner,
+ Claims: cred.Claims,
+ }) {
+ // Request is allowed return the appropriate access key.
+ return ErrNone
+ }
+
+ if action == policy.ListBucketVersionsAction {
+ // In AWS S3 s3:ListBucket permission is same as s3:ListBucketVersions permission
+ // verify as a fallback.
+ if globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.ListBucketAction,
+ BucketName: bucket,
+ ConditionValues: getConditionValues(r, "", cred),
+ ObjectName: object,
+ IsOwner: owner,
+ Claims: cred.Claims,
+ }) {
+ // Request is allowed return the appropriate access key.
+ return ErrNone
+ }
+ }
+
+ return ErrAccessDenied
+}
+
+// Check request auth type verifies the incoming http request
+// - validates the request signature
+// - validates the policy action if anonymous tests bucket policies if any,
+// for authenticated requests validates IAM policies.
+//
+// returns APIErrorCode if any to be replied to the client.
+// Additionally returns the accessKey used in the request, and if this request is by an admin.
+func checkRequestAuthTypeCredential(ctx context.Context, r *http.Request, action policy.Action) (cred auth.Credentials, owner bool, s3Err APIErrorCode) {
+ s3Err = authenticateRequest(ctx, r, action)
+ reqInfo := logger.GetReqInfo(ctx)
+ if reqInfo == nil {
+ return cred, owner, ErrAccessDenied
+ }
+
+ cred = reqInfo.Cred
+ owner = reqInfo.Owner
+ if s3Err != ErrNone {
+ return cred, owner, s3Err
+ }
+
+ return cred, owner, authorizeRequest(ctx, r, action)
+}
+
+// Verify if request has valid AWS Signature Version '2'.
+func isReqAuthenticatedV2(r *http.Request) (s3Error APIErrorCode) {
+ if isRequestSignatureV2(r) {
+ return doesSignV2Match(r)
+ }
+ return doesPresignV2SignatureMatch(r)
+}
+
+func reqSignatureV4Verify(r *http.Request, region string, stype serviceType) (s3Error APIErrorCode) {
+ sha256sum := getContentSha256Cksum(r, stype)
+ switch {
+ case isRequestSignatureV4(r):
+ return doesSignatureMatch(sha256sum, r, region, stype)
+ case isRequestPresignedSignatureV4(r):
+ return doesPresignedSignatureMatch(sha256sum, r, region, stype)
+ default:
+ return ErrAccessDenied
+ }
+}
+
+// Verify if request has valid AWS Signature Version '4'.
+func isReqAuthenticated(ctx context.Context, r *http.Request, region string, stype serviceType) (s3Error APIErrorCode) {
+ if errCode := reqSignatureV4Verify(r, region, stype); errCode != ErrNone {
+ return errCode
+ }
+
+ clientETag, err := etag.FromContentMD5(r.Header)
+ if err != nil {
+ return ErrInvalidDigest
+ }
+
+ // Extract either 'X-Amz-Content-Sha256' header or 'X-Amz-Content-Sha256' query parameter (if V4 presigned)
+ // Do not verify 'X-Amz-Content-Sha256' if skipSHA256.
+ var contentSHA256 []byte
+ if skipSHA256 := skipContentSha256Cksum(r); !skipSHA256 && isRequestPresignedSignatureV4(r) {
+ if sha256Sum, ok := r.Form[xhttp.AmzContentSha256]; ok && len(sha256Sum) > 0 {
+ contentSHA256, err = hex.DecodeString(sha256Sum[0])
+ if err != nil {
+ return ErrContentSHA256Mismatch
+ }
+ }
+ } else if _, ok := r.Header[xhttp.AmzContentSha256]; !skipSHA256 && ok {
+ contentSHA256, err = hex.DecodeString(r.Header.Get(xhttp.AmzContentSha256))
+ if err != nil || len(contentSHA256) == 0 {
+ return ErrContentSHA256Mismatch
+ }
+ }
+
+ // Verify 'Content-Md5' and/or 'X-Amz-Content-Sha256' if present.
+ // The verification happens implicit during reading.
+ reader, err := hash.NewReader(ctx, r.Body, -1, clientETag.String(), hex.EncodeToString(contentSHA256), -1)
+ if err != nil {
+ return toAPIErrorCode(ctx, err)
+ }
+ r.Body = reader
+ return ErrNone
+}
+
+// List of all support S3 auth types.
+var supportedS3AuthTypes = map[authType]struct{}{
+ authTypeAnonymous: {},
+ authTypePresigned: {},
+ authTypePresignedV2: {},
+ authTypeSigned: {},
+ authTypeSignedV2: {},
+ authTypePostPolicy: {},
+ authTypeStreamingSigned: {},
+ authTypeStreamingSignedTrailer: {},
+ authTypeStreamingUnsignedTrailer: {},
+}
+
+// Validate if the authType is valid and supported.
+func isSupportedS3AuthType(aType authType) bool {
+ _, ok := supportedS3AuthTypes[aType]
+ return ok
+}
+
+// setAuthMiddleware to validate authorization header for the incoming request.
+func setAuthMiddleware(h http.Handler) http.Handler {
+ // handler for validating incoming authorization headers.
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
+
+ aType := getRequestAuthType(r)
+ switch aType {
+ case authTypeSigned, authTypeSignedV2, authTypeStreamingSigned, authTypeStreamingSignedTrailer:
+ // Verify if date headers are set, if not reject the request
+ amzDate, errCode := parseAmzDateHeader(r)
+ if errCode != ErrNone {
+ if ok {
+ tc.FuncName = "handler.Auth"
+ tc.ResponseRecorder.LogErrBody = true
+ }
+
+ // All our internal APIs are sensitive towards Date
+ // header, for all requests where Date header is not
+ // present we will reject such clients.
+ defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
+ writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(errCode), r.URL)
+ atomic.AddUint64(&globalHTTPStats.rejectedRequestsTime, 1)
+ return
+ }
+ // Verify if the request date header is shifted by less than globalMaxSkewTime parameter in the past
+ // or in the future, reject request otherwise.
+ curTime := UTCNow()
+ if curTime.Sub(amzDate) > globalMaxSkewTime || amzDate.Sub(curTime) > globalMaxSkewTime {
+ if ok {
+ tc.FuncName = "handler.Auth"
+ tc.ResponseRecorder.LogErrBody = true
+ }
+
+ defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
+ writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrRequestTimeTooSkewed), r.URL)
+ atomic.AddUint64(&globalHTTPStats.rejectedRequestsTime, 1)
+ return
+ }
+ h.ServeHTTP(w, r)
+ return
+ case authTypeJWT, authTypeSTS:
+ h.ServeHTTP(w, r)
+ return
+ default:
+ if isSupportedS3AuthType(aType) {
+ h.ServeHTTP(w, r)
+ return
+ }
+ }
+
+ if ok {
+ tc.FuncName = "handler.Auth"
+ tc.ResponseRecorder.LogErrBody = true
+ }
+
+ defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
+ writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrSignatureVersionNotSupported), r.URL)
+ atomic.AddUint64(&globalHTTPStats.rejectedRequestsAuth, 1)
+ })
+}
+
+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
+ if cred.AccessKey == "" {
+ return ErrAccessDenied
+ }
+
+ conditions := getConditionValues(r, "", cred)
+ conditions["object-lock-mode"] = []string{string(retMode)}
+ conditions["object-lock-retain-until-date"] = []string{retDate.UTC().Format(time.RFC3339)}
+ if retDays > 0 {
+ conditions["object-lock-remaining-retention-days"] = []string{strconv.Itoa(retDays)}
+ }
+ if retMode == objectlock.RetGovernance && byPassSet {
+ byPassSet = globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.BypassGovernanceRetentionAction,
+ BucketName: bucketName,
+ ObjectName: objectName,
+ ConditionValues: conditions,
+ IsOwner: owner,
+ Claims: cred.Claims,
+ })
+ }
+ if globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.PutObjectRetentionAction,
+ BucketName: bucketName,
+ ConditionValues: conditions,
+ ObjectName: objectName,
+ IsOwner: owner,
+ Claims: cred.Claims,
+ }) {
+ retSet = true
+ }
+ if byPassSet || retSet {
+ return ErrNone
+ }
+ return ErrAccessDenied
+}
+
+// isPutActionAllowed - check if PUT operation is allowed on the resource, this
+// call verifies bucket policies and IAM policies, supports multi user
+// checks etc.
+func isPutActionAllowed(ctx context.Context, atype authType, bucketName, objectName string, r *http.Request, action policy.Action) (s3Err APIErrorCode) {
+ var cred auth.Credentials
+ var owner bool
+ region := globalSite.Region()
+ switch atype {
+ case authTypeUnknown:
+ return ErrSignatureVersionNotSupported
+ case authTypeSignedV2, authTypePresignedV2:
+ cred, owner, s3Err = getReqAccessKeyV2(r)
+ case authTypeStreamingSigned, authTypePresigned, authTypeSigned, authTypeStreamingSignedTrailer:
+ 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 {
+ return s3Err
+ }
+
+ logger.GetReqInfo(ctx).Cred = cred
+ logger.GetReqInfo(ctx).Owner = owner
+ logger.GetReqInfo(ctx).Region = region
+
+ // Do not check for PutObjectRetentionAction permission,
+ // if mode and retain until date are not set.
+ // Can happen when bucket has default lock config set
+ if action == policy.PutObjectRetentionAction &&
+ r.Header.Get(xhttp.AmzObjectLockMode) == "" &&
+ r.Header.Get(xhttp.AmzObjectLockRetainUntilDate) == "" {
+ return ErrNone
+ }
+
+ if cred.AccessKey == "" {
+ if globalPolicySys.IsAllowed(policy.BucketPolicyArgs{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: action,
+ BucketName: bucketName,
+ ConditionValues: getConditionValues(r, "", auth.AnonymousCredentials),
+ IsOwner: false,
+ ObjectName: objectName,
+ }) {
+ return ErrNone
+ }
+ return ErrAccessDenied
+ }
+
+ if globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: action,
+ BucketName: bucketName,
+ ConditionValues: getConditionValues(r, "", cred),
+ ObjectName: objectName,
+ IsOwner: owner,
+ Claims: cred.Claims,
+ }) {
+ return ErrNone
+ }
+ return ErrAccessDenied
+}
diff --git a/cmd/auth-handler_test.go b/cmd/auth-handler_test.go
new file mode 100644
index 00000000..47f12add
--- /dev/null
+++ b/cmd/auth-handler_test.go
@@ -0,0 +1,501 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/minio/minio/internal/auth"
+ "github.com/minio/pkg/v3/policy"
+)
+
+type nullReader struct{}
+
+func (r *nullReader) Read(b []byte) (int, error) {
+ return len(b), nil
+}
+
+// Test get request auth type.
+func TestGetRequestAuthType(t *testing.T) {
+ type testCase struct {
+ req *http.Request
+ authT authType
+ }
+ nopCloser := io.NopCloser(io.LimitReader(&nullReader{}, 1024))
+ testCases := []testCase{
+ // Test case - 1
+ // Check for generic signature v4 header.
+ {
+ req: &http.Request{
+ URL: &url.URL{
+ Host: "127.0.0.1:9000",
+ Scheme: httpScheme,
+ Path: SlashSeparator,
+ },
+ Header: http.Header{
+ "Authorization": []string{"AWS4-HMAC-SHA256 "},
+ "X-Amz-Content-Sha256": []string{streamingContentSHA256},
+ "Content-Encoding": []string{streamingContentEncoding},
+ },
+ Method: http.MethodPut,
+ Body: nopCloser,
+ },
+ authT: authTypeStreamingSigned,
+ },
+ // Test case - 2
+ // Check for JWT header.
+ {
+ req: &http.Request{
+ URL: &url.URL{
+ Host: "127.0.0.1:9000",
+ Scheme: httpScheme,
+ Path: SlashSeparator,
+ },
+ Header: http.Header{
+ "Authorization": []string{"Bearer 12313123"},
+ },
+ },
+ authT: authTypeJWT,
+ },
+ // Test case - 3
+ // Empty authorization header.
+ {
+ req: &http.Request{
+ URL: &url.URL{
+ Host: "127.0.0.1:9000",
+ Scheme: httpScheme,
+ Path: SlashSeparator,
+ },
+ Header: http.Header{
+ "Authorization": []string{""},
+ },
+ },
+ authT: authTypeUnknown,
+ },
+ // Test case - 4
+ // Check for presigned.
+ {
+ req: &http.Request{
+ URL: &url.URL{
+ Host: "127.0.0.1:9000",
+ Scheme: httpScheme,
+ Path: SlashSeparator,
+ RawQuery: "X-Amz-Credential=EXAMPLEINVALIDEXAMPL%2Fs3%2F20160314%2Fus-east-1",
+ },
+ },
+ authT: authTypePresigned,
+ },
+ // Test case - 5
+ // Check for post policy.
+ {
+ req: &http.Request{
+ URL: &url.URL{
+ Host: "127.0.0.1:9000",
+ Scheme: httpScheme,
+ Path: SlashSeparator,
+ },
+ Header: http.Header{
+ "Content-Type": []string{"multipart/form-data"},
+ },
+ Method: http.MethodPost,
+ Body: nopCloser,
+ },
+ authT: authTypePostPolicy,
+ },
+ }
+
+ // .. Tests all request auth type.
+ for i, testc := range testCases {
+ authT := getRequestAuthType(testc.req)
+ if authT != testc.authT {
+ t.Errorf("Test %d: Expected %d, got %d", i+1, testc.authT, authT)
+ }
+ }
+}
+
+// Test all s3 supported auth types.
+func TestS3SupportedAuthType(t *testing.T) {
+ type testCase struct {
+ authT authType
+ pass bool
+ }
+ // List of all valid and invalid test cases.
+ testCases := []testCase{
+ // Test 1 - supported s3 type anonymous.
+ {
+ authT: authTypeAnonymous,
+ pass: true,
+ },
+ // Test 2 - supported s3 type presigned.
+ {
+ authT: authTypePresigned,
+ pass: true,
+ },
+ // Test 3 - supported s3 type signed.
+ {
+ authT: authTypeSigned,
+ pass: true,
+ },
+ // Test 4 - supported s3 type with post policy.
+ {
+ authT: authTypePostPolicy,
+ pass: true,
+ },
+ // Test 5 - supported s3 type with streaming signed.
+ {
+ authT: authTypeStreamingSigned,
+ pass: true,
+ },
+ // Test 6 - supported s3 type with signature v2.
+ {
+ authT: authTypeSignedV2,
+ pass: true,
+ },
+ // Test 7 - supported s3 type with presign v2.
+ {
+ authT: authTypePresignedV2,
+ pass: true,
+ },
+ // Test 8 - JWT is not supported s3 type.
+ {
+ authT: authTypeJWT,
+ pass: false,
+ },
+ // Test 9 - unknown auth header is not supported s3 type.
+ {
+ authT: authTypeUnknown,
+ pass: false,
+ },
+ // Test 10 - some new auth type is not supported s3 type.
+ {
+ authT: authType(9),
+ pass: false,
+ },
+ }
+ // Validate all the test cases.
+ for i, tt := range testCases {
+ ok := isSupportedS3AuthType(tt.authT)
+ if ok != tt.pass {
+ t.Errorf("Test %d:, Expected %t, got %t", i+1, tt.pass, ok)
+ }
+ }
+}
+
+func TestIsRequestPresignedSignatureV2(t *testing.T) {
+ testCases := []struct {
+ inputQueryKey string
+ inputQueryValue string
+ expectedResult bool
+ }{
+ // Test case - 1.
+ // Test case with query key "AWSAccessKeyId" set.
+ {"", "", false},
+ // Test case - 2.
+ {"AWSAccessKeyId", "", true},
+ // Test case - 3.
+ {"X-Amz-Content-Sha256", "", false},
+ }
+
+ for i, testCase := range testCases {
+ // creating an input HTTP request.
+ // Only the query parameters are relevant for this particular test.
+ inputReq, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
+ if err != nil {
+ t.Fatalf("Error initializing input HTTP request: %v", err)
+ }
+ q := inputReq.URL.Query()
+ q.Add(testCase.inputQueryKey, testCase.inputQueryValue)
+ inputReq.URL.RawQuery = q.Encode()
+ inputReq.ParseForm()
+
+ actualResult := isRequestPresignedSignatureV2(inputReq)
+ if testCase.expectedResult != actualResult {
+ t.Errorf("Test %d: Expected the result to `%v`, but instead got `%v`", i+1, testCase.expectedResult, actualResult)
+ }
+ }
+}
+
+// TestIsRequestPresignedSignatureV4 - Test validates the logic for presign signature version v4 detection.
+func TestIsRequestPresignedSignatureV4(t *testing.T) {
+ testCases := []struct {
+ inputQueryKey string
+ inputQueryValue string
+ expectedResult bool
+ }{
+ // Test case - 1.
+ // Test case with query key ""X-Amz-Credential" set.
+ {"", "", false},
+ // Test case - 2.
+ {"X-Amz-Credential", "", true},
+ // Test case - 3.
+ {"X-Amz-Content-Sha256", "", false},
+ }
+
+ for i, testCase := range testCases {
+ // creating an input HTTP request.
+ // Only the query parameters are relevant for this particular test.
+ inputReq, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
+ if err != nil {
+ t.Fatalf("Error initializing input HTTP request: %v", err)
+ }
+ q := inputReq.URL.Query()
+ q.Add(testCase.inputQueryKey, testCase.inputQueryValue)
+ inputReq.URL.RawQuery = q.Encode()
+ inputReq.ParseForm()
+
+ actualResult := isRequestPresignedSignatureV4(inputReq)
+ if testCase.expectedResult != actualResult {
+ t.Errorf("Test %d: Expected the result to `%v`, but instead got `%v`", i+1, testCase.expectedResult, actualResult)
+ }
+ }
+}
+
+// Provides a fully populated http request instance, fails otherwise.
+func mustNewRequest(method string, urlStr string, contentLength int64, body io.ReadSeeker, t *testing.T) *http.Request {
+ req, err := newTestRequest(method, urlStr, contentLength, body)
+ if err != nil {
+ t.Fatalf("Unable to initialize new http request %s", err)
+ }
+ return req
+}
+
+// This is similar to mustNewRequest but additionally the request
+// is signed with AWS Signature V4, fails if not able to do so.
+func mustNewSignedRequest(method string, urlStr string, contentLength int64, body io.ReadSeeker, t *testing.T) *http.Request {
+ req := mustNewRequest(method, urlStr, contentLength, body, t)
+ cred := globalActiveCred
+ if err := signRequestV4(req, cred.AccessKey, cred.SecretKey); err != nil {
+ t.Fatalf("Unable to initialized new signed http request %s", err)
+ }
+ return req
+}
+
+// This is similar to mustNewRequest but additionally the request
+// is signed with AWS Signature V2, fails if not able to do so.
+func mustNewSignedV2Request(method string, urlStr string, contentLength int64, body io.ReadSeeker, t *testing.T) *http.Request {
+ req := mustNewRequest(method, urlStr, contentLength, body, t)
+ cred := globalActiveCred
+ if err := signRequestV2(req, cred.AccessKey, cred.SecretKey); err != nil {
+ t.Fatalf("Unable to initialized new signed http request %s", err)
+ }
+ return req
+}
+
+// This is similar to mustNewRequest but additionally the request
+// is presigned with AWS Signature V2, fails if not able to do so.
+func mustNewPresignedV2Request(method string, urlStr string, contentLength int64, body io.ReadSeeker, t *testing.T) *http.Request {
+ req := mustNewRequest(method, urlStr, contentLength, body, t)
+ cred := globalActiveCred
+ if err := preSignV2(req, cred.AccessKey, cred.SecretKey, time.Now().Add(10*time.Minute).Unix()); err != nil {
+ t.Fatalf("Unable to initialized new signed http request %s", err)
+ }
+ return req
+}
+
+// This is similar to mustNewRequest but additionally the request
+// is presigned with AWS Signature V4, fails if not able to do so.
+func mustNewPresignedRequest(method string, urlStr string, contentLength int64, body io.ReadSeeker, t *testing.T) *http.Request {
+ req := mustNewRequest(method, urlStr, contentLength, body, t)
+ cred := globalActiveCred
+ if err := preSignV4(req, cred.AccessKey, cred.SecretKey, time.Now().Add(10*time.Minute).Unix()); err != nil {
+ t.Fatalf("Unable to initialized new signed http request %s", err)
+ }
+ return req
+}
+
+func mustNewSignedShortMD5Request(method string, urlStr string, contentLength int64, body io.ReadSeeker, t *testing.T) *http.Request {
+ req := mustNewRequest(method, urlStr, contentLength, body, t)
+ req.Header.Set("Content-Md5", "invalid-digest")
+ cred := globalActiveCred
+ if err := signRequestV4(req, cred.AccessKey, cred.SecretKey); err != nil {
+ t.Fatalf("Unable to initialized new signed http request %s", err)
+ }
+ return req
+}
+
+func mustNewSignedEmptyMD5Request(method string, urlStr string, contentLength int64, body io.ReadSeeker, t *testing.T) *http.Request {
+ req := mustNewRequest(method, urlStr, contentLength, body, t)
+ req.Header.Set("Content-Md5", "")
+ cred := globalActiveCred
+ if err := signRequestV4(req, cred.AccessKey, cred.SecretKey); err != nil {
+ t.Fatalf("Unable to initialized new signed http request %s", err)
+ }
+ return req
+}
+
+func mustNewSignedBadMD5Request(method string, urlStr string, contentLength int64,
+ body io.ReadSeeker, t *testing.T,
+) *http.Request {
+ req := mustNewRequest(method, urlStr, contentLength, body, t)
+ req.Header.Set("Content-Md5", "YWFhYWFhYWFhYWFhYWFhCg==")
+ cred := globalActiveCred
+ if err := signRequestV4(req, cred.AccessKey, cred.SecretKey); err != nil {
+ t.Fatalf("Unable to initialized new signed http request %s", err)
+ }
+ return req
+}
+
+// Tests is requested authenticated function, tests replies for s3 errors.
+func TestIsReqAuthenticated(t *testing.T) {
+ ctx, cancel := context.WithCancel(GlobalContext)
+ defer cancel()
+
+ objLayer, fsDir, err := prepareFS(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(fsDir)
+ if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
+ t.Fatalf("unable initialize config file, %s", err)
+ }
+
+ initAllSubsystems(ctx)
+
+ initConfigSubsystem(ctx, objLayer)
+
+ creds, err := auth.CreateCredentials("myuser", "mypassword")
+ if err != nil {
+ t.Fatalf("unable create credential, %s", err)
+ }
+
+ globalActiveCred = creds
+
+ globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
+
+ // List of test cases for validating http request authentication.
+ testCases := []struct {
+ req *http.Request
+ s3Error APIErrorCode
+ }{
+ // When request is unsigned, access denied is returned.
+ {mustNewRequest(http.MethodGet, "http://127.0.0.1:9000", 0, nil, t), ErrAccessDenied},
+ // Empty Content-Md5 header.
+ {mustNewSignedEmptyMD5Request(http.MethodPut, "http://127.0.0.1:9000/", 5, bytes.NewReader([]byte("hello")), t), ErrInvalidDigest},
+ // Short Content-Md5 header.
+ {mustNewSignedShortMD5Request(http.MethodPut, "http://127.0.0.1:9000/", 5, bytes.NewReader([]byte("hello")), t), ErrInvalidDigest},
+ // When request is properly signed, but has bad Content-MD5 header.
+ {mustNewSignedBadMD5Request(http.MethodPut, "http://127.0.0.1:9000/", 5, bytes.NewReader([]byte("hello")), t), ErrBadDigest},
+ // When request is properly signed, error is none.
+ {mustNewSignedRequest(http.MethodGet, "http://127.0.0.1:9000", 0, nil, t), ErrNone},
+ }
+
+ // Validates all testcases.
+ for i, testCase := range testCases {
+ s3Error := isReqAuthenticated(ctx, testCase.req, globalSite.Region(), serviceS3)
+ if s3Error != testCase.s3Error {
+ if _, err := io.ReadAll(testCase.req.Body); toAPIErrorCode(ctx, err) != testCase.s3Error {
+ t.Fatalf("Test %d: Unexpected S3 error: want %d - got %d (got after reading request %s)", i, testCase.s3Error, s3Error, toAPIError(ctx, err).Code)
+ }
+ }
+ }
+}
+
+func TestCheckAdminRequestAuthType(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ objLayer, fsDir, err := prepareFS(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(fsDir)
+
+ if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
+ t.Fatalf("unable initialize config file, %s", err)
+ }
+
+ creds, err := auth.CreateCredentials("myuser", "mypassword")
+ if err != nil {
+ t.Fatalf("unable create credential, %s", err)
+ }
+
+ globalActiveCred = creds
+ testCases := []struct {
+ Request *http.Request
+ ErrCode APIErrorCode
+ }{
+ {Request: mustNewRequest(http.MethodGet, "http://127.0.0.1:9000", 0, nil, t), ErrCode: ErrAccessDenied},
+ {Request: mustNewSignedRequest(http.MethodGet, "http://127.0.0.1:9000", 0, nil, t), ErrCode: ErrNone},
+ {Request: mustNewSignedV2Request(http.MethodGet, "http://127.0.0.1:9000", 0, nil, t), ErrCode: ErrAccessDenied},
+ {Request: mustNewPresignedV2Request(http.MethodGet, "http://127.0.0.1:9000", 0, nil, t), ErrCode: ErrAccessDenied},
+ {Request: mustNewPresignedRequest(http.MethodGet, "http://127.0.0.1:9000", 0, nil, t), ErrCode: ErrAccessDenied},
+ }
+ for i, testCase := range testCases {
+ if _, s3Error := checkAdminRequestAuth(ctx, testCase.Request, policy.AllAdminActions, globalSite.Region()); s3Error != testCase.ErrCode {
+ t.Errorf("Test %d: Unexpected s3error returned wanted %d, got %d", i, testCase.ErrCode, s3Error)
+ }
+ }
+}
+
+func TestValidateAdminSignature(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ objLayer, fsDir, err := prepareFS(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(fsDir)
+
+ if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
+ t.Fatalf("unable initialize config file, %s", err)
+ }
+
+ initAllSubsystems(ctx)
+
+ initConfigSubsystem(ctx, objLayer)
+
+ creds, err := auth.CreateCredentials("admin", "mypassword")
+ if err != nil {
+ t.Fatalf("unable create credential, %s", err)
+ }
+ globalActiveCred = creds
+
+ globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
+
+ testCases := []struct {
+ AccessKey string
+ SecretKey string
+ ErrCode APIErrorCode
+ }{
+ {"", "", ErrInvalidAccessKeyID},
+ {"admin", "", ErrSignatureDoesNotMatch},
+ {"admin", "wrongpassword", ErrSignatureDoesNotMatch},
+ {"wronguser", "mypassword", ErrInvalidAccessKeyID},
+ {"", "mypassword", ErrInvalidAccessKeyID},
+ {"admin", "mypassword", ErrNone},
+ }
+
+ for i, testCase := range testCases {
+ req := mustNewRequest(http.MethodGet, "http://localhost:9000/", 0, nil, t)
+ if err := signRequestV4(req, testCase.AccessKey, testCase.SecretKey); err != nil {
+ t.Fatalf("Unable to initialized new signed http request %s", err)
+ }
+ _, _, s3Error := validateAdminSignature(ctx, req, globalMinioDefaultRegion)
+ if s3Error != testCase.ErrCode {
+ t.Errorf("Test %d: Unexpected s3error returned wanted %d, got %d", i+1, testCase.ErrCode, s3Error)
+ }
+ }
+}
diff --git a/cmd/authtype_string.go b/cmd/authtype_string.go
new file mode 100644
index 00000000..0fea5481
--- /dev/null
+++ b/cmd/authtype_string.go
@@ -0,0 +1,34 @@
+// Code generated by "stringer -type=authType -trimprefix=authType auth-handler.go"; DO NOT EDIT.
+
+package cmd
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[authTypeUnknown-0]
+ _ = x[authTypeAnonymous-1]
+ _ = x[authTypePresigned-2]
+ _ = x[authTypePresignedV2-3]
+ _ = x[authTypePostPolicy-4]
+ _ = x[authTypeStreamingSigned-5]
+ _ = x[authTypeSigned-6]
+ _ = x[authTypeSignedV2-7]
+ _ = x[authTypeJWT-8]
+ _ = x[authTypeSTS-9]
+ _ = x[authTypeStreamingSignedTrailer-10]
+ _ = x[authTypeStreamingUnsignedTrailer-11]
+}
+
+const _authType_name = "UnknownAnonymousPresignedPresignedV2PostPolicyStreamingSignedSignedSignedV2JWTSTSStreamingSignedTrailerStreamingUnsignedTrailer"
+
+var _authType_index = [...]uint8{0, 7, 16, 25, 36, 46, 61, 67, 75, 78, 81, 103, 127}
+
+func (i authType) String() string {
+ if i < 0 || i >= authType(len(_authType_index)-1) {
+ return "authType(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _authType_name[_authType_index[i]:_authType_index[i+1]]
+}
diff --git a/cmd/background-heal-ops.go b/cmd/background-heal-ops.go
new file mode 100644
index 00000000..3eeff509
--- /dev/null
+++ b/cmd/background-heal-ops.go
@@ -0,0 +1,189 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "runtime"
+ "strconv"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/pkg/v3/env"
+)
+
+// healTask represents what to heal along with options
+//
+// path: '/' => Heal disk formats along with metadata
+// path: 'bucket/' or '/bucket/' => Heal bucket
+// path: 'bucket/object' => Heal object
+type healTask struct {
+ bucket string
+ object string
+ versionID string
+ opts madmin.HealOpts
+ // Healing response will be sent here
+ respCh chan healResult
+}
+
+// healResult represents a healing result with a possible error
+type healResult struct {
+ result madmin.HealResultItem
+ err error
+}
+
+// healRoutine receives heal tasks, to heal buckets, objects and format.json
+type healRoutine struct {
+ tasks chan healTask
+ workers int
+}
+
+func activeListeners() int {
+ // Bucket notification and http trace are not costly, it is okay to ignore them
+ // while counting the number of concurrent connections
+ return int(globalHTTPListen.Subscribers()) + int(globalTrace.Subscribers())
+}
+
+func waitForLowIO(maxIO int, maxWait time.Duration, currentIO func() int) {
+ // No need to wait run at full speed.
+ if maxIO <= 0 {
+ return
+ }
+
+ const waitTick = 100 * time.Millisecond
+
+ tmpMaxWait := maxWait
+
+ for currentIO() >= maxIO {
+ if tmpMaxWait > 0 {
+ if tmpMaxWait < waitTick {
+ time.Sleep(tmpMaxWait)
+ return
+ }
+ time.Sleep(waitTick)
+ tmpMaxWait -= waitTick
+ }
+ if tmpMaxWait <= 0 {
+ return
+ }
+ }
+}
+
+func currentHTTPIO() int {
+ httpServer := newHTTPServerFn()
+ if httpServer == nil {
+ return 0
+ }
+
+ return httpServer.GetRequestCount() - activeListeners()
+}
+
+func waitForLowHTTPReq() {
+ maxIO, maxWait, _ := globalHealConfig.Clone()
+ waitForLowIO(maxIO, maxWait, currentHTTPIO)
+}
+
+func initBackgroundHealing(ctx context.Context, objAPI ObjectLayer) {
+ bgSeq := newBgHealSequence()
+ // Run the background healer
+ for range globalBackgroundHealRoutine.workers {
+ go globalBackgroundHealRoutine.AddWorker(ctx, objAPI, bgSeq)
+ }
+
+ globalBackgroundHealState.LaunchNewHealSequence(bgSeq, objAPI)
+}
+
+// Wait for heal requests and process them
+func (h *healRoutine) AddWorker(ctx context.Context, objAPI ObjectLayer, bgSeq *healSequence) {
+ for {
+ select {
+ case task, ok := <-h.tasks:
+ if !ok {
+ return
+ }
+
+ var res madmin.HealResultItem
+ var err error
+ switch task.bucket {
+ case nopHeal:
+ err = errSkipFile
+ case SlashSeparator:
+ res, err = healDiskFormat(ctx, objAPI, task.opts)
+ default:
+ if task.object == "" {
+ res, err = objAPI.HealBucket(ctx, task.bucket, task.opts)
+ } else {
+ res, err = objAPI.HealObject(ctx, task.bucket, task.object, task.versionID, task.opts)
+ }
+ }
+
+ if task.respCh != nil {
+ task.respCh <- healResult{result: res, err: err}
+ continue
+ }
+
+ // when respCh is not set caller is not waiting but we
+ // update the relevant metrics for them
+ if bgSeq != nil {
+ if err == nil {
+ bgSeq.countHealed(res.Type)
+ } else {
+ bgSeq.countFailed(res.Type)
+ }
+ }
+ case <-ctx.Done():
+ return
+ }
+ }
+}
+
+func newHealRoutine() *healRoutine {
+ workers := runtime.GOMAXPROCS(0) / 2
+
+ if envHealWorkers := env.Get("_MINIO_HEAL_WORKERS", ""); envHealWorkers != "" {
+ if numHealers, err := strconv.Atoi(envHealWorkers); err != nil {
+ bugLogIf(context.Background(), fmt.Errorf("invalid _MINIO_HEAL_WORKERS value: %w", err))
+ } else {
+ workers = numHealers
+ }
+ }
+
+ if workers == 0 {
+ workers = 4
+ }
+
+ return &healRoutine{
+ tasks: make(chan healTask),
+ workers: workers,
+ }
+}
+
+// healDiskFormat - heals format.json, return value indicates if a
+// failure error occurred.
+func healDiskFormat(ctx context.Context, objAPI ObjectLayer, opts madmin.HealOpts) (madmin.HealResultItem, error) {
+ res, err := objAPI.HealFormat(ctx, opts.DryRun)
+
+ // return any error, ignore error returned when disks have
+ // already healed.
+ if err != nil && err != errNoHealRequired {
+ return madmin.HealResultItem{}, err
+ }
+
+ return res, nil
+}
diff --git a/cmd/background-newdisks-heal-ops.go b/cmd/background-newdisks-heal-ops.go
new file mode 100644
index 00000000..330bace4
--- /dev/null
+++ b/cmd/background-newdisks-heal-ops.go
@@ -0,0 +1,605 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "slices"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/dustin/go-humanize"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio-go/v7/pkg/set"
+ "github.com/minio/minio/internal/config"
+ "github.com/minio/pkg/v3/env"
+)
+
+const (
+ defaultMonitorNewDiskInterval = time.Second * 10
+ healingTrackerFilename = ".healing.bin"
+)
+
+//go:generate msgp -file $GOFILE -unexported
+
+// healingTracker is used to persist healing information during a heal.
+type healingTracker struct {
+ disk StorageAPI `msg:"-"`
+ mu *sync.RWMutex `msg:"-"`
+
+ ID string
+ PoolIndex int
+ SetIndex int
+ DiskIndex int
+ Path string
+ Endpoint string
+ Started time.Time
+ LastUpdate time.Time
+
+ ObjectsTotalCount uint64
+ ObjectsTotalSize uint64
+
+ ItemsHealed uint64
+ ItemsFailed uint64
+
+ BytesDone uint64
+ BytesFailed uint64
+
+ // Last object scanned.
+ Bucket string `json:"-"`
+ Object string `json:"-"`
+
+ // Numbers when current bucket started healing,
+ // for resuming with correct numbers.
+ ResumeItemsHealed uint64 `json:"-"`
+ ResumeItemsFailed uint64 `json:"-"`
+ ResumeItemsSkipped uint64 `json:"-"`
+ ResumeBytesDone uint64 `json:"-"`
+ ResumeBytesFailed uint64 `json:"-"`
+ ResumeBytesSkipped uint64 `json:"-"`
+
+ // Filled on startup/restarts.
+ QueuedBuckets []string
+
+ // Filled during heal.
+ HealedBuckets []string
+
+ // ID of the current healing operation
+ HealID string
+
+ ItemsSkipped uint64
+ BytesSkipped uint64
+
+ RetryAttempts uint64
+
+ Finished bool // finished healing, whether with errors or not
+
+ // Add future tracking capabilities
+ // Be sure that they are included in toHealingDisk
+}
+
+// loadHealingTracker will load the healing tracker from the supplied disk.
+// The disk ID will be validated against the loaded one.
+func loadHealingTracker(ctx context.Context, disk StorageAPI) (*healingTracker, error) {
+ if disk == nil {
+ return nil, errors.New("loadHealingTracker: nil drive given")
+ }
+ diskID, err := disk.GetDiskID()
+ if err != nil {
+ return nil, err
+ }
+ b, err := disk.ReadAll(ctx, minioMetaBucket,
+ pathJoin(bucketMetaPrefix, healingTrackerFilename))
+ if err != nil {
+ return nil, err
+ }
+ var h healingTracker
+ _, err = h.UnmarshalMsg(b)
+ if err != nil {
+ return nil, err
+ }
+ if h.ID != diskID && h.ID != "" {
+ return nil, fmt.Errorf("loadHealingTracker: drive id mismatch expected %s, got %s", h.ID, diskID)
+ }
+ h.disk = disk
+ h.ID = diskID
+ h.mu = &sync.RWMutex{}
+ return &h, nil
+}
+
+// newHealingTracker will create a new healing tracker for the disk.
+func newHealingTracker() *healingTracker {
+ return &healingTracker{
+ mu: &sync.RWMutex{},
+ }
+}
+
+func initHealingTracker(disk StorageAPI, healID string) *healingTracker {
+ h := newHealingTracker()
+ diskID, _ := disk.GetDiskID()
+ h.disk = disk
+ h.ID = diskID
+ h.HealID = healID
+ h.Path = disk.String()
+ h.Endpoint = disk.Endpoint().String()
+ h.Started = time.Now().UTC()
+ h.PoolIndex, h.SetIndex, h.DiskIndex = disk.GetDiskLoc()
+ return h
+}
+
+func (h *healingTracker) resetHealing() {
+ h.mu.Lock()
+ defer h.mu.Unlock()
+
+ h.ItemsHealed = 0
+ h.ItemsFailed = 0
+ h.BytesDone = 0
+ h.BytesFailed = 0
+ h.ResumeItemsHealed = 0
+ h.ResumeItemsFailed = 0
+ h.ResumeBytesDone = 0
+ h.ResumeBytesFailed = 0
+ h.ItemsSkipped = 0
+ h.BytesSkipped = 0
+
+ h.HealedBuckets = nil
+ h.Object = ""
+ h.Bucket = ""
+}
+
+func (h *healingTracker) getLastUpdate() time.Time {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+
+ return h.LastUpdate
+}
+
+func (h *healingTracker) getBucket() string {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+
+ return h.Bucket
+}
+
+func (h *healingTracker) setBucket(bucket string) {
+ h.mu.Lock()
+ defer h.mu.Unlock()
+
+ h.Bucket = bucket
+}
+
+func (h *healingTracker) getObject() string {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+
+ return h.Object
+}
+
+func (h *healingTracker) setObject(object string) {
+ h.mu.Lock()
+ defer h.mu.Unlock()
+
+ h.Object = object
+}
+
+func (h *healingTracker) updateProgress(success, skipped bool, bytes uint64) {
+ h.mu.Lock()
+ defer h.mu.Unlock()
+
+ switch {
+ case success:
+ h.ItemsHealed++
+ h.BytesDone += bytes
+ case skipped:
+ h.ItemsSkipped++
+ h.BytesSkipped += bytes
+ default:
+ h.ItemsFailed++
+ h.BytesFailed += bytes
+ }
+}
+
+// update will update the tracker on the disk.
+// If the tracker has been deleted an error is returned.
+func (h *healingTracker) update(ctx context.Context) error {
+ h.mu.Lock()
+ if h.ID == "" || h.PoolIndex < 0 || h.SetIndex < 0 || h.DiskIndex < 0 {
+ h.ID, _ = h.disk.GetDiskID()
+ h.PoolIndex, h.SetIndex, h.DiskIndex = h.disk.GetDiskLoc()
+ }
+ h.mu.Unlock()
+ return h.save(ctx)
+}
+
+// save will unconditionally save the tracker and will be created if not existing.
+func (h *healingTracker) save(ctx context.Context) error {
+ h.mu.Lock()
+ if h.PoolIndex < 0 || h.SetIndex < 0 || h.DiskIndex < 0 {
+ // Attempt to get location.
+ if api := newObjectLayerFn(); api != nil {
+ if ep, ok := api.(*erasureServerPools); ok {
+ h.PoolIndex, h.SetIndex, h.DiskIndex, _ = ep.getPoolAndSet(h.ID)
+ }
+ }
+ }
+ h.LastUpdate = time.Now().UTC()
+ htrackerBytes, err := h.MarshalMsg(nil)
+ h.mu.Unlock()
+ if err != nil {
+ return err
+ }
+ globalBackgroundHealState.updateHealStatus(h)
+ return h.disk.WriteAll(ctx, minioMetaBucket,
+ pathJoin(bucketMetaPrefix, healingTrackerFilename),
+ htrackerBytes)
+}
+
+// delete the tracker on disk.
+func (h *healingTracker) delete(ctx context.Context) error {
+ return h.disk.Delete(ctx, minioMetaBucket,
+ pathJoin(bucketMetaPrefix, healingTrackerFilename),
+ DeleteOptions{
+ Recursive: false,
+ Immediate: false,
+ },
+ )
+}
+
+func (h *healingTracker) isHealed(bucket string) bool {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+ return slices.Contains(h.HealedBuckets, bucket)
+}
+
+// resume will reset progress to the numbers at the start of the bucket.
+func (h *healingTracker) resume() {
+ h.mu.Lock()
+ defer h.mu.Unlock()
+
+ h.ItemsHealed = h.ResumeItemsHealed
+ h.ItemsFailed = h.ResumeItemsFailed
+ h.ItemsSkipped = h.ResumeItemsSkipped
+ h.BytesDone = h.ResumeBytesDone
+ h.BytesFailed = h.ResumeBytesFailed
+ h.BytesSkipped = h.ResumeBytesSkipped
+}
+
+// bucketDone should be called when a bucket is done healing.
+// Adds the bucket to the list of healed buckets and updates resume numbers.
+func (h *healingTracker) bucketDone(bucket string) {
+ h.mu.Lock()
+ defer h.mu.Unlock()
+
+ h.ResumeItemsHealed = h.ItemsHealed
+ h.ResumeItemsFailed = h.ItemsFailed
+ h.ResumeItemsSkipped = h.ItemsSkipped
+ h.ResumeBytesDone = h.BytesDone
+ h.ResumeBytesFailed = h.BytesFailed
+ h.ResumeBytesSkipped = h.BytesSkipped
+ h.HealedBuckets = append(h.HealedBuckets, bucket)
+ for i, b := range h.QueuedBuckets {
+ if b == bucket {
+ // Delete...
+ h.QueuedBuckets = append(h.QueuedBuckets[:i], h.QueuedBuckets[i+1:]...)
+ }
+ }
+}
+
+// setQueuedBuckets will add buckets, but exclude any that is already in h.HealedBuckets.
+// Order is preserved.
+func (h *healingTracker) setQueuedBuckets(buckets []BucketInfo) {
+ h.mu.Lock()
+ defer h.mu.Unlock()
+
+ s := set.CreateStringSet(h.HealedBuckets...)
+ h.QueuedBuckets = make([]string, 0, len(buckets))
+ for _, b := range buckets {
+ if !s.Contains(b.Name) {
+ h.QueuedBuckets = append(h.QueuedBuckets, b.Name)
+ }
+ }
+}
+
+func (h *healingTracker) printTo(writer io.Writer) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+
+ b, err := json.MarshalIndent(h, "", " ")
+ if err != nil {
+ writer.Write([]byte(err.Error()))
+ return
+ }
+ writer.Write(b)
+}
+
+// toHealingDisk converts the information to madmin.HealingDisk
+func (h *healingTracker) toHealingDisk() madmin.HealingDisk {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+
+ return madmin.HealingDisk{
+ ID: h.ID,
+ HealID: h.HealID,
+ Endpoint: h.Endpoint,
+ PoolIndex: h.PoolIndex,
+ SetIndex: h.SetIndex,
+ DiskIndex: h.DiskIndex,
+ Finished: h.Finished,
+ Path: h.Path,
+ Started: h.Started.UTC(),
+ LastUpdate: h.LastUpdate.UTC(),
+ ObjectsTotalCount: h.ObjectsTotalCount,
+ ObjectsTotalSize: h.ObjectsTotalSize,
+ ItemsHealed: h.ItemsHealed,
+ ItemsSkipped: h.ItemsSkipped,
+ ItemsFailed: h.ItemsFailed,
+ BytesDone: h.BytesDone,
+ BytesSkipped: h.BytesSkipped,
+ BytesFailed: h.BytesFailed,
+ Bucket: h.Bucket,
+ Object: h.Object,
+ QueuedBuckets: h.QueuedBuckets,
+ HealedBuckets: h.HealedBuckets,
+ RetryAttempts: h.RetryAttempts,
+
+ ObjectsHealed: h.ItemsHealed, // Deprecated July 2021
+ ObjectsFailed: h.ItemsFailed, // Deprecated July 2021
+
+ }
+}
+
+func initAutoHeal(ctx context.Context, objAPI ObjectLayer) {
+ z, ok := objAPI.(*erasureServerPools)
+ if !ok {
+ return
+ }
+
+ initBackgroundHealing(ctx, objAPI) // start quick background healing
+ if env.Get("_MINIO_AUTO_DRIVE_HEALING", config.EnableOn) == config.EnableOn {
+ globalBackgroundHealState.pushHealLocalDisks(getLocalDisksToHeal()...)
+ go monitorLocalDisksAndHeal(ctx, z)
+ }
+
+ go globalMRFState.startMRFPersistence()
+ go globalMRFState.healRoutine(z)
+}
+
+func getLocalDisksToHeal() (disksToHeal Endpoints) {
+ globalLocalDrivesMu.RLock()
+ localDrives := cloneDrives(globalLocalDrivesMap)
+ globalLocalDrivesMu.RUnlock()
+ for _, disk := range localDrives {
+ _, err := disk.DiskInfo(context.Background(), DiskInfoOptions{})
+ if errors.Is(err, errUnformattedDisk) {
+ disksToHeal = append(disksToHeal, disk.Endpoint())
+ continue
+ }
+ if h := disk.Healing(); h != nil && !h.Finished {
+ disksToHeal = append(disksToHeal, disk.Endpoint())
+ }
+ }
+ if len(disksToHeal) == globalEndpoints.NEndpoints() {
+ // When all disks == all command line endpoints
+ // this is a fresh setup, no need to trigger healing.
+ return Endpoints{}
+ }
+ return disksToHeal
+}
+
+var newDiskHealingTimeout = newDynamicTimeout(30*time.Second, 10*time.Second)
+
+var errRetryHealing = errors.New("some items failed to heal, we will retry healing this drive again")
+
+func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint) error {
+ poolIdx, setIdx := endpoint.PoolIdx, endpoint.SetIdx
+ disk := getStorageViaEndpoint(endpoint)
+ if disk == nil {
+ return fmt.Errorf("Unexpected error disk must be initialized by now after formatting: %s", endpoint)
+ }
+
+ _, err := disk.DiskInfo(ctx, DiskInfoOptions{})
+ if err != nil {
+ if errors.Is(err, errDriveIsRoot) {
+ // This is a root drive, ignore and move on
+ return nil
+ }
+ if !errors.Is(err, errUnformattedDisk) {
+ return err
+ }
+ }
+
+ // Prevent parallel erasure set healing
+ locker := z.NewNSLock(minioMetaBucket, fmt.Sprintf("new-drive-healing/%d/%d", poolIdx, setIdx))
+ lkctx, err := locker.GetLock(ctx, newDiskHealingTimeout)
+ if err != nil {
+ return fmt.Errorf("Healing of drive '%v' on %s pool, belonging to %s erasure set already in progress: %w",
+ disk, humanize.Ordinal(poolIdx+1), humanize.Ordinal(setIdx+1), err)
+ }
+ ctx = lkctx.Context()
+ defer locker.Unlock(lkctx)
+
+ // Load healing tracker in this disk
+ tracker, err := loadHealingTracker(ctx, disk)
+ if err != nil {
+ // A healing tracker may be deleted if another disk in the
+ // same erasure set with same healing-id successfully finished
+ // healing.
+ if errors.Is(err, errFileNotFound) {
+ return nil
+ }
+ healingLogIf(ctx, fmt.Errorf("Unable to load healing tracker on '%s': %w, re-initializing..", disk, err))
+ tracker = initHealingTracker(disk, mustGetUUID())
+ }
+
+ healingLogEvent(ctx, "Healing drive '%s' - 'mc admin heal alias/ --verbose' to check the current status.", endpoint)
+
+ buckets, _ := z.ListBuckets(ctx, BucketOptions{})
+ // Buckets data are dispersed in multiple pools/sets, make
+ // sure to heal all bucket metadata configuration.
+ buckets = append(buckets, BucketInfo{
+ Name: pathJoin(minioMetaBucket, minioConfigPrefix),
+ }, BucketInfo{
+ Name: pathJoin(minioMetaBucket, bucketMetaPrefix),
+ })
+
+ // Heal latest buckets first.
+ sort.Slice(buckets, func(i, j int) bool {
+ a, b := strings.HasPrefix(buckets[i].Name, minioMetaBucket), strings.HasPrefix(buckets[j].Name, minioMetaBucket)
+ if a != b {
+ return a
+ }
+ return buckets[i].Created.After(buckets[j].Created)
+ })
+
+ // Load bucket totals
+ cache := dataUsageCache{}
+ if err := cache.load(ctx, z.serverPools[poolIdx].sets[setIdx], dataUsageCacheName); err == nil {
+ dataUsageInfo := cache.dui(dataUsageRoot, nil)
+ tracker.ObjectsTotalCount = dataUsageInfo.ObjectsTotalCount
+ tracker.ObjectsTotalSize = dataUsageInfo.ObjectsTotalSize
+ }
+
+ tracker.PoolIndex, tracker.SetIndex, tracker.DiskIndex = disk.GetDiskLoc()
+ tracker.setQueuedBuckets(buckets)
+ if err := tracker.save(ctx); err != nil {
+ return err
+ }
+
+ // Start or resume healing of this erasure set
+ if err = z.serverPools[poolIdx].sets[setIdx].healErasureSet(ctx, tracker.QueuedBuckets, tracker); err != nil {
+ return err
+ }
+
+ // if objects have failed healing, we attempt a retry to heal the drive upto 3 times before giving up.
+ if tracker.ItemsFailed > 0 && tracker.RetryAttempts < 4 {
+ tracker.RetryAttempts++
+
+ healingLogEvent(ctx, "Healing of drive '%s' is incomplete, retrying %s time (healed: %d, skipped: %d, failed: %d).", disk,
+ humanize.Ordinal(int(tracker.RetryAttempts)), tracker.ItemsHealed, tracker.ItemsSkipped, tracker.ItemsFailed)
+
+ tracker.resetHealing()
+ bugLogIf(ctx, tracker.update(ctx))
+
+ return errRetryHealing
+ }
+
+ if tracker.ItemsFailed > 0 {
+ healingLogEvent(ctx, "Healing of drive '%s' is incomplete, retried %d times (healed: %d, skipped: %d, failed: %d).", disk,
+ tracker.RetryAttempts, tracker.ItemsHealed, tracker.ItemsSkipped, tracker.ItemsFailed)
+ } else {
+ if tracker.RetryAttempts > 0 {
+ healingLogEvent(ctx, "Healing of drive '%s' is complete, retried %d times (healed: %d, skipped: %d).", disk,
+ tracker.RetryAttempts-1, tracker.ItemsHealed, tracker.ItemsSkipped)
+ } else {
+ healingLogEvent(ctx, "Healing of drive '%s' is finished (healed: %d, skipped: %d).", disk, tracker.ItemsHealed, tracker.ItemsSkipped)
+ }
+ }
+ if serverDebugLog {
+ tracker.printTo(os.Stdout)
+ fmt.Printf("\n")
+ }
+
+ if tracker.HealID == "" { // HealID was empty only before Feb 2023
+ bugLogIf(ctx, tracker.delete(ctx))
+ return nil
+ }
+
+ // Remove .healing.bin from all disks with similar heal-id
+ disks, err := z.GetDisks(poolIdx, setIdx)
+ if err != nil {
+ return err
+ }
+
+ for _, disk := range disks {
+ if disk == nil {
+ continue
+ }
+
+ t, err := loadHealingTracker(ctx, disk)
+ if err != nil {
+ if !errors.Is(err, errFileNotFound) {
+ healingLogIf(ctx, err)
+ }
+ continue
+ }
+ if t.HealID == tracker.HealID {
+ t.Finished = true
+ t.update(ctx)
+ }
+ }
+
+ return nil
+}
+
+// monitorLocalDisksAndHeal - ensures that detected new disks are healed
+// 1. Only the concerned erasure set will be listed and healed
+// 2. Only the node hosting the disk is responsible to perform the heal
+func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools) {
+ // Perform automatic disk healing when a disk is replaced locally.
+ diskCheckTimer := time.NewTimer(defaultMonitorNewDiskInterval)
+ defer diskCheckTimer.Stop()
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-diskCheckTimer.C:
+ healDisks := globalBackgroundHealState.getHealLocalDiskEndpoints()
+ if len(healDisks) == 0 {
+ // Reset for next interval.
+ diskCheckTimer.Reset(defaultMonitorNewDiskInterval)
+ continue
+ }
+
+ // Reformat disks immediately
+ _, err := z.HealFormat(context.Background(), false)
+ if err != nil && !errors.Is(err, errNoHealRequired) {
+ healingLogIf(ctx, err)
+ // Reset for next interval.
+ diskCheckTimer.Reset(defaultMonitorNewDiskInterval)
+ continue
+ }
+
+ for _, disk := range healDisks {
+ go func(disk Endpoint) {
+ globalBackgroundHealState.setDiskHealingStatus(disk, true)
+ if err := healFreshDisk(ctx, z, disk); err != nil {
+ globalBackgroundHealState.setDiskHealingStatus(disk, false)
+ timedout := OperationTimedOut{}
+ if !errors.Is(err, context.Canceled) && !errors.As(err, &timedout) && !errors.Is(err, errRetryHealing) {
+ printEndpointError(disk, err, false)
+ }
+ return
+ }
+ // Only upon success pop the healed disk.
+ globalBackgroundHealState.popHealLocalDisks(disk)
+ }(disk)
+ }
+
+ // Reset for next interval.
+ diskCheckTimer.Reset(defaultMonitorNewDiskInterval)
+ }
+ }
+}
diff --git a/cmd/background-newdisks-heal-ops_gen.go b/cmd/background-newdisks-heal-ops_gen.go
new file mode 100644
index 00000000..7c3eb6b8
--- /dev/null
+++ b/cmd/background-newdisks-heal-ops_gen.go
@@ -0,0 +1,890 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "github.com/tinylib/msgp/msgp"
+)
+
+// DecodeMsg implements msgp.Decodable
+func (z *healingTracker) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ID":
+ z.ID, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "ID")
+ return
+ }
+ case "PoolIndex":
+ z.PoolIndex, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "PoolIndex")
+ return
+ }
+ case "SetIndex":
+ z.SetIndex, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "SetIndex")
+ return
+ }
+ case "DiskIndex":
+ z.DiskIndex, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "DiskIndex")
+ return
+ }
+ case "Path":
+ z.Path, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Path")
+ return
+ }
+ case "Endpoint":
+ z.Endpoint, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Endpoint")
+ return
+ }
+ case "Started":
+ z.Started, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "Started")
+ return
+ }
+ case "LastUpdate":
+ z.LastUpdate, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "LastUpdate")
+ return
+ }
+ case "ObjectsTotalCount":
+ z.ObjectsTotalCount, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "ObjectsTotalCount")
+ return
+ }
+ case "ObjectsTotalSize":
+ z.ObjectsTotalSize, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "ObjectsTotalSize")
+ return
+ }
+ case "ItemsHealed":
+ z.ItemsHealed, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "ItemsHealed")
+ return
+ }
+ case "ItemsFailed":
+ z.ItemsFailed, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "ItemsFailed")
+ return
+ }
+ case "BytesDone":
+ z.BytesDone, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "BytesDone")
+ return
+ }
+ case "BytesFailed":
+ z.BytesFailed, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "BytesFailed")
+ return
+ }
+ case "Bucket":
+ z.Bucket, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "Object":
+ z.Object, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ case "ResumeItemsHealed":
+ z.ResumeItemsHealed, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "ResumeItemsHealed")
+ return
+ }
+ case "ResumeItemsFailed":
+ z.ResumeItemsFailed, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "ResumeItemsFailed")
+ return
+ }
+ case "ResumeItemsSkipped":
+ z.ResumeItemsSkipped, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "ResumeItemsSkipped")
+ return
+ }
+ case "ResumeBytesDone":
+ z.ResumeBytesDone, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "ResumeBytesDone")
+ return
+ }
+ case "ResumeBytesFailed":
+ z.ResumeBytesFailed, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "ResumeBytesFailed")
+ return
+ }
+ case "ResumeBytesSkipped":
+ z.ResumeBytesSkipped, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "ResumeBytesSkipped")
+ return
+ }
+ case "QueuedBuckets":
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "QueuedBuckets")
+ return
+ }
+ if cap(z.QueuedBuckets) >= int(zb0002) {
+ z.QueuedBuckets = (z.QueuedBuckets)[:zb0002]
+ } else {
+ z.QueuedBuckets = make([]string, zb0002)
+ }
+ for za0001 := range z.QueuedBuckets {
+ z.QueuedBuckets[za0001], err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "QueuedBuckets", za0001)
+ return
+ }
+ }
+ case "HealedBuckets":
+ var zb0003 uint32
+ zb0003, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "HealedBuckets")
+ return
+ }
+ if cap(z.HealedBuckets) >= int(zb0003) {
+ z.HealedBuckets = (z.HealedBuckets)[:zb0003]
+ } else {
+ z.HealedBuckets = make([]string, zb0003)
+ }
+ for za0002 := range z.HealedBuckets {
+ z.HealedBuckets[za0002], err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "HealedBuckets", za0002)
+ return
+ }
+ }
+ case "HealID":
+ z.HealID, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "HealID")
+ return
+ }
+ case "ItemsSkipped":
+ z.ItemsSkipped, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "ItemsSkipped")
+ return
+ }
+ case "BytesSkipped":
+ z.BytesSkipped, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "BytesSkipped")
+ return
+ }
+ case "RetryAttempts":
+ z.RetryAttempts, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "RetryAttempts")
+ return
+ }
+ case "Finished":
+ z.Finished, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "Finished")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *healingTracker) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 29
+ // write "ID"
+ err = en.Append(0xde, 0x0, 0x1d, 0xa2, 0x49, 0x44)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.ID)
+ if err != nil {
+ err = msgp.WrapError(err, "ID")
+ return
+ }
+ // write "PoolIndex"
+ err = en.Append(0xa9, 0x50, 0x6f, 0x6f, 0x6c, 0x49, 0x6e, 0x64, 0x65, 0x78)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.PoolIndex)
+ if err != nil {
+ err = msgp.WrapError(err, "PoolIndex")
+ return
+ }
+ // write "SetIndex"
+ err = en.Append(0xa8, 0x53, 0x65, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.SetIndex)
+ if err != nil {
+ err = msgp.WrapError(err, "SetIndex")
+ return
+ }
+ // write "DiskIndex"
+ err = en.Append(0xa9, 0x44, 0x69, 0x73, 0x6b, 0x49, 0x6e, 0x64, 0x65, 0x78)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.DiskIndex)
+ if err != nil {
+ err = msgp.WrapError(err, "DiskIndex")
+ return
+ }
+ // write "Path"
+ err = en.Append(0xa4, 0x50, 0x61, 0x74, 0x68)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Path)
+ if err != nil {
+ err = msgp.WrapError(err, "Path")
+ return
+ }
+ // write "Endpoint"
+ err = en.Append(0xa8, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Endpoint)
+ if err != nil {
+ err = msgp.WrapError(err, "Endpoint")
+ return
+ }
+ // write "Started"
+ err = en.Append(0xa7, 0x53, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.Started)
+ if err != nil {
+ err = msgp.WrapError(err, "Started")
+ return
+ }
+ // write "LastUpdate"
+ err = en.Append(0xaa, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.LastUpdate)
+ if err != nil {
+ err = msgp.WrapError(err, "LastUpdate")
+ return
+ }
+ // write "ObjectsTotalCount"
+ err = en.Append(0xb1, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.ObjectsTotalCount)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjectsTotalCount")
+ return
+ }
+ // write "ObjectsTotalSize"
+ err = en.Append(0xb0, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x53, 0x69, 0x7a, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.ObjectsTotalSize)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjectsTotalSize")
+ return
+ }
+ // write "ItemsHealed"
+ err = en.Append(0xab, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.ItemsHealed)
+ if err != nil {
+ err = msgp.WrapError(err, "ItemsHealed")
+ return
+ }
+ // write "ItemsFailed"
+ err = en.Append(0xab, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.ItemsFailed)
+ if err != nil {
+ err = msgp.WrapError(err, "ItemsFailed")
+ return
+ }
+ // write "BytesDone"
+ err = en.Append(0xa9, 0x42, 0x79, 0x74, 0x65, 0x73, 0x44, 0x6f, 0x6e, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.BytesDone)
+ if err != nil {
+ err = msgp.WrapError(err, "BytesDone")
+ return
+ }
+ // write "BytesFailed"
+ err = en.Append(0xab, 0x42, 0x79, 0x74, 0x65, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.BytesFailed)
+ if err != nil {
+ err = msgp.WrapError(err, "BytesFailed")
+ return
+ }
+ // write "Bucket"
+ err = en.Append(0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Bucket)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ // write "Object"
+ err = en.Append(0xa6, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Object)
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ // write "ResumeItemsHealed"
+ err = en.Append(0xb1, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.ResumeItemsHealed)
+ if err != nil {
+ err = msgp.WrapError(err, "ResumeItemsHealed")
+ return
+ }
+ // write "ResumeItemsFailed"
+ err = en.Append(0xb1, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.ResumeItemsFailed)
+ if err != nil {
+ err = msgp.WrapError(err, "ResumeItemsFailed")
+ return
+ }
+ // write "ResumeItemsSkipped"
+ err = en.Append(0xb2, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x53, 0x6b, 0x69, 0x70, 0x70, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.ResumeItemsSkipped)
+ if err != nil {
+ err = msgp.WrapError(err, "ResumeItemsSkipped")
+ return
+ }
+ // write "ResumeBytesDone"
+ err = en.Append(0xaf, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x44, 0x6f, 0x6e, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.ResumeBytesDone)
+ if err != nil {
+ err = msgp.WrapError(err, "ResumeBytesDone")
+ return
+ }
+ // write "ResumeBytesFailed"
+ err = en.Append(0xb1, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.ResumeBytesFailed)
+ if err != nil {
+ err = msgp.WrapError(err, "ResumeBytesFailed")
+ return
+ }
+ // write "ResumeBytesSkipped"
+ err = en.Append(0xb2, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x53, 0x6b, 0x69, 0x70, 0x70, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.ResumeBytesSkipped)
+ if err != nil {
+ err = msgp.WrapError(err, "ResumeBytesSkipped")
+ return
+ }
+ // write "QueuedBuckets"
+ err = en.Append(0xad, 0x51, 0x75, 0x65, 0x75, 0x65, 0x64, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(len(z.QueuedBuckets)))
+ if err != nil {
+ err = msgp.WrapError(err, "QueuedBuckets")
+ return
+ }
+ for za0001 := range z.QueuedBuckets {
+ err = en.WriteString(z.QueuedBuckets[za0001])
+ if err != nil {
+ err = msgp.WrapError(err, "QueuedBuckets", za0001)
+ return
+ }
+ }
+ // write "HealedBuckets"
+ err = en.Append(0xad, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(len(z.HealedBuckets)))
+ if err != nil {
+ err = msgp.WrapError(err, "HealedBuckets")
+ return
+ }
+ for za0002 := range z.HealedBuckets {
+ err = en.WriteString(z.HealedBuckets[za0002])
+ if err != nil {
+ err = msgp.WrapError(err, "HealedBuckets", za0002)
+ return
+ }
+ }
+ // write "HealID"
+ err = en.Append(0xa6, 0x48, 0x65, 0x61, 0x6c, 0x49, 0x44)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.HealID)
+ if err != nil {
+ err = msgp.WrapError(err, "HealID")
+ return
+ }
+ // write "ItemsSkipped"
+ err = en.Append(0xac, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x53, 0x6b, 0x69, 0x70, 0x70, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.ItemsSkipped)
+ if err != nil {
+ err = msgp.WrapError(err, "ItemsSkipped")
+ return
+ }
+ // write "BytesSkipped"
+ err = en.Append(0xac, 0x42, 0x79, 0x74, 0x65, 0x73, 0x53, 0x6b, 0x69, 0x70, 0x70, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.BytesSkipped)
+ if err != nil {
+ err = msgp.WrapError(err, "BytesSkipped")
+ return
+ }
+ // write "RetryAttempts"
+ err = en.Append(0xad, 0x52, 0x65, 0x74, 0x72, 0x79, 0x41, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.RetryAttempts)
+ if err != nil {
+ err = msgp.WrapError(err, "RetryAttempts")
+ return
+ }
+ // write "Finished"
+ err = en.Append(0xa8, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.Finished)
+ if err != nil {
+ err = msgp.WrapError(err, "Finished")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *healingTracker) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 29
+ // string "ID"
+ o = append(o, 0xde, 0x0, 0x1d, 0xa2, 0x49, 0x44)
+ o = msgp.AppendString(o, z.ID)
+ // string "PoolIndex"
+ o = append(o, 0xa9, 0x50, 0x6f, 0x6f, 0x6c, 0x49, 0x6e, 0x64, 0x65, 0x78)
+ o = msgp.AppendInt(o, z.PoolIndex)
+ // string "SetIndex"
+ o = append(o, 0xa8, 0x53, 0x65, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78)
+ o = msgp.AppendInt(o, z.SetIndex)
+ // string "DiskIndex"
+ o = append(o, 0xa9, 0x44, 0x69, 0x73, 0x6b, 0x49, 0x6e, 0x64, 0x65, 0x78)
+ o = msgp.AppendInt(o, z.DiskIndex)
+ // string "Path"
+ o = append(o, 0xa4, 0x50, 0x61, 0x74, 0x68)
+ o = msgp.AppendString(o, z.Path)
+ // string "Endpoint"
+ o = append(o, 0xa8, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74)
+ o = msgp.AppendString(o, z.Endpoint)
+ // string "Started"
+ o = append(o, 0xa7, 0x53, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64)
+ o = msgp.AppendTime(o, z.Started)
+ // string "LastUpdate"
+ o = append(o, 0xaa, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65)
+ o = msgp.AppendTime(o, z.LastUpdate)
+ // string "ObjectsTotalCount"
+ o = append(o, 0xb1, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ o = msgp.AppendUint64(o, z.ObjectsTotalCount)
+ // string "ObjectsTotalSize"
+ o = append(o, 0xb0, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x53, 0x69, 0x7a, 0x65)
+ o = msgp.AppendUint64(o, z.ObjectsTotalSize)
+ // string "ItemsHealed"
+ o = append(o, 0xab, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
+ o = msgp.AppendUint64(o, z.ItemsHealed)
+ // string "ItemsFailed"
+ o = append(o, 0xab, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
+ o = msgp.AppendUint64(o, z.ItemsFailed)
+ // string "BytesDone"
+ o = append(o, 0xa9, 0x42, 0x79, 0x74, 0x65, 0x73, 0x44, 0x6f, 0x6e, 0x65)
+ o = msgp.AppendUint64(o, z.BytesDone)
+ // string "BytesFailed"
+ o = append(o, 0xab, 0x42, 0x79, 0x74, 0x65, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
+ o = msgp.AppendUint64(o, z.BytesFailed)
+ // string "Bucket"
+ o = append(o, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ o = msgp.AppendString(o, z.Bucket)
+ // string "Object"
+ o = append(o, 0xa6, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74)
+ o = msgp.AppendString(o, z.Object)
+ // string "ResumeItemsHealed"
+ o = append(o, 0xb1, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
+ o = msgp.AppendUint64(o, z.ResumeItemsHealed)
+ // string "ResumeItemsFailed"
+ o = append(o, 0xb1, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
+ o = msgp.AppendUint64(o, z.ResumeItemsFailed)
+ // string "ResumeItemsSkipped"
+ o = append(o, 0xb2, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x53, 0x6b, 0x69, 0x70, 0x70, 0x65, 0x64)
+ o = msgp.AppendUint64(o, z.ResumeItemsSkipped)
+ // string "ResumeBytesDone"
+ o = append(o, 0xaf, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x44, 0x6f, 0x6e, 0x65)
+ o = msgp.AppendUint64(o, z.ResumeBytesDone)
+ // string "ResumeBytesFailed"
+ o = append(o, 0xb1, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
+ o = msgp.AppendUint64(o, z.ResumeBytesFailed)
+ // string "ResumeBytesSkipped"
+ o = append(o, 0xb2, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x53, 0x6b, 0x69, 0x70, 0x70, 0x65, 0x64)
+ o = msgp.AppendUint64(o, z.ResumeBytesSkipped)
+ // string "QueuedBuckets"
+ o = append(o, 0xad, 0x51, 0x75, 0x65, 0x75, 0x65, 0x64, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.QueuedBuckets)))
+ for za0001 := range z.QueuedBuckets {
+ o = msgp.AppendString(o, z.QueuedBuckets[za0001])
+ }
+ // string "HealedBuckets"
+ o = append(o, 0xad, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.HealedBuckets)))
+ for za0002 := range z.HealedBuckets {
+ o = msgp.AppendString(o, z.HealedBuckets[za0002])
+ }
+ // string "HealID"
+ o = append(o, 0xa6, 0x48, 0x65, 0x61, 0x6c, 0x49, 0x44)
+ o = msgp.AppendString(o, z.HealID)
+ // string "ItemsSkipped"
+ o = append(o, 0xac, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x53, 0x6b, 0x69, 0x70, 0x70, 0x65, 0x64)
+ o = msgp.AppendUint64(o, z.ItemsSkipped)
+ // string "BytesSkipped"
+ o = append(o, 0xac, 0x42, 0x79, 0x74, 0x65, 0x73, 0x53, 0x6b, 0x69, 0x70, 0x70, 0x65, 0x64)
+ o = msgp.AppendUint64(o, z.BytesSkipped)
+ // string "RetryAttempts"
+ o = append(o, 0xad, 0x52, 0x65, 0x74, 0x72, 0x79, 0x41, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x73)
+ o = msgp.AppendUint64(o, z.RetryAttempts)
+ // string "Finished"
+ o = append(o, 0xa8, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64)
+ o = msgp.AppendBool(o, z.Finished)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *healingTracker) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ID":
+ z.ID, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ID")
+ return
+ }
+ case "PoolIndex":
+ z.PoolIndex, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "PoolIndex")
+ return
+ }
+ case "SetIndex":
+ z.SetIndex, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "SetIndex")
+ return
+ }
+ case "DiskIndex":
+ z.DiskIndex, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "DiskIndex")
+ return
+ }
+ case "Path":
+ z.Path, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Path")
+ return
+ }
+ case "Endpoint":
+ z.Endpoint, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Endpoint")
+ return
+ }
+ case "Started":
+ z.Started, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Started")
+ return
+ }
+ case "LastUpdate":
+ z.LastUpdate, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "LastUpdate")
+ return
+ }
+ case "ObjectsTotalCount":
+ z.ObjectsTotalCount, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjectsTotalCount")
+ return
+ }
+ case "ObjectsTotalSize":
+ z.ObjectsTotalSize, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjectsTotalSize")
+ return
+ }
+ case "ItemsHealed":
+ z.ItemsHealed, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ItemsHealed")
+ return
+ }
+ case "ItemsFailed":
+ z.ItemsFailed, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ItemsFailed")
+ return
+ }
+ case "BytesDone":
+ z.BytesDone, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "BytesDone")
+ return
+ }
+ case "BytesFailed":
+ z.BytesFailed, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "BytesFailed")
+ return
+ }
+ case "Bucket":
+ z.Bucket, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "Object":
+ z.Object, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ case "ResumeItemsHealed":
+ z.ResumeItemsHealed, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ResumeItemsHealed")
+ return
+ }
+ case "ResumeItemsFailed":
+ z.ResumeItemsFailed, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ResumeItemsFailed")
+ return
+ }
+ case "ResumeItemsSkipped":
+ z.ResumeItemsSkipped, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ResumeItemsSkipped")
+ return
+ }
+ case "ResumeBytesDone":
+ z.ResumeBytesDone, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ResumeBytesDone")
+ return
+ }
+ case "ResumeBytesFailed":
+ z.ResumeBytesFailed, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ResumeBytesFailed")
+ return
+ }
+ case "ResumeBytesSkipped":
+ z.ResumeBytesSkipped, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ResumeBytesSkipped")
+ return
+ }
+ case "QueuedBuckets":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "QueuedBuckets")
+ return
+ }
+ if cap(z.QueuedBuckets) >= int(zb0002) {
+ z.QueuedBuckets = (z.QueuedBuckets)[:zb0002]
+ } else {
+ z.QueuedBuckets = make([]string, zb0002)
+ }
+ for za0001 := range z.QueuedBuckets {
+ z.QueuedBuckets[za0001], bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "QueuedBuckets", za0001)
+ return
+ }
+ }
+ case "HealedBuckets":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "HealedBuckets")
+ return
+ }
+ if cap(z.HealedBuckets) >= int(zb0003) {
+ z.HealedBuckets = (z.HealedBuckets)[:zb0003]
+ } else {
+ z.HealedBuckets = make([]string, zb0003)
+ }
+ for za0002 := range z.HealedBuckets {
+ z.HealedBuckets[za0002], bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "HealedBuckets", za0002)
+ return
+ }
+ }
+ case "HealID":
+ z.HealID, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "HealID")
+ return
+ }
+ case "ItemsSkipped":
+ z.ItemsSkipped, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ItemsSkipped")
+ return
+ }
+ case "BytesSkipped":
+ z.BytesSkipped, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "BytesSkipped")
+ return
+ }
+ case "RetryAttempts":
+ z.RetryAttempts, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "RetryAttempts")
+ return
+ }
+ case "Finished":
+ z.Finished, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Finished")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *healingTracker) Msgsize() (s int) {
+ s = 3 + 3 + msgp.StringPrefixSize + len(z.ID) + 10 + msgp.IntSize + 9 + msgp.IntSize + 10 + msgp.IntSize + 5 + msgp.StringPrefixSize + len(z.Path) + 9 + msgp.StringPrefixSize + len(z.Endpoint) + 8 + msgp.TimeSize + 11 + msgp.TimeSize + 18 + msgp.Uint64Size + 17 + msgp.Uint64Size + 12 + msgp.Uint64Size + 12 + msgp.Uint64Size + 10 + msgp.Uint64Size + 12 + msgp.Uint64Size + 7 + msgp.StringPrefixSize + len(z.Bucket) + 7 + msgp.StringPrefixSize + len(z.Object) + 18 + msgp.Uint64Size + 18 + msgp.Uint64Size + 19 + msgp.Uint64Size + 16 + msgp.Uint64Size + 18 + msgp.Uint64Size + 19 + msgp.Uint64Size + 14 + msgp.ArrayHeaderSize
+ for za0001 := range z.QueuedBuckets {
+ s += msgp.StringPrefixSize + len(z.QueuedBuckets[za0001])
+ }
+ s += 14 + msgp.ArrayHeaderSize
+ for za0002 := range z.HealedBuckets {
+ s += msgp.StringPrefixSize + len(z.HealedBuckets[za0002])
+ }
+ s += 7 + msgp.StringPrefixSize + len(z.HealID) + 13 + msgp.Uint64Size + 13 + msgp.Uint64Size + 14 + msgp.Uint64Size + 9 + msgp.BoolSize
+ return
+}
diff --git a/cmd/background-newdisks-heal-ops_gen_test.go b/cmd/background-newdisks-heal-ops_gen_test.go
new file mode 100644
index 00000000..36e9eb71
--- /dev/null
+++ b/cmd/background-newdisks-heal-ops_gen_test.go
@@ -0,0 +1,123 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+func TestMarshalUnmarshalhealingTracker(t *testing.T) {
+ v := healingTracker{}
+ 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 BenchmarkMarshalMsghealingTracker(b *testing.B) {
+ v := healingTracker{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsghealingTracker(b *testing.B) {
+ v := healingTracker{}
+ 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 BenchmarkUnmarshalhealingTracker(b *testing.B) {
+ v := healingTracker{}
+ 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 TestEncodeDecodehealingTracker(t *testing.T) {
+ v := healingTracker{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodehealingTracker Msgsize() is inaccurate")
+ }
+
+ vn := healingTracker{}
+ 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 BenchmarkEncodehealingTracker(b *testing.B) {
+ v := healingTracker{}
+ 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 BenchmarkDecodehealingTracker(b *testing.B) {
+ v := healingTracker{}
+ 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)
+ }
+ }
+}
diff --git a/cmd/batch-expire.go b/cmd/batch-expire.go
new file mode 100644
index 00000000..fab59266
--- /dev/null
+++ b/cmd/batch-expire.go
@@ -0,0 +1,839 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "runtime"
+ "strconv"
+ "time"
+
+ "github.com/minio/minio-go/v7/pkg/tags"
+ "github.com/minio/minio/internal/bucket/versioning"
+ xhttp "github.com/minio/minio/internal/http"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/pkg/v3/env"
+ "github.com/minio/pkg/v3/wildcard"
+ "github.com/minio/pkg/v3/workers"
+ "github.com/minio/pkg/v3/xtime"
+ "gopkg.in/yaml.v3"
+)
+
+// expire: # Expire objects that match a condition
+// apiVersion: v1
+// bucket: mybucket # Bucket where this batch job will expire matching objects from
+// prefix: myprefix # (Optional) Prefix under which this job will expire objects matching the rules below.
+// rules:
+// - type: object # regular objects with zero or more older versions
+// name: NAME # match object names that satisfy the wildcard expression.
+// olderThan: 70h # match objects older than this value
+// createdBefore: "2006-01-02T15:04:05.00Z" # match objects created before "date"
+// tags:
+// - key: name
+// value: pick* # match objects with tag 'name', all values starting with 'pick'
+// metadata:
+// - key: content-type
+// value: image/* # match objects with 'content-type', all values starting with 'image/'
+// size:
+// lessThan: "10MiB" # match objects with size less than this value (e.g. 10MiB)
+// greaterThan: 1MiB # match objects with size greater than this value (e.g. 1MiB)
+// purge:
+// # retainVersions: 0 # (default) delete all versions of the object. This option is the fastest.
+// # retainVersions: 5 # keep the latest 5 versions of the object.
+//
+// - type: deleted # objects with delete marker as their latest version
+// name: NAME # match object names that satisfy the wildcard expression.
+// olderThan: 10h # match objects older than this value (e.g. 7d10h31s)
+// createdBefore: "2006-01-02T15:04:05.00Z" # match objects created before "date"
+// purge:
+// # retainVersions: 0 # (default) delete all versions of the object. This option is the fastest.
+// # retainVersions: 5 # keep the latest 5 versions of the object including delete markers.
+//
+// notify:
+// endpoint: https://notify.endpoint # notification endpoint to receive job completion status
+// token: Bearer xxxxx # optional authentication token for the notification endpoint
+//
+// retry:
+// attempts: 10 # number of retries for the job before giving up
+// delay: 500ms # least amount of delay between each retry
+
+//go:generate msgp -file $GOFILE
+
+// BatchJobExpirePurge type accepts non-negative versions to be retained
+type BatchJobExpirePurge struct {
+ line, col int
+ RetainVersions int `yaml:"retainVersions" json:"retainVersions"`
+}
+
+var _ yaml.Unmarshaler = &BatchJobExpirePurge{}
+
+// UnmarshalYAML - BatchJobExpirePurge extends unmarshal to extract line, col
+func (p *BatchJobExpirePurge) UnmarshalYAML(val *yaml.Node) error {
+ type purge BatchJobExpirePurge
+ var tmp purge
+ err := val.Decode(&tmp)
+ if err != nil {
+ return err
+ }
+
+ *p = BatchJobExpirePurge(tmp)
+ p.line, p.col = val.Line, val.Column
+ return nil
+}
+
+// Validate returns nil if value is valid, ie > 0.
+func (p BatchJobExpirePurge) Validate() error {
+ if p.RetainVersions < 0 {
+ return BatchJobYamlErr{
+ line: p.line,
+ col: p.col,
+ msg: "retainVersions must be >= 0",
+ }
+ }
+ return nil
+}
+
+// BatchJobExpireFilter holds all the filters currently supported for batch replication
+type BatchJobExpireFilter struct {
+ line, col int
+ OlderThan xtime.Duration `yaml:"olderThan,omitempty" json:"olderThan"`
+ CreatedBefore *time.Time `yaml:"createdBefore,omitempty" json:"createdBefore"`
+ Tags []BatchJobKV `yaml:"tags,omitempty" json:"tags"`
+ Metadata []BatchJobKV `yaml:"metadata,omitempty" json:"metadata"`
+ Size BatchJobSizeFilter `yaml:"size" json:"size"`
+ Type string `yaml:"type" json:"type"`
+ Name string `yaml:"name" json:"name"`
+ Purge BatchJobExpirePurge `yaml:"purge" json:"purge"`
+}
+
+var _ yaml.Unmarshaler = &BatchJobExpireFilter{}
+
+// UnmarshalYAML - BatchJobExpireFilter extends unmarshal to extract line, col
+// information
+func (ef *BatchJobExpireFilter) UnmarshalYAML(value *yaml.Node) error {
+ type expFilter BatchJobExpireFilter
+ var tmp expFilter
+ err := value.Decode(&tmp)
+ if err != nil {
+ return err
+ }
+ *ef = BatchJobExpireFilter(tmp)
+ ef.line, ef.col = value.Line, value.Column
+ return err
+}
+
+// Matches returns true if obj matches the filter conditions specified in ef.
+func (ef BatchJobExpireFilter) Matches(obj ObjectInfo, now time.Time) bool {
+ switch ef.Type {
+ case BatchJobExpireObject:
+ if obj.DeleteMarker {
+ return false
+ }
+ case BatchJobExpireDeleted:
+ if !obj.DeleteMarker {
+ return false
+ }
+ default:
+ // we should never come here, Validate should have caught this.
+ batchLogOnceIf(context.Background(), fmt.Errorf("invalid filter type: %s", ef.Type), ef.Type)
+ return false
+ }
+
+ if len(ef.Name) > 0 && !wildcard.Match(ef.Name, obj.Name) {
+ return false
+ }
+ if ef.OlderThan > 0 && now.Sub(obj.ModTime) <= ef.OlderThan.D() {
+ return false
+ }
+
+ if ef.CreatedBefore != nil && !obj.ModTime.Before(*ef.CreatedBefore) {
+ return false
+ }
+
+ if len(ef.Tags) > 0 && !obj.DeleteMarker {
+ // Only parse object tags if tags filter is specified.
+ var tagMap map[string]string
+ if len(obj.UserTags) != 0 {
+ t, err := tags.ParseObjectTags(obj.UserTags)
+ if err != nil {
+ return false
+ }
+ tagMap = t.ToMap()
+ }
+
+ for _, kv := range ef.Tags {
+ // Object (version) must match all tags specified in
+ // the filter
+ var match bool
+ for t, v := range tagMap {
+ if kv.Match(BatchJobKV{Key: t, Value: v}) {
+ match = true
+ }
+ }
+ if !match {
+ return false
+ }
+ }
+ }
+
+ if len(ef.Metadata) > 0 && !obj.DeleteMarker {
+ for _, kv := range ef.Metadata {
+ // Object (version) must match all x-amz-meta and
+ // standard metadata headers
+ // specified in the filter
+ var match bool
+ for k, v := range obj.UserDefined {
+ if !stringsHasPrefixFold(k, "x-amz-meta-") && !isStandardHeader(k) {
+ continue
+ }
+ // We only need to match x-amz-meta or standardHeaders
+ if kv.Match(BatchJobKV{Key: k, Value: v}) {
+ match = true
+ }
+ }
+ if !match {
+ return false
+ }
+ }
+ }
+
+ return ef.Size.InRange(obj.Size)
+}
+
+const (
+ // BatchJobExpireObject - object type
+ BatchJobExpireObject string = "object"
+ // BatchJobExpireDeleted - delete marker type
+ BatchJobExpireDeleted string = "deleted"
+)
+
+// Validate returns nil if ef has valid fields, validation error otherwise.
+func (ef BatchJobExpireFilter) Validate() error {
+ switch ef.Type {
+ case BatchJobExpireObject:
+ case BatchJobExpireDeleted:
+ if len(ef.Tags) > 0 || len(ef.Metadata) > 0 {
+ return BatchJobYamlErr{
+ line: ef.line,
+ col: ef.col,
+ msg: "delete type filter can't have tags or metadata",
+ }
+ }
+ default:
+ return BatchJobYamlErr{
+ line: ef.line,
+ col: ef.col,
+ msg: "invalid batch-expire type",
+ }
+ }
+
+ for _, tag := range ef.Tags {
+ if err := tag.Validate(); err != nil {
+ return err
+ }
+ }
+
+ for _, meta := range ef.Metadata {
+ if err := meta.Validate(); err != nil {
+ return err
+ }
+ }
+ if err := ef.Purge.Validate(); err != nil {
+ return err
+ }
+ if err := ef.Size.Validate(); err != nil {
+ return err
+ }
+ if ef.CreatedBefore != nil && !ef.CreatedBefore.Before(time.Now()) {
+ return BatchJobYamlErr{
+ line: ef.line,
+ col: ef.col,
+ msg: "CreatedBefore is in the future",
+ }
+ }
+ return nil
+}
+
+// BatchJobExpire represents configuration parameters for a batch expiration
+// job typically supplied in yaml form
+type BatchJobExpire struct {
+ line, col int
+ APIVersion string `yaml:"apiVersion" json:"apiVersion"`
+ Bucket string `yaml:"bucket" json:"bucket"`
+ Prefix BatchJobPrefix `yaml:"prefix" json:"prefix"`
+ NotificationCfg BatchJobNotification `yaml:"notify" json:"notify"`
+ Retry BatchJobRetry `yaml:"retry" json:"retry"`
+ Rules []BatchJobExpireFilter `yaml:"rules" json:"rules"`
+}
+
+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.
+func (r *BatchJobExpire) UnmarshalYAML(val *yaml.Node) error {
+ type expireJob BatchJobExpire
+ var tmp expireJob
+ err := val.Decode(&tmp)
+ if err != nil {
+ return err
+ }
+
+ *r = BatchJobExpire(tmp)
+ r.line, r.col = val.Line, val.Column
+ return nil
+}
+
+// Notify notifies notification endpoint if configured regarding job failure or success.
+func (r BatchJobExpire) Notify(ctx context.Context, body io.Reader) error {
+ if r.NotificationCfg.Endpoint == "" {
+ return nil
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, r.NotificationCfg.Endpoint, body)
+ if err != nil {
+ return err
+ }
+
+ if r.NotificationCfg.Token != "" {
+ req.Header.Set("Authorization", r.NotificationCfg.Token)
+ }
+
+ clnt := http.Client{Transport: getRemoteInstanceTransport()}
+ resp, err := clnt.Do(req)
+ if err != nil {
+ return err
+ }
+
+ xhttp.DrainBody(resp.Body)
+ if resp.StatusCode != http.StatusOK {
+ return errors.New(resp.Status)
+ }
+
+ return nil
+}
+
+// Expire expires object versions which have already matched supplied filter conditions
+func (r *BatchJobExpire) Expire(ctx context.Context, api ObjectLayer, vc *versioning.Versioning, objsToDel []ObjectToDelete) []error {
+ opts := ObjectOptions{
+ PrefixEnabledFn: vc.PrefixEnabled,
+ VersionSuspended: vc.Suspended(),
+ }
+
+ allErrs := make([]error, 0, len(objsToDel))
+
+ for {
+ count := len(objsToDel)
+ if count == 0 {
+ break
+ }
+ if count > maxDeleteList {
+ count = maxDeleteList
+ }
+ _, errs := api.DeleteObjects(ctx, r.Bucket, objsToDel[:count], opts)
+ allErrs = append(allErrs, errs...)
+ // Next batch of deletion
+ objsToDel = objsToDel[count:]
+ }
+
+ return allErrs
+}
+
+const (
+ batchExpireName = "batch-expire.bin"
+ batchExpireFormat = 1
+ batchExpireVersionV1 = 1
+ batchExpireVersion = batchExpireVersionV1
+ batchExpireAPIVersion = "v1"
+ batchExpireJobDefaultRetries = 3
+ batchExpireJobDefaultRetryDelay = 250 * time.Millisecond
+)
+
+type objInfoCache map[string]*ObjectInfo
+
+func newObjInfoCache() objInfoCache {
+ return objInfoCache(make(map[string]*ObjectInfo))
+}
+
+func (oiCache objInfoCache) Add(toDel ObjectToDelete, oi *ObjectInfo) {
+ oiCache[fmt.Sprintf("%s-%s", toDel.ObjectName, toDel.VersionID)] = oi
+}
+
+func (oiCache objInfoCache) Get(toDel ObjectToDelete) (*ObjectInfo, bool) {
+ oi, ok := oiCache[fmt.Sprintf("%s-%s", toDel.ObjectName, toDel.VersionID)]
+ return oi, ok
+}
+
+func batchObjsForDelete(ctx context.Context, r *BatchJobExpire, ri *batchJobInfo, job BatchJobRequest, api ObjectLayer, wk *workers.Workers, expireCh <-chan []expireObjInfo) {
+ vc, _ := globalBucketVersioningSys.Get(r.Bucket)
+ retryAttempts := job.Expire.Retry.Attempts
+ if retryAttempts <= 0 {
+ retryAttempts = batchExpireJobDefaultRetries
+ }
+ delay := job.Expire.Retry.Delay
+ if delay <= 0 {
+ delay = batchExpireJobDefaultRetryDelay
+ }
+
+ var i int
+ for toExpire := range expireCh {
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ }
+ if i > 0 {
+ if wait := globalBatchConfig.ExpirationWait(); wait > 0 {
+ time.Sleep(wait)
+ }
+ }
+ i++
+ wk.Take()
+ go func(toExpire []expireObjInfo) {
+ defer wk.Give()
+
+ toExpireAll := make([]expireObjInfo, 0, len(toExpire))
+ toDel := make([]ObjectToDelete, 0, len(toExpire))
+ oiCache := newObjInfoCache()
+ for _, exp := range toExpire {
+ if exp.ExpireAll {
+ toExpireAll = append(toExpireAll, exp)
+ continue
+ }
+ // Cache ObjectInfo value via pointers for
+ // subsequent use to track objects which
+ // couldn't be deleted.
+ od := ObjectToDelete{
+ ObjectV: ObjectV{
+ ObjectName: exp.Name,
+ VersionID: exp.VersionID,
+ },
+ }
+ toDel = append(toDel, od)
+ oiCache.Add(od, &exp.ObjectInfo)
+ }
+
+ // DeleteObject(deletePrefix: true) to expire all versions of an object
+ for _, exp := range toExpireAll {
+ var success bool
+ for attempts := 1; attempts <= retryAttempts; attempts++ {
+ select {
+ case <-ctx.Done():
+ ri.trackMultipleObjectVersions(exp, success)
+ return
+ default:
+ }
+ stopFn := globalBatchJobsMetrics.trace(batchJobMetricExpire, ri.JobID, attempts)
+ _, err := api.DeleteObject(ctx, exp.Bucket, encodeDirObject(exp.Name), ObjectOptions{
+ DeletePrefix: true,
+ DeletePrefixObject: true, // use prefix delete on exact object (this is an optimization to avoid fan-out calls)
+ })
+ if err != nil {
+ stopFn(exp, err)
+ batchLogIf(ctx, fmt.Errorf("Failed to expire %s/%s due to %v (attempts=%d)", exp.Bucket, exp.Name, err, attempts))
+ } else {
+ stopFn(exp, err)
+ success = true
+ break
+ }
+ }
+ ri.trackMultipleObjectVersions(exp, success)
+ }
+
+ // DeleteMultiple objects
+ toDelCopy := make([]ObjectToDelete, len(toDel))
+ for attempts := 1; attempts <= retryAttempts; attempts++ {
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ }
+
+ stopFn := globalBatchJobsMetrics.trace(batchJobMetricExpire, ri.JobID, attempts)
+ // Copying toDel to select from objects whose
+ // deletion failed
+ copy(toDelCopy, toDel)
+ var failed int
+ errs := r.Expire(ctx, api, vc, toDel)
+ // reslice toDel in preparation for next retry attempt
+ toDel = toDel[:0]
+ for i, err := range errs {
+ if err != nil {
+ stopFn(toDelCopy[i], err)
+ batchLogIf(ctx, fmt.Errorf("Failed to expire %s/%s versionID=%s due to %v (attempts=%d)", ri.Bucket, toDelCopy[i].ObjectName, toDelCopy[i].VersionID,
+ err, attempts))
+ failed++
+ if oi, ok := oiCache.Get(toDelCopy[i]); ok {
+ ri.trackCurrentBucketObject(r.Bucket, *oi, false, attempts)
+ }
+ if attempts != retryAttempts {
+ // retry
+ toDel = append(toDel, toDelCopy[i])
+ }
+ } else {
+ stopFn(toDelCopy[i], nil)
+ if oi, ok := oiCache.Get(toDelCopy[i]); ok {
+ ri.trackCurrentBucketObject(r.Bucket, *oi, true, attempts)
+ }
+ }
+ }
+
+ globalBatchJobsMetrics.save(ri.JobID, ri)
+
+ if failed == 0 {
+ break
+ }
+
+ // Add a delay between retry attempts
+ if attempts < retryAttempts {
+ time.Sleep(delay)
+ }
+ }
+ }(toExpire)
+ }
+}
+
+type expireObjInfo struct {
+ ObjectInfo
+ ExpireAll bool
+ DeleteMarkerCount int64
+}
+
+// Start the batch expiration job, resumes if there was a pending job via "job.ID"
+func (r *BatchJobExpire) Start(ctx context.Context, api ObjectLayer, job BatchJobRequest) error {
+ ri := &batchJobInfo{
+ JobID: job.ID,
+ JobType: string(job.Type()),
+ StartTime: job.Started,
+ }
+ if err := ri.loadOrInit(ctx, api, job); err != nil {
+ return err
+ }
+
+ globalBatchJobsMetrics.save(job.ID, ri)
+ lastObject := ri.Object
+
+ now := time.Now().UTC()
+
+ workerSize, err := strconv.Atoi(env.Get("_MINIO_BATCH_EXPIRATION_WORKERS", strconv.Itoa(runtime.GOMAXPROCS(0)/2)))
+ if err != nil {
+ return err
+ }
+
+ wk, err := workers.New(workerSize)
+ if err != nil {
+ // invalid worker size.
+ return err
+ }
+
+ ctx, cancelCause := context.WithCancelCause(ctx)
+ defer cancelCause(nil)
+
+ results := make(chan itemOrErr[ObjectInfo], workerSize)
+ go func() {
+ prefixes := r.Prefix.F()
+ if len(prefixes) == 0 {
+ prefixes = []string{""}
+ }
+ for _, prefix := range prefixes {
+ prefixResultCh := make(chan itemOrErr[ObjectInfo], workerSize)
+ err := api.Walk(ctx, r.Bucket, prefix, prefixResultCh, WalkOptions{
+ Marker: lastObject,
+ LatestOnly: false, // we need to visit all versions of the object to implement purge: retainVersions
+ VersionsSort: WalkVersionsSortDesc,
+ })
+ if err != nil {
+ cancelCause(err)
+ xioutil.SafeClose(results)
+ return
+ }
+ for result := range prefixResultCh {
+ results <- result
+ }
+ }
+ xioutil.SafeClose(results)
+ }()
+
+ // Goroutine to periodically save batch-expire job's in-memory state
+ saverQuitCh := make(chan struct{})
+ go func() {
+ saveTicker := time.NewTicker(10 * time.Second)
+ defer saveTicker.Stop()
+ quit := false
+ after := time.Minute
+ for !quit {
+ select {
+ case <-saveTicker.C:
+ case <-ctx.Done():
+ quit = true
+ case <-saverQuitCh:
+ quit = true
+ }
+
+ if quit {
+ // save immediately if we are quitting
+ after = 0
+ }
+
+ ctx, cancel := context.WithTimeout(GlobalContext, 30*time.Second) // independent context
+ batchLogIf(ctx, ri.updateAfter(ctx, api, after, job))
+ cancel()
+ }
+ }()
+
+ expireCh := make(chan []expireObjInfo, workerSize)
+ expireDoneCh := make(chan struct{})
+ go func() {
+ defer close(expireDoneCh)
+ batchObjsForDelete(ctx, r, ri, job, api, wk, expireCh)
+ }()
+
+ var (
+ prevObj ObjectInfo
+ matchedFilter BatchJobExpireFilter
+ versionsCount int
+ toDel []expireObjInfo
+ failed bool
+ done bool
+ )
+ deleteMarkerCountMap := map[string]int64{}
+ pushToExpire := func() {
+ // set preObject deleteMarkerCount
+ if len(toDel) > 0 {
+ lastDelIndex := len(toDel) - 1
+ lastDel := toDel[lastDelIndex]
+ if lastDel.ExpireAll {
+ toDel[lastDelIndex].DeleteMarkerCount = deleteMarkerCountMap[lastDel.Name]
+ // delete the key
+ delete(deleteMarkerCountMap, lastDel.Name)
+ }
+ }
+ // send down filtered entries to be deleted using
+ // DeleteObjects method
+ if len(toDel) > 10 { // batch up to 10 objects/versions to be expired simultaneously.
+ xfer := make([]expireObjInfo, len(toDel))
+ copy(xfer, toDel)
+ select {
+ case expireCh <- xfer:
+ toDel = toDel[:0] // resetting toDel
+ case <-ctx.Done():
+ done = true
+ }
+ }
+ }
+ for {
+ select {
+ case result, ok := <-results:
+ if !ok {
+ done = true
+ break
+ }
+ if result.Err != nil {
+ failed = true
+ batchLogIf(ctx, result.Err)
+ continue
+ }
+ if result.Item.DeleteMarker {
+ deleteMarkerCountMap[result.Item.Name]++
+ }
+ // Apply filter to find the matching rule to apply expiry
+ // actions accordingly.
+ // nolint:gocritic
+ if result.Item.IsLatest {
+ var match BatchJobExpireFilter
+ var found bool
+ for _, rule := range r.Rules {
+ if rule.Matches(result.Item, now) {
+ match = rule
+ found = true
+ break
+ }
+ }
+ if !found {
+ continue
+ }
+
+ if prevObj.Name != result.Item.Name {
+ // switch the object
+ pushToExpire()
+ }
+
+ prevObj = result.Item
+ matchedFilter = match
+ versionsCount = 1
+ // Include the latest version
+ if matchedFilter.Purge.RetainVersions == 0 {
+ toDel = append(toDel, expireObjInfo{
+ ObjectInfo: result.Item,
+ ExpireAll: true,
+ })
+ continue
+ }
+ } else if prevObj.Name == result.Item.Name {
+ if matchedFilter.Purge.RetainVersions == 0 {
+ continue // including latest version in toDel suffices, skipping other versions
+ }
+ versionsCount++
+ } else {
+ // switch the object
+ pushToExpire()
+ // a file switched with no LatestVersion, logging it
+ batchLogIf(ctx, fmt.Errorf("skipping object %s, no latest version found", result.Item.Name))
+ continue
+ }
+
+ if versionsCount <= matchedFilter.Purge.RetainVersions {
+ continue // retain versions
+ }
+ toDel = append(toDel, expireObjInfo{
+ ObjectInfo: result.Item,
+ })
+ pushToExpire()
+ case <-ctx.Done():
+ done = true
+ }
+ if done {
+ break
+ }
+ }
+
+ if context.Cause(ctx) != nil {
+ xioutil.SafeClose(expireCh)
+ return context.Cause(ctx)
+ }
+ pushToExpire()
+ // Send any remaining objects downstream
+ if len(toDel) > 0 {
+ select {
+ case <-ctx.Done():
+ case expireCh <- toDel:
+ }
+ }
+ xioutil.SafeClose(expireCh)
+
+ <-expireDoneCh // waits for the expire goroutine to complete
+ wk.Wait() // waits for all expire workers to retire
+
+ ri.Complete = !failed && ri.ObjectsFailed == 0
+ ri.Failed = failed || ri.ObjectsFailed > 0
+ globalBatchJobsMetrics.save(job.ID, ri)
+
+ // Close the saverQuitCh - this also triggers saving in-memory state
+ // immediately one last time before we exit this method.
+ xioutil.SafeClose(saverQuitCh)
+
+ // Notify expire jobs final status to the configured endpoint
+ buf, _ := json.Marshal(ri)
+ if err := r.Notify(context.Background(), bytes.NewReader(buf)); err != nil {
+ batchLogIf(context.Background(), fmt.Errorf("unable to notify %v", err))
+ }
+
+ return nil
+}
+
+//msgp:ignore batchExpireJobError
+type batchExpireJobError struct {
+ Code string
+ Description string
+ HTTPStatusCode int
+}
+
+func (e batchExpireJobError) Error() string {
+ return e.Description
+}
+
+// maxBatchRules maximum number of rules a batch-expiry job supports
+const maxBatchRules = 50
+
+// Validate validates the job definition input
+func (r *BatchJobExpire) Validate(ctx context.Context, job BatchJobRequest, o ObjectLayer) error {
+ if r == nil {
+ return nil
+ }
+
+ if r.APIVersion != batchExpireAPIVersion {
+ return batchExpireJobError{
+ Code: "InvalidArgument",
+ Description: "Unsupported batch expire API version",
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ }
+
+ if r.Bucket == "" {
+ return batchExpireJobError{
+ Code: "InvalidArgument",
+ Description: "Bucket argument missing",
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ }
+
+ if _, err := o.GetBucketInfo(ctx, r.Bucket, BucketOptions{}); err != nil {
+ if isErrBucketNotFound(err) {
+ return batchExpireJobError{
+ Code: "NoSuchSourceBucket",
+ Description: "The specified source bucket does not exist",
+ HTTPStatusCode: http.StatusNotFound,
+ }
+ }
+ return err
+ }
+
+ if len(r.Rules) > maxBatchRules {
+ return batchExpireJobError{
+ Code: "InvalidArgument",
+ Description: "Too many rules. Batch expire job can't have more than 100 rules",
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ }
+
+ for _, rule := range r.Rules {
+ if err := rule.Validate(); err != nil {
+ return batchExpireJobError{
+ Code: "InvalidArgument",
+ Description: fmt.Sprintf("Invalid batch expire rule: %s", err),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ }
+ }
+
+ if err := r.Retry.Validate(); err != nil {
+ return batchExpireJobError{
+ Code: "InvalidArgument",
+ Description: fmt.Sprintf("Invalid batch expire retry configuration: %s", err),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ }
+ return nil
+}
diff --git a/cmd/batch-expire_gen.go b/cmd/batch-expire_gen.go
new file mode 100644
index 00000000..bb2021fc
--- /dev/null
+++ b/cmd/batch-expire_gen.go
@@ -0,0 +1,864 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "time"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchJobExpire) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "APIVersion":
+ z.APIVersion, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "APIVersion")
+ return
+ }
+ case "Bucket":
+ z.Bucket, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "Prefix":
+ err = z.Prefix.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefix")
+ return
+ }
+ case "NotificationCfg":
+ err = z.NotificationCfg.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "NotificationCfg")
+ return
+ }
+ case "Retry":
+ err = z.Retry.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Retry")
+ return
+ }
+ case "Rules":
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Rules")
+ return
+ }
+ if cap(z.Rules) >= int(zb0002) {
+ z.Rules = (z.Rules)[:zb0002]
+ } else {
+ z.Rules = make([]BatchJobExpireFilter, zb0002)
+ }
+ for za0001 := range z.Rules {
+ err = z.Rules[za0001].DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Rules", za0001)
+ return
+ }
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *BatchJobExpire) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 6
+ // write "APIVersion"
+ err = en.Append(0x86, 0xaa, 0x41, 0x50, 0x49, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.APIVersion)
+ if err != nil {
+ err = msgp.WrapError(err, "APIVersion")
+ return
+ }
+ // write "Bucket"
+ err = en.Append(0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Bucket)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ // write "Prefix"
+ err = en.Append(0xa6, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78)
+ if err != nil {
+ return
+ }
+ err = z.Prefix.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefix")
+ return
+ }
+ // write "NotificationCfg"
+ err = en.Append(0xaf, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x66, 0x67)
+ if err != nil {
+ return
+ }
+ err = z.NotificationCfg.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "NotificationCfg")
+ return
+ }
+ // write "Retry"
+ err = en.Append(0xa5, 0x52, 0x65, 0x74, 0x72, 0x79)
+ if err != nil {
+ return
+ }
+ err = z.Retry.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Retry")
+ return
+ }
+ // write "Rules"
+ err = en.Append(0xa5, 0x52, 0x75, 0x6c, 0x65, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(len(z.Rules)))
+ if err != nil {
+ err = msgp.WrapError(err, "Rules")
+ return
+ }
+ for za0001 := range z.Rules {
+ err = z.Rules[za0001].EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Rules", za0001)
+ return
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *BatchJobExpire) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 6
+ // string "APIVersion"
+ o = append(o, 0x86, 0xaa, 0x41, 0x50, 0x49, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e)
+ o = msgp.AppendString(o, z.APIVersion)
+ // string "Bucket"
+ o = append(o, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ o = msgp.AppendString(o, z.Bucket)
+ // string "Prefix"
+ o = append(o, 0xa6, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78)
+ o, err = z.Prefix.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefix")
+ return
+ }
+ // string "NotificationCfg"
+ o = append(o, 0xaf, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x66, 0x67)
+ o, err = z.NotificationCfg.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "NotificationCfg")
+ return
+ }
+ // string "Retry"
+ o = append(o, 0xa5, 0x52, 0x65, 0x74, 0x72, 0x79)
+ o, err = z.Retry.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Retry")
+ return
+ }
+ // string "Rules"
+ o = append(o, 0xa5, 0x52, 0x75, 0x6c, 0x65, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.Rules)))
+ for za0001 := range z.Rules {
+ o, err = z.Rules[za0001].MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Rules", za0001)
+ return
+ }
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BatchJobExpire) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "APIVersion":
+ z.APIVersion, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "APIVersion")
+ return
+ }
+ case "Bucket":
+ z.Bucket, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "Prefix":
+ bts, err = z.Prefix.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefix")
+ return
+ }
+ case "NotificationCfg":
+ bts, err = z.NotificationCfg.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "NotificationCfg")
+ return
+ }
+ case "Retry":
+ bts, err = z.Retry.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Retry")
+ return
+ }
+ case "Rules":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Rules")
+ return
+ }
+ if cap(z.Rules) >= int(zb0002) {
+ z.Rules = (z.Rules)[:zb0002]
+ } else {
+ z.Rules = make([]BatchJobExpireFilter, zb0002)
+ }
+ for za0001 := range z.Rules {
+ bts, err = z.Rules[za0001].UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Rules", za0001)
+ return
+ }
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *BatchJobExpire) Msgsize() (s int) {
+ s = 1 + 11 + msgp.StringPrefixSize + len(z.APIVersion) + 7 + msgp.StringPrefixSize + len(z.Bucket) + 7 + z.Prefix.Msgsize() + 16 + z.NotificationCfg.Msgsize() + 6 + z.Retry.Msgsize() + 6 + msgp.ArrayHeaderSize
+ for za0001 := range z.Rules {
+ s += z.Rules[za0001].Msgsize()
+ }
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchJobExpireFilter) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "OlderThan":
+ err = z.OlderThan.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "OlderThan")
+ return
+ }
+ case "CreatedBefore":
+ if dc.IsNil() {
+ err = dc.ReadNil()
+ if err != nil {
+ err = msgp.WrapError(err, "CreatedBefore")
+ return
+ }
+ z.CreatedBefore = nil
+ } else {
+ if z.CreatedBefore == nil {
+ z.CreatedBefore = new(time.Time)
+ }
+ *z.CreatedBefore, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "CreatedBefore")
+ return
+ }
+ }
+ case "Tags":
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Tags")
+ return
+ }
+ if cap(z.Tags) >= int(zb0002) {
+ z.Tags = (z.Tags)[:zb0002]
+ } else {
+ z.Tags = make([]BatchJobKV, zb0002)
+ }
+ for za0001 := range z.Tags {
+ err = z.Tags[za0001].DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Tags", za0001)
+ return
+ }
+ }
+ case "Metadata":
+ var zb0003 uint32
+ zb0003, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Metadata")
+ return
+ }
+ if cap(z.Metadata) >= int(zb0003) {
+ z.Metadata = (z.Metadata)[:zb0003]
+ } else {
+ z.Metadata = make([]BatchJobKV, zb0003)
+ }
+ for za0002 := range z.Metadata {
+ err = z.Metadata[za0002].DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Metadata", za0002)
+ return
+ }
+ }
+ case "Size":
+ err = z.Size.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ case "Type":
+ z.Type, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Type")
+ return
+ }
+ case "Name":
+ z.Name, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Name")
+ return
+ }
+ case "Purge":
+ var zb0004 uint32
+ zb0004, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Purge")
+ return
+ }
+ for zb0004 > 0 {
+ zb0004--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err, "Purge")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "RetainVersions":
+ z.Purge.RetainVersions, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "Purge", "RetainVersions")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err, "Purge")
+ return
+ }
+ }
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *BatchJobExpireFilter) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 8
+ // write "OlderThan"
+ err = en.Append(0x88, 0xa9, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e)
+ if err != nil {
+ return
+ }
+ err = z.OlderThan.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "OlderThan")
+ return
+ }
+ // write "CreatedBefore"
+ err = en.Append(0xad, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65)
+ if err != nil {
+ return
+ }
+ if z.CreatedBefore == nil {
+ err = en.WriteNil()
+ if err != nil {
+ return
+ }
+ } else {
+ err = en.WriteTime(*z.CreatedBefore)
+ if err != nil {
+ err = msgp.WrapError(err, "CreatedBefore")
+ return
+ }
+ }
+ // write "Tags"
+ err = en.Append(0xa4, 0x54, 0x61, 0x67, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(len(z.Tags)))
+ if err != nil {
+ err = msgp.WrapError(err, "Tags")
+ return
+ }
+ for za0001 := range z.Tags {
+ err = z.Tags[za0001].EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Tags", za0001)
+ return
+ }
+ }
+ // write "Metadata"
+ err = en.Append(0xa8, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(len(z.Metadata)))
+ if err != nil {
+ err = msgp.WrapError(err, "Metadata")
+ return
+ }
+ for za0002 := range z.Metadata {
+ err = z.Metadata[za0002].EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Metadata", za0002)
+ return
+ }
+ }
+ // write "Size"
+ err = en.Append(0xa4, 0x53, 0x69, 0x7a, 0x65)
+ if err != nil {
+ return
+ }
+ err = z.Size.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ // write "Type"
+ err = en.Append(0xa4, 0x54, 0x79, 0x70, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Type)
+ if err != nil {
+ err = msgp.WrapError(err, "Type")
+ return
+ }
+ // write "Name"
+ err = en.Append(0xa4, 0x4e, 0x61, 0x6d, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Name)
+ if err != nil {
+ err = msgp.WrapError(err, "Name")
+ return
+ }
+ // write "Purge"
+ err = en.Append(0xa5, 0x50, 0x75, 0x72, 0x67, 0x65)
+ if err != nil {
+ return
+ }
+ // map header, size 1
+ // write "RetainVersions"
+ err = en.Append(0x81, 0xae, 0x52, 0x65, 0x74, 0x61, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.Purge.RetainVersions)
+ if err != nil {
+ err = msgp.WrapError(err, "Purge", "RetainVersions")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *BatchJobExpireFilter) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 8
+ // string "OlderThan"
+ o = append(o, 0x88, 0xa9, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e)
+ o, err = z.OlderThan.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "OlderThan")
+ return
+ }
+ // string "CreatedBefore"
+ o = append(o, 0xad, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65)
+ if z.CreatedBefore == nil {
+ o = msgp.AppendNil(o)
+ } else {
+ o = msgp.AppendTime(o, *z.CreatedBefore)
+ }
+ // string "Tags"
+ o = append(o, 0xa4, 0x54, 0x61, 0x67, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.Tags)))
+ for za0001 := range z.Tags {
+ o, err = z.Tags[za0001].MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Tags", za0001)
+ return
+ }
+ }
+ // string "Metadata"
+ o = append(o, 0xa8, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.Metadata)))
+ for za0002 := range z.Metadata {
+ o, err = z.Metadata[za0002].MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Metadata", za0002)
+ return
+ }
+ }
+ // string "Size"
+ o = append(o, 0xa4, 0x53, 0x69, 0x7a, 0x65)
+ o, err = z.Size.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ // string "Type"
+ o = append(o, 0xa4, 0x54, 0x79, 0x70, 0x65)
+ o = msgp.AppendString(o, z.Type)
+ // string "Name"
+ o = append(o, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
+ o = msgp.AppendString(o, z.Name)
+ // string "Purge"
+ o = append(o, 0xa5, 0x50, 0x75, 0x72, 0x67, 0x65)
+ // map header, size 1
+ // string "RetainVersions"
+ o = append(o, 0x81, 0xae, 0x52, 0x65, 0x74, 0x61, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73)
+ o = msgp.AppendInt(o, z.Purge.RetainVersions)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BatchJobExpireFilter) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "OlderThan":
+ bts, err = z.OlderThan.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "OlderThan")
+ return
+ }
+ case "CreatedBefore":
+ if msgp.IsNil(bts) {
+ bts, err = msgp.ReadNilBytes(bts)
+ if err != nil {
+ return
+ }
+ z.CreatedBefore = nil
+ } else {
+ if z.CreatedBefore == nil {
+ z.CreatedBefore = new(time.Time)
+ }
+ *z.CreatedBefore, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "CreatedBefore")
+ return
+ }
+ }
+ case "Tags":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Tags")
+ return
+ }
+ if cap(z.Tags) >= int(zb0002) {
+ z.Tags = (z.Tags)[:zb0002]
+ } else {
+ z.Tags = make([]BatchJobKV, zb0002)
+ }
+ for za0001 := range z.Tags {
+ bts, err = z.Tags[za0001].UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Tags", za0001)
+ return
+ }
+ }
+ case "Metadata":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Metadata")
+ return
+ }
+ if cap(z.Metadata) >= int(zb0003) {
+ z.Metadata = (z.Metadata)[:zb0003]
+ } else {
+ z.Metadata = make([]BatchJobKV, zb0003)
+ }
+ for za0002 := range z.Metadata {
+ bts, err = z.Metadata[za0002].UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Metadata", za0002)
+ return
+ }
+ }
+ case "Size":
+ bts, err = z.Size.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ case "Type":
+ z.Type, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Type")
+ return
+ }
+ case "Name":
+ z.Name, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Name")
+ return
+ }
+ case "Purge":
+ var zb0004 uint32
+ zb0004, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Purge")
+ return
+ }
+ for zb0004 > 0 {
+ zb0004--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Purge")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "RetainVersions":
+ z.Purge.RetainVersions, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Purge", "RetainVersions")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Purge")
+ return
+ }
+ }
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *BatchJobExpireFilter) Msgsize() (s int) {
+ s = 1 + 10 + z.OlderThan.Msgsize() + 14
+ if z.CreatedBefore == nil {
+ s += msgp.NilSize
+ } else {
+ s += msgp.TimeSize
+ }
+ s += 5 + msgp.ArrayHeaderSize
+ for za0001 := range z.Tags {
+ s += z.Tags[za0001].Msgsize()
+ }
+ s += 9 + msgp.ArrayHeaderSize
+ for za0002 := range z.Metadata {
+ s += z.Metadata[za0002].Msgsize()
+ }
+ s += 5 + z.Size.Msgsize() + 5 + msgp.StringPrefixSize + len(z.Type) + 5 + msgp.StringPrefixSize + len(z.Name) + 6 + 1 + 15 + msgp.IntSize
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchJobExpirePurge) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "RetainVersions":
+ z.RetainVersions, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "RetainVersions")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z BatchJobExpirePurge) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 1
+ // write "RetainVersions"
+ err = en.Append(0x81, 0xae, 0x52, 0x65, 0x74, 0x61, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.RetainVersions)
+ if err != nil {
+ err = msgp.WrapError(err, "RetainVersions")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z BatchJobExpirePurge) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 1
+ // string "RetainVersions"
+ o = append(o, 0x81, 0xae, 0x52, 0x65, 0x74, 0x61, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73)
+ o = msgp.AppendInt(o, z.RetainVersions)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BatchJobExpirePurge) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "RetainVersions":
+ z.RetainVersions, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "RetainVersions")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z BatchJobExpirePurge) Msgsize() (s int) {
+ s = 1 + 15 + msgp.IntSize
+ return
+}
diff --git a/cmd/batch-expire_gen_test.go b/cmd/batch-expire_gen_test.go
new file mode 100644
index 00000000..d30cc337
--- /dev/null
+++ b/cmd/batch-expire_gen_test.go
@@ -0,0 +1,349 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+func TestMarshalUnmarshalBatchJobExpire(t *testing.T) {
+ v := BatchJobExpire{}
+ 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 BenchmarkMarshalMsgBatchJobExpire(b *testing.B) {
+ v := BatchJobExpire{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBatchJobExpire(b *testing.B) {
+ v := BatchJobExpire{}
+ 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 BenchmarkUnmarshalBatchJobExpire(b *testing.B) {
+ v := BatchJobExpire{}
+ 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 TestEncodeDecodeBatchJobExpire(t *testing.T) {
+ v := BatchJobExpire{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBatchJobExpire Msgsize() is inaccurate")
+ }
+
+ vn := BatchJobExpire{}
+ 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 BenchmarkEncodeBatchJobExpire(b *testing.B) {
+ v := BatchJobExpire{}
+ 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 BenchmarkDecodeBatchJobExpire(b *testing.B) {
+ v := BatchJobExpire{}
+ 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 TestMarshalUnmarshalBatchJobExpireFilter(t *testing.T) {
+ v := BatchJobExpireFilter{}
+ 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 BenchmarkMarshalMsgBatchJobExpireFilter(b *testing.B) {
+ v := BatchJobExpireFilter{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBatchJobExpireFilter(b *testing.B) {
+ v := BatchJobExpireFilter{}
+ 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 BenchmarkUnmarshalBatchJobExpireFilter(b *testing.B) {
+ v := BatchJobExpireFilter{}
+ 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 TestEncodeDecodeBatchJobExpireFilter(t *testing.T) {
+ v := BatchJobExpireFilter{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBatchJobExpireFilter Msgsize() is inaccurate")
+ }
+
+ vn := BatchJobExpireFilter{}
+ 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 BenchmarkEncodeBatchJobExpireFilter(b *testing.B) {
+ v := BatchJobExpireFilter{}
+ 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 BenchmarkDecodeBatchJobExpireFilter(b *testing.B) {
+ v := BatchJobExpireFilter{}
+ 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 TestMarshalUnmarshalBatchJobExpirePurge(t *testing.T) {
+ v := BatchJobExpirePurge{}
+ 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 BenchmarkMarshalMsgBatchJobExpirePurge(b *testing.B) {
+ v := BatchJobExpirePurge{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBatchJobExpirePurge(b *testing.B) {
+ v := BatchJobExpirePurge{}
+ 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 BenchmarkUnmarshalBatchJobExpirePurge(b *testing.B) {
+ v := BatchJobExpirePurge{}
+ 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 TestEncodeDecodeBatchJobExpirePurge(t *testing.T) {
+ v := BatchJobExpirePurge{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBatchJobExpirePurge Msgsize() is inaccurate")
+ }
+
+ vn := BatchJobExpirePurge{}
+ 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 BenchmarkEncodeBatchJobExpirePurge(b *testing.B) {
+ v := BatchJobExpirePurge{}
+ 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 BenchmarkDecodeBatchJobExpirePurge(b *testing.B) {
+ v := BatchJobExpirePurge{}
+ 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)
+ }
+ }
+}
diff --git a/cmd/batch-expire_test.go b/cmd/batch-expire_test.go
new file mode 100644
index 00000000..18f7150d
--- /dev/null
+++ b/cmd/batch-expire_test.go
@@ -0,0 +1,125 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "slices"
+ "testing"
+
+ "gopkg.in/yaml.v3"
+)
+
+func TestParseBatchJobExpire(t *testing.T) {
+ expireYaml := `
+expire: # Expire objects that match a condition
+ apiVersion: v1
+ bucket: mybucket # Bucket where this batch job will expire matching objects from
+ prefix: myprefix # (Optional) Prefix under which this job will expire objects matching the rules below.
+ rules:
+ - type: object # regular objects with zero or more older versions
+ name: NAME # match object names that satisfy the wildcard expression.
+ olderThan: 7d10h # match objects older than this value
+ createdBefore: "2006-01-02T15:04:05.00Z" # match objects created before "date"
+ tags:
+ - key: name
+ value: pick* # match objects with tag 'name', all values starting with 'pick'
+ metadata:
+ - key: content-type
+ value: image/* # match objects with 'content-type', all values starting with 'image/'
+ size:
+ lessThan: "10MiB" # match objects with size less than this value (e.g. 10MiB)
+ greaterThan: 1MiB # match objects with size greater than this value (e.g. 1MiB)
+ purge:
+ # retainVersions: 0 # (default) delete all versions of the object. This option is the fastest.
+ # retainVersions: 5 # keep the latest 5 versions of the object.
+
+ - type: deleted # objects with delete marker as their latest version
+ name: NAME # match object names that satisfy the wildcard expression.
+ olderThan: 10h # match objects older than this value (e.g. 7d10h31s)
+ createdBefore: "2006-01-02T15:04:05.00Z" # match objects created before "date"
+ purge:
+ # retainVersions: 0 # (default) delete all versions of the object. This option is the fastest.
+ # retainVersions: 5 # keep the latest 5 versions of the object including delete markers.
+
+ notify:
+ endpoint: https://notify.endpoint # notification endpoint to receive job completion status
+ token: Bearer xxxxx # optional authentication token for the notification endpoint
+
+ retry:
+ attempts: 10 # number of retries for the job before giving up
+ delay: 500ms # least amount of delay between each retry
+`
+ var job BatchJobRequest
+ err := yaml.Unmarshal([]byte(expireYaml), &job)
+ if err != nil {
+ t.Fatal("Failed to parse batch-job-expire yaml", err)
+ }
+ if !slices.Equal(job.Expire.Prefix.F(), []string{"myprefix"}) {
+ t.Fatal("Failed to parse batch-job-expire yaml")
+ }
+
+ multiPrefixExpireYaml := `
+expire: # Expire objects that match a condition
+ apiVersion: v1
+ bucket: mybucket # Bucket where this batch job will expire matching objects from
+ prefix: # (Optional) Prefix under which this job will expire objects matching the rules below.
+ - myprefix
+ - myprefix1
+ rules:
+ - type: object # regular objects with zero or more older versions
+ name: NAME # match object names that satisfy the wildcard expression.
+ olderThan: 7d10h # match objects older than this value
+ createdBefore: "2006-01-02T15:04:05.00Z" # match objects created before "date"
+ tags:
+ - key: name
+ value: pick* # match objects with tag 'name', all values starting with 'pick'
+ metadata:
+ - key: content-type
+ value: image/* # match objects with 'content-type', all values starting with 'image/'
+ size:
+ lessThan: "10MiB" # match objects with size less than this value (e.g. 10MiB)
+ greaterThan: 1MiB # match objects with size greater than this value (e.g. 1MiB)
+ purge:
+ # retainVersions: 0 # (default) delete all versions of the object. This option is the fastest.
+ # retainVersions: 5 # keep the latest 5 versions of the object.
+
+ - type: deleted # objects with delete marker as their latest version
+ name: NAME # match object names that satisfy the wildcard expression.
+ olderThan: 10h # match objects older than this value (e.g. 7d10h31s)
+ createdBefore: "2006-01-02T15:04:05.00Z" # match objects created before "date"
+ purge:
+ # retainVersions: 0 # (default) delete all versions of the object. This option is the fastest.
+ # retainVersions: 5 # keep the latest 5 versions of the object including delete markers.
+
+ notify:
+ endpoint: https://notify.endpoint # notification endpoint to receive job completion status
+ token: Bearer xxxxx # optional authentication token for the notification endpoint
+
+ retry:
+ attempts: 10 # number of retries for the job before giving up
+ delay: 500ms # least amount of delay between each retry
+`
+ var multiPrefixJob BatchJobRequest
+ err = yaml.Unmarshal([]byte(multiPrefixExpireYaml), &multiPrefixJob)
+ if err != nil {
+ t.Fatal("Failed to parse batch-job-expire yaml", err)
+ }
+ if !slices.Equal(multiPrefixJob.Expire.Prefix.F(), []string{"myprefix", "myprefix1"}) {
+ t.Fatal("Failed to parse batch-job-expire yaml")
+ }
+}
diff --git a/cmd/batch-handlers.go b/cmd/batch-handlers.go
new file mode 100644
index 00000000..484882e9
--- /dev/null
+++ b/cmd/batch-handlers.go
@@ -0,0 +1,2356 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "encoding/binary"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "maps"
+ "math/rand"
+ "net/http"
+ "net/url"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/dustin/go-humanize"
+ "github.com/lithammer/shortuuid/v4"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio-go/v7"
+ "github.com/minio/minio-go/v7/pkg/credentials"
+ "github.com/minio/minio-go/v7/pkg/encrypt"
+ "github.com/minio/minio-go/v7/pkg/tags"
+ "github.com/minio/minio/internal/config/batch"
+ "github.com/minio/minio/internal/crypto"
+ "github.com/minio/minio/internal/hash"
+ xhttp "github.com/minio/minio/internal/http"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/pkg/v3/console"
+ "github.com/minio/pkg/v3/env"
+ "github.com/minio/pkg/v3/policy"
+ "github.com/minio/pkg/v3/workers"
+ "gopkg.in/yaml.v3"
+)
+
+var globalBatchConfig batch.Config
+
+const (
+ // Keep the completed/failed job stats 3 days before removing it
+ oldJobsExpiration = 3 * 24 * time.Hour
+
+ redactedText = "**REDACTED**"
+)
+
+// BatchJobRequest this is an internal data structure not for external consumption.
+type BatchJobRequest struct {
+ ID string `yaml:"-" json:"name"`
+ User string `yaml:"-" json:"user"`
+ Started time.Time `yaml:"-" json:"started"`
+ Replicate *BatchJobReplicateV1 `yaml:"replicate" json:"replicate"`
+ KeyRotate *BatchJobKeyRotateV1 `yaml:"keyrotate" json:"keyrotate"`
+ Expire *BatchJobExpire `yaml:"expire" json:"expire"`
+ 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 {
+ if endpoint == "" {
+ return nil
+ }
+
+ buf, err := json.Marshal(ri)
+ if err != nil {
+ return err
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(buf))
+ if err != nil {
+ return err
+ }
+
+ if token != "" {
+ req.Header.Set("Authorization", token)
+ }
+ req.Header.Set("Content-Type", "application/json")
+
+ clnt := http.Client{Transport: getRemoteInstanceTransport()}
+ resp, err := clnt.Do(req)
+ if err != nil {
+ return err
+ }
+
+ xhttp.DrainBody(resp.Body)
+ if resp.StatusCode != http.StatusOK {
+ return errors.New(resp.Status)
+ }
+
+ return nil
+}
+
+// Notify notifies notification endpoint if configured regarding job failure or success.
+func (r BatchJobReplicateV1) Notify(ctx context.Context, ri *batchJobInfo) error {
+ return notifyEndpoint(ctx, ri, r.Flags.Notify.Endpoint, r.Flags.Notify.Token)
+}
+
+// ReplicateFromSource - this is not implemented yet where source is 'remote' and target is local.
+func (r *BatchJobReplicateV1) ReplicateFromSource(ctx context.Context, api ObjectLayer, core *minio.Core, srcObjInfo ObjectInfo, retry bool) error {
+ srcBucket := r.Source.Bucket
+ tgtBucket := r.Target.Bucket
+ srcObject := srcObjInfo.Name
+ tgtObject := srcObjInfo.Name
+ if r.Target.Prefix != "" {
+ tgtObject = pathJoin(r.Target.Prefix, srcObjInfo.Name)
+ }
+
+ versionID := srcObjInfo.VersionID
+ if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 {
+ versionID = ""
+ }
+ if srcObjInfo.DeleteMarker {
+ _, err := api.DeleteObject(ctx, tgtBucket, tgtObject, ObjectOptions{
+ VersionID: versionID,
+ // Since we are preserving a delete marker, we have to make sure this is always true.
+ // regardless of the current configuration of the bucket we must preserve all versions
+ // on the pool being batch replicated from source.
+ Versioned: true,
+ MTime: srcObjInfo.ModTime,
+ DeleteMarker: srcObjInfo.DeleteMarker,
+ ReplicationRequest: true,
+ })
+ return err
+ }
+
+ opts := ObjectOptions{
+ VersionID: srcObjInfo.VersionID,
+ MTime: srcObjInfo.ModTime,
+ PreserveETag: srcObjInfo.ETag,
+ UserDefined: srcObjInfo.UserDefined,
+ }
+ if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 {
+ opts.VersionID = ""
+ }
+ if crypto.S3.IsEncrypted(srcObjInfo.UserDefined) {
+ opts.ServerSideEncryption = encrypt.NewSSE()
+ }
+ slc := strings.Split(srcObjInfo.ETag, "-")
+ if len(slc) == 2 {
+ partsCount, err := strconv.Atoi(slc[1])
+ if err != nil {
+ return err
+ }
+ return r.copyWithMultipartfromSource(ctx, api, core, srcObjInfo, opts, partsCount)
+ }
+ gopts := minio.GetObjectOptions{
+ VersionID: srcObjInfo.VersionID,
+ }
+ if err := gopts.SetMatchETag(srcObjInfo.ETag); err != nil {
+ return err
+ }
+ rd, objInfo, _, err := core.GetObject(ctx, srcBucket, srcObject, gopts)
+ if err != nil {
+ return ErrorRespToObjectError(err, srcBucket, srcObject, srcObjInfo.VersionID)
+ }
+ defer rd.Close()
+
+ hr, err := hash.NewReader(ctx, rd, objInfo.Size, "", "", objInfo.Size)
+ if err != nil {
+ return err
+ }
+ pReader := NewPutObjReader(hr)
+ _, err = api.PutObject(ctx, tgtBucket, tgtObject, pReader, opts)
+ return err
+}
+
+func (r *BatchJobReplicateV1) copyWithMultipartfromSource(ctx context.Context, api ObjectLayer, c *minio.Core, srcObjInfo ObjectInfo, opts ObjectOptions, partsCount int) (err error) {
+ srcBucket := r.Source.Bucket
+ tgtBucket := r.Target.Bucket
+ srcObject := srcObjInfo.Name
+ tgtObject := srcObjInfo.Name
+ if r.Target.Prefix != "" {
+ tgtObject = pathJoin(r.Target.Prefix, srcObjInfo.Name)
+ }
+ if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 {
+ opts.VersionID = ""
+ }
+ var uploadedParts []CompletePart
+ res, err := api.NewMultipartUpload(context.Background(), tgtBucket, tgtObject, opts)
+ if err != nil {
+ return err
+ }
+
+ defer func() {
+ if err != nil {
+ // block and abort remote upload upon failure.
+ attempts := 1
+ for attempts <= 3 {
+ aerr := api.AbortMultipartUpload(ctx, tgtBucket, tgtObject, res.UploadID, ObjectOptions{})
+ if aerr == nil {
+ return
+ }
+ batchLogIf(ctx,
+ fmt.Errorf("trying %s: Unable to cleanup failed multipart replication %s on remote %s/%s: %w - this may consume space on remote cluster",
+ humanize.Ordinal(attempts), res.UploadID, tgtBucket, tgtObject, aerr))
+ attempts++
+ time.Sleep(time.Second)
+ }
+ }
+ }()
+
+ var (
+ hr *hash.Reader
+ pInfo PartInfo
+ )
+
+ for i := range partsCount {
+ gopts := minio.GetObjectOptions{
+ VersionID: srcObjInfo.VersionID,
+ PartNumber: i + 1,
+ }
+ if err := gopts.SetMatchETag(srcObjInfo.ETag); err != nil {
+ return err
+ }
+ rd, objInfo, _, err := c.GetObject(ctx, srcBucket, srcObject, gopts)
+ if err != nil {
+ return ErrorRespToObjectError(err, srcBucket, srcObject, srcObjInfo.VersionID)
+ }
+ defer rd.Close()
+
+ hr, err = hash.NewReader(ctx, io.LimitReader(rd, objInfo.Size), objInfo.Size, "", "", objInfo.Size)
+ if err != nil {
+ return err
+ }
+ pReader := NewPutObjReader(hr)
+ opts.PreserveETag = ""
+ pInfo, err = api.PutObjectPart(ctx, tgtBucket, tgtObject, res.UploadID, i+1, pReader, opts)
+ if err != nil {
+ return err
+ }
+ if pInfo.Size != objInfo.Size {
+ return fmt.Errorf("Part size mismatch: got %d, want %d", pInfo.Size, objInfo.Size)
+ }
+ uploadedParts = append(uploadedParts, CompletePart{
+ PartNumber: pInfo.PartNumber,
+ ETag: pInfo.ETag,
+ })
+ }
+ _, err = api.CompleteMultipartUpload(ctx, tgtBucket, tgtObject, res.UploadID, uploadedParts, opts)
+ return err
+}
+
+// StartFromSource starts the batch replication job from remote source, resumes if there was a pending job via "job.ID"
+func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLayer, job BatchJobRequest) error {
+ ri := &batchJobInfo{
+ JobID: job.ID,
+ JobType: string(job.Type()),
+ StartTime: job.Started,
+ }
+ if err := ri.loadOrInit(ctx, api, job); err != nil {
+ return err
+ }
+ if ri.Complete {
+ return nil
+ }
+ globalBatchJobsMetrics.save(job.ID, ri)
+
+ retryAttempts := job.Replicate.Flags.Retry.Attempts
+ if retryAttempts <= 0 {
+ retryAttempts = batchReplJobDefaultRetries
+ }
+ delay := job.Replicate.Flags.Retry.Delay
+ if delay <= 0 {
+ delay = batchReplJobDefaultRetryDelay
+ }
+ rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
+
+ hasTags := len(r.Flags.Filter.Tags) != 0
+ isMetadata := len(r.Flags.Filter.Metadata) != 0
+ isStorageClassOnly := len(r.Flags.Filter.Metadata) == 1 && strings.EqualFold(r.Flags.Filter.Metadata[0].Key, xhttp.AmzStorageClass)
+
+ skip := func(oi ObjectInfo) (ok bool) {
+ if r.Flags.Filter.OlderThan > 0 && time.Since(oi.ModTime) < r.Flags.Filter.OlderThan.D() {
+ // skip all objects that are newer than specified older duration
+ return true
+ }
+
+ if r.Flags.Filter.NewerThan > 0 && time.Since(oi.ModTime) >= r.Flags.Filter.NewerThan.D() {
+ // skip all objects that are older than specified newer duration
+ return true
+ }
+
+ if !r.Flags.Filter.CreatedAfter.IsZero() && r.Flags.Filter.CreatedAfter.After(oi.ModTime) {
+ // skip all objects that are created before the specified time.
+ return true
+ }
+
+ if !r.Flags.Filter.CreatedBefore.IsZero() && r.Flags.Filter.CreatedBefore.Before(oi.ModTime) {
+ // skip all objects that are created after the specified time.
+ return true
+ }
+
+ if hasTags {
+ // Only parse object tags if tags filter is specified.
+ tagMap := map[string]string{}
+ tagStr := oi.UserTags
+ if len(tagStr) != 0 {
+ t, err := tags.ParseObjectTags(tagStr)
+ if err != nil {
+ return false
+ }
+ tagMap = t.ToMap()
+ }
+ for _, kv := range r.Flags.Filter.Tags {
+ for t, v := range tagMap {
+ if kv.Match(BatchJobKV{Key: t, Value: v}) {
+ return true
+ }
+ }
+ }
+
+ // None of the provided tags filter match skip the object
+ return false
+ }
+
+ for _, kv := range r.Flags.Filter.Metadata {
+ for k, v := range oi.UserDefined {
+ if !stringsHasPrefixFold(k, "x-amz-meta-") && !isStandardHeader(k) {
+ continue
+ }
+ // We only need to match x-amz-meta or standardHeaders
+ if kv.Match(BatchJobKV{Key: k, Value: v}) {
+ return true
+ }
+ }
+ }
+
+ // None of the provided filters match
+ return false
+ }
+
+ u, err := url.Parse(r.Source.Endpoint)
+ if err != nil {
+ return err
+ }
+
+ cred := r.Source.Creds
+
+ c, err := minio.New(u.Host, &minio.Options{
+ Creds: credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
+ Secure: u.Scheme == "https",
+ Transport: getRemoteInstanceTransport(),
+ BucketLookup: lookupStyle(r.Source.Path),
+ })
+ if err != nil {
+ return err
+ }
+
+ c.SetAppInfo("minio-"+batchJobPrefix, r.APIVersion+" "+job.ID)
+ core := &minio.Core{Client: c}
+
+ workerSize, err := strconv.Atoi(env.Get("_MINIO_BATCH_REPLICATION_WORKERS", strconv.Itoa(runtime.GOMAXPROCS(0)/2)))
+ if err != nil {
+ return err
+ }
+
+ wk, err := workers.New(workerSize)
+ if err != nil {
+ // invalid worker size.
+ return err
+ }
+
+ retry := false
+ for attempts := 1; attempts <= retryAttempts; attempts++ {
+ attempts := attempts
+ // one of source/target is s3, skip delete marker and all versions under the same object name.
+ s3Type := r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3
+ minioSrc := r.Source.Type == BatchJobReplicateResourceMinIO
+ ctx, cancel := context.WithCancel(ctx)
+
+ objInfoCh := make(chan minio.ObjectInfo, 1)
+ go func() {
+ prefixes := r.Source.Prefix.F()
+ if len(prefixes) == 0 {
+ prefixes = []string{""}
+ }
+ for _, prefix := range prefixes {
+ prefixObjInfoCh := c.ListObjects(ctx, r.Source.Bucket, minio.ListObjectsOptions{
+ Prefix: prefix,
+ WithVersions: minioSrc,
+ Recursive: true,
+ WithMetadata: true,
+ })
+ for obj := range prefixObjInfoCh {
+ objInfoCh <- obj
+ }
+ }
+ xioutil.SafeClose(objInfoCh)
+ }()
+
+ prevObj := ""
+ skipReplicate := false
+
+ for obj := range objInfoCh {
+ oi := toObjectInfo(r.Source.Bucket, obj.Key, obj)
+ if !minioSrc {
+ // Check if metadata filter was requested and it is expected to have
+ // all user metadata or just storageClass. If its only storageClass
+ // List() already returns relevant information for filter to be applied.
+ if isMetadata && !isStorageClassOnly {
+ oi2, err := c.StatObject(ctx, r.Source.Bucket, obj.Key, minio.StatObjectOptions{})
+ if err == nil {
+ oi = toObjectInfo(r.Source.Bucket, obj.Key, oi2)
+ } else {
+ if !isErrMethodNotAllowed(ErrorRespToObjectError(err, r.Source.Bucket, obj.Key)) &&
+ !isErrObjectNotFound(ErrorRespToObjectError(err, r.Source.Bucket, obj.Key)) {
+ batchLogIf(ctx, err)
+ }
+ continue
+ }
+ }
+ if hasTags {
+ tags, err := c.GetObjectTagging(ctx, r.Source.Bucket, obj.Key, minio.GetObjectTaggingOptions{})
+ if err == nil {
+ oi.UserTags = tags.String()
+ } else {
+ if !isErrMethodNotAllowed(ErrorRespToObjectError(err, r.Source.Bucket, obj.Key)) &&
+ !isErrObjectNotFound(ErrorRespToObjectError(err, r.Source.Bucket, obj.Key)) {
+ batchLogIf(ctx, err)
+ }
+ continue
+ }
+ }
+ }
+ if skip(oi) {
+ continue
+ }
+ if obj.Key != prevObj {
+ prevObj = obj.Key
+ // skip replication of delete marker and all versions under the same object name if one of source or target is s3.
+ skipReplicate = obj.IsDeleteMarker && s3Type
+ }
+ if skipReplicate {
+ continue
+ }
+
+ wk.Take()
+ go func() {
+ defer wk.Give()
+ stopFn := globalBatchJobsMetrics.trace(batchJobMetricReplication, job.ID, attempts)
+ success := true
+ if err := r.ReplicateFromSource(ctx, api, core, oi, retry); err != nil {
+ // object must be deleted concurrently, allow these failures but do not count them
+ if isErrVersionNotFound(err) || isErrObjectNotFound(err) {
+ return
+ }
+ stopFn(oi, err)
+ batchLogIf(ctx, err)
+ success = false
+ } else {
+ stopFn(oi, nil)
+ }
+ ri.trackCurrentBucketObject(r.Target.Bucket, oi, success, attempts)
+ globalBatchJobsMetrics.save(job.ID, ri)
+ // persist in-memory state to disk after every 10secs.
+ batchLogIf(ctx, ri.updateAfter(ctx, api, 10*time.Second, job))
+
+ if wait := globalBatchConfig.ReplicationWait(); wait > 0 {
+ time.Sleep(wait)
+ }
+ }()
+ }
+ wk.Wait()
+
+ ri.RetryAttempts = attempts
+ ri.Complete = ri.ObjectsFailed == 0
+ ri.Failed = ri.ObjectsFailed > 0
+
+ globalBatchJobsMetrics.save(job.ID, ri)
+ // persist in-memory state to disk.
+ batchLogIf(ctx, ri.updateAfter(ctx, api, 0, job))
+
+ if err := r.Notify(ctx, ri); err != nil {
+ batchLogIf(ctx, fmt.Errorf("unable to notify %v", err))
+ }
+
+ cancel()
+ if ri.Failed {
+ ri.ObjectsFailed = 0
+ ri.Bucket = ""
+ ri.Object = ""
+ ri.Objects = 0
+ ri.BytesFailed = 0
+ ri.BytesTransferred = 0
+ retry = true // indicate we are retrying..
+ time.Sleep(delay + time.Duration(rnd.Float64()*float64(delay)))
+ continue
+ }
+
+ break
+ }
+
+ return nil
+}
+
+// toObjectInfo converts minio.ObjectInfo to ObjectInfo
+func toObjectInfo(bucket, object string, objInfo minio.ObjectInfo) ObjectInfo {
+ tags, _ := tags.MapToObjectTags(objInfo.UserTags)
+ oi := ObjectInfo{
+ Bucket: bucket,
+ Name: object,
+ ModTime: objInfo.LastModified,
+ Size: objInfo.Size,
+ ETag: objInfo.ETag,
+ VersionID: objInfo.VersionID,
+ IsLatest: objInfo.IsLatest,
+ DeleteMarker: objInfo.IsDeleteMarker,
+ ContentType: objInfo.ContentType,
+ Expires: objInfo.Expires,
+ StorageClass: objInfo.StorageClass,
+ ReplicationStatusInternal: objInfo.ReplicationStatus,
+ UserTags: tags.String(),
+ }
+
+ oi.UserDefined = make(map[string]string, len(objInfo.Metadata))
+ for k, v := range objInfo.Metadata {
+ oi.UserDefined[k] = v[0]
+ }
+
+ ce, ok := oi.UserDefined[xhttp.ContentEncoding]
+ if !ok {
+ ce, ok = oi.UserDefined[strings.ToLower(xhttp.ContentEncoding)]
+ }
+ if ok {
+ oi.ContentEncoding = ce
+ }
+
+ _, ok = oi.UserDefined[xhttp.AmzStorageClass]
+ if !ok {
+ oi.UserDefined[xhttp.AmzStorageClass] = objInfo.StorageClass
+ }
+
+ maps.Copy(oi.UserDefined, objInfo.UserMetadata)
+
+ return oi
+}
+
+func (r BatchJobReplicateV1) writeAsArchive(ctx context.Context, objAPI ObjectLayer, remoteClnt *minio.Client, entries []ObjectInfo, prefix string) error {
+ input := make(chan minio.SnowballObject, 1)
+ opts := minio.SnowballOptions{
+ Opts: minio.PutObjectOptions{},
+ InMemory: *r.Source.Snowball.InMemory,
+ Compress: *r.Source.Snowball.Compress,
+ SkipErrs: *r.Source.Snowball.SkipErrs,
+ }
+
+ go func() {
+ defer xioutil.SafeClose(input)
+
+ for _, entry := range entries {
+ gr, err := objAPI.GetObjectNInfo(ctx, r.Source.Bucket,
+ entry.Name, nil, nil, ObjectOptions{
+ VersionID: entry.VersionID,
+ })
+ if err != nil {
+ batchLogIf(ctx, err)
+ continue
+ }
+
+ if prefix != "" {
+ entry.Name = pathJoin(prefix, entry.Name)
+ }
+
+ snowballObj := minio.SnowballObject{
+ // Create path to store objects within the bucket.
+ Key: entry.Name,
+ Size: entry.Size,
+ ModTime: entry.ModTime,
+ VersionID: entry.VersionID,
+ Content: gr,
+ Headers: make(http.Header),
+ Close: func() {
+ gr.Close()
+ },
+ }
+
+ opts, _, err := batchReplicationOpts(ctx, "", gr.ObjInfo)
+ if err != nil {
+ batchLogIf(ctx, err)
+ 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 _, v := range vals {
+ snowballObj.Headers.Add(k, v)
+ }
+ }
+
+ input <- snowballObj
+ }
+ }()
+
+ // Collect and upload all entries.
+ return remoteClnt.PutObjectsSnowball(ctx, r.Target.Bucket, opts, input)
+}
+
+// ReplicateToTarget read from source and replicate to configured target
+func (r *BatchJobReplicateV1) ReplicateToTarget(ctx context.Context, api ObjectLayer, c *minio.Core, srcObjInfo ObjectInfo, retry bool) error {
+ srcBucket := r.Source.Bucket
+ tgtBucket := r.Target.Bucket
+ tgtPrefix := r.Target.Prefix
+ srcObject := srcObjInfo.Name
+ s3Type := r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3
+
+ if srcObjInfo.DeleteMarker || !srcObjInfo.VersionPurgeStatus.Empty() {
+ if retry && !s3Type {
+ if _, err := c.StatObject(ctx, tgtBucket, pathJoin(tgtPrefix, srcObject), minio.StatObjectOptions{
+ VersionID: srcObjInfo.VersionID,
+ Internal: minio.AdvancedGetOptions{
+ ReplicationProxyRequest: "false",
+ },
+ }); isErrMethodNotAllowed(ErrorRespToObjectError(err, tgtBucket, pathJoin(tgtPrefix, srcObject))) {
+ return nil
+ }
+ }
+
+ versionID := srcObjInfo.VersionID
+ dmVersionID := ""
+ if srcObjInfo.VersionPurgeStatus.Empty() {
+ dmVersionID = srcObjInfo.VersionID
+ }
+ if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 {
+ dmVersionID = ""
+ versionID = ""
+ }
+ return c.RemoveObject(ctx, tgtBucket, pathJoin(tgtPrefix, srcObject), minio.RemoveObjectOptions{
+ VersionID: versionID,
+ Internal: minio.AdvancedRemoveOptions{
+ ReplicationDeleteMarker: dmVersionID != "",
+ ReplicationMTime: srcObjInfo.ModTime,
+ ReplicationStatus: minio.ReplicationStatusReplica,
+ ReplicationRequest: true, // always set this to distinguish between `mc mirror` replication and serverside
+ },
+ })
+ }
+
+ if retry && !s3Type { // when we are retrying avoid copying if necessary.
+ gopts := minio.GetObjectOptions{}
+ if err := gopts.SetMatchETag(srcObjInfo.ETag); err != nil {
+ return err
+ }
+ if _, err := c.StatObject(ctx, tgtBucket, pathJoin(tgtPrefix, srcObject), gopts); err == nil {
+ return nil
+ }
+ }
+
+ versioned := globalBucketVersioningSys.PrefixEnabled(srcBucket, srcObject)
+ versionSuspended := globalBucketVersioningSys.PrefixSuspended(srcBucket, srcObject)
+
+ opts := ObjectOptions{
+ VersionID: srcObjInfo.VersionID,
+ Versioned: versioned,
+ VersionSuspended: versionSuspended,
+ }
+ rd, err := api.GetObjectNInfo(ctx, srcBucket, srcObject, nil, http.Header{}, opts)
+ if err != nil {
+ return err
+ }
+ defer rd.Close()
+ objInfo := rd.ObjInfo
+
+ size, err := objInfo.GetActualSize()
+ if err != nil {
+ return err
+ }
+
+ putOpts, isMP, err := batchReplicationOpts(ctx, "", objInfo)
+ if err != nil {
+ return err
+ }
+ if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 {
+ putOpts.Internal = minio.AdvancedPutOptions{}
+ }
+ if isMP {
+ if err := replicateObjectWithMultipart(ctx, c, tgtBucket, pathJoin(tgtPrefix, objInfo.Name), rd, objInfo, putOpts); err != nil {
+ return err
+ }
+ } else {
+ if _, err = c.PutObject(ctx, tgtBucket, pathJoin(tgtPrefix, objInfo.Name), rd, size, "", "", putOpts); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+//go:generate msgp -file $GOFILE -unexported
+
+// batchJobInfo current batch replication information
+type batchJobInfo struct {
+ mu sync.RWMutex `json:"-" msg:"-"`
+
+ Version int `json:"-" msg:"v"`
+ JobID string `json:"jobID" msg:"jid"`
+ JobType string `json:"jobType" msg:"jt"`
+ StartTime time.Time `json:"startTime" msg:"st"`
+ LastUpdate time.Time `json:"lastUpdate" msg:"lu"`
+ RetryAttempts int `json:"retryAttempts" msg:"ra"`
+ Attempts int `json:"attempts" msg:"at"`
+
+ Complete bool `json:"complete" msg:"cmp"`
+ Failed bool `json:"failed" msg:"fld"`
+
+ // Last bucket/object batch replicated
+ Bucket string `json:"-" msg:"lbkt"`
+ Object string `json:"-" msg:"lobj"`
+
+ // Verbose information
+ Objects int64 `json:"objects" msg:"ob"`
+ DeleteMarkers int64 `json:"deleteMarkers" msg:"dm"`
+ ObjectsFailed int64 `json:"objectsFailed" msg:"obf"`
+ DeleteMarkersFailed int64 `json:"deleteMarkersFailed" msg:"dmf"`
+ BytesTransferred int64 `json:"bytesTransferred" msg:"bt"`
+ BytesFailed int64 `json:"bytesFailed" msg:"bf"`
+}
+
+const (
+ batchReplName = "batch-replicate.bin"
+ batchReplFormat = 1
+ batchReplVersionV1 = 1
+ batchReplVersion = batchReplVersionV1
+ batchJobName = "job.bin"
+ batchJobPrefix = "batch-jobs"
+ batchJobReportsPrefix = batchJobPrefix + "/reports"
+
+ batchReplJobAPIVersion = "v1"
+ batchReplJobDefaultRetries = 3
+ batchReplJobDefaultRetryDelay = time.Second
+)
+
+func getJobPath(job BatchJobRequest) string {
+ return pathJoin(batchJobPrefix, job.ID)
+}
+
+func (ri *batchJobInfo) getJobReportPath() (string, error) {
+ var fileName string
+ switch madmin.BatchJobType(ri.JobType) {
+ case madmin.BatchJobReplicate:
+ fileName = batchReplName
+ case madmin.BatchJobKeyRotate:
+ fileName = batchKeyRotationName
+ case madmin.BatchJobExpire:
+ fileName = batchExpireName
+ default:
+ return "", fmt.Errorf("unknown job type: %v", ri.JobType)
+ }
+ return pathJoin(batchJobReportsPrefix, ri.JobID, fileName), nil
+}
+
+func (ri *batchJobInfo) loadOrInit(ctx context.Context, api ObjectLayer, job BatchJobRequest) error {
+ err := ri.load(ctx, api, job)
+ if errors.Is(err, errNoSuchJob) {
+ switch {
+ case job.Replicate != nil:
+ ri.Version = batchReplVersionV1
+ case job.KeyRotate != nil:
+ ri.Version = batchKeyRotateVersionV1
+ case job.Expire != nil:
+ ri.Version = batchExpireVersionV1
+ }
+ return nil
+ }
+ return err
+}
+
+func (ri *batchJobInfo) load(ctx context.Context, api ObjectLayer, job BatchJobRequest) error {
+ path, err := job.getJobReportPath()
+ if err != nil {
+ batchLogIf(ctx, err)
+ return err
+ }
+ return ri.loadByPath(ctx, api, path)
+}
+
+func (ri *batchJobInfo) loadByPath(ctx context.Context, api ObjectLayer, path string) error {
+ var format, version uint16
+ switch filepath.Base(path) {
+ case batchReplName:
+ version = batchReplVersionV1
+ format = batchReplFormat
+ case batchKeyRotationName:
+ version = batchKeyRotateVersionV1
+ format = batchKeyRotationFormat
+ case batchExpireName:
+ version = batchExpireVersionV1
+ format = batchExpireFormat
+ default:
+ return errors.New("no supported batch job request specified")
+ }
+
+ data, err := readConfig(ctx, api, path)
+ if err != nil {
+ if errors.Is(err, errConfigNotFound) || isErrObjectNotFound(err) {
+ return errNoSuchJob
+ }
+ return err
+ }
+ if len(data) == 0 {
+ // Seems to be empty create a new batchRepl object.
+ return nil
+ }
+ if len(data) <= 4 {
+ return fmt.Errorf("%s: no data", ri.JobType)
+ }
+ // Read header
+ switch binary.LittleEndian.Uint16(data[0:2]) {
+ case format:
+ default:
+ return fmt.Errorf("%s: unknown format: %d", ri.JobType, binary.LittleEndian.Uint16(data[0:2]))
+ }
+ switch binary.LittleEndian.Uint16(data[2:4]) {
+ case version:
+ default:
+ return fmt.Errorf("%s: unknown version: %d", ri.JobType, binary.LittleEndian.Uint16(data[2:4]))
+ }
+
+ ri.mu.Lock()
+ defer ri.mu.Unlock()
+
+ // OK, parse data.
+ if _, err = ri.UnmarshalMsg(data[4:]); err != nil {
+ return err
+ }
+
+ switch ri.Version {
+ case batchReplVersionV1:
+ default:
+ return fmt.Errorf("unexpected batch %s meta version: %d", ri.JobType, ri.Version)
+ }
+
+ return nil
+}
+
+func (ri *batchJobInfo) clone() *batchJobInfo {
+ ri.mu.RLock()
+ defer ri.mu.RUnlock()
+
+ return &batchJobInfo{
+ Version: ri.Version,
+ JobID: ri.JobID,
+ JobType: ri.JobType,
+ RetryAttempts: ri.RetryAttempts,
+ Complete: ri.Complete,
+ Failed: ri.Failed,
+ StartTime: ri.StartTime,
+ LastUpdate: ri.LastUpdate,
+ Bucket: ri.Bucket,
+ Object: ri.Object,
+ Objects: ri.Objects,
+ ObjectsFailed: ri.ObjectsFailed,
+ DeleteMarkers: ri.DeleteMarkers,
+ DeleteMarkersFailed: ri.DeleteMarkersFailed,
+ BytesTransferred: ri.BytesTransferred,
+ BytesFailed: ri.BytesFailed,
+ Attempts: ri.Attempts,
+ }
+}
+
+func (ri *batchJobInfo) countItem(size int64, dmarker, success bool, attempt int) {
+ if ri == nil {
+ return
+ }
+ ri.Attempts++
+ if success {
+ if dmarker {
+ ri.DeleteMarkers++
+ } else {
+ ri.Objects++
+ ri.BytesTransferred += size
+ }
+ if attempt > 1 {
+ if dmarker {
+ ri.DeleteMarkersFailed--
+ } else {
+ ri.ObjectsFailed--
+ ri.BytesFailed += size
+ }
+ }
+ } else {
+ if attempt > 1 {
+ // Only count first attempt
+ return
+ }
+ if dmarker {
+ ri.DeleteMarkersFailed++
+ } else {
+ ri.ObjectsFailed++
+ ri.BytesFailed += size
+ }
+ }
+}
+
+func (ri *batchJobInfo) updateAfter(ctx context.Context, api ObjectLayer, duration time.Duration, job BatchJobRequest) error {
+ if ri == nil {
+ return errInvalidArgument
+ }
+ now := UTCNow()
+ ri.mu.Lock()
+ var (
+ format, version uint16
+ jobTyp string
+ )
+
+ if now.Sub(ri.LastUpdate) >= duration {
+ switch job.Type() {
+ case madmin.BatchJobReplicate:
+ format = batchReplFormat
+ version = batchReplVersion
+ jobTyp = string(job.Type())
+ ri.Version = batchReplVersionV1
+ case madmin.BatchJobKeyRotate:
+ format = batchKeyRotationFormat
+ version = batchKeyRotateVersion
+ jobTyp = string(job.Type())
+ ri.Version = batchKeyRotateVersionV1
+ case madmin.BatchJobExpire:
+ format = batchExpireFormat
+ version = batchExpireVersion
+ jobTyp = string(job.Type())
+ ri.Version = batchExpireVersionV1
+ default:
+ return errInvalidArgument
+ }
+ if serverDebugLog {
+ console.Debugf("%s: persisting info on drive: threshold:%s, %s:%#v\n", jobTyp, now.Sub(ri.LastUpdate), jobTyp, ri)
+ }
+ ri.LastUpdate = now
+
+ data := make([]byte, 4, ri.Msgsize()+4)
+
+ // Initialize the header.
+ binary.LittleEndian.PutUint16(data[0:2], format)
+ binary.LittleEndian.PutUint16(data[2:4], version)
+
+ buf, err := ri.MarshalMsg(data)
+ ri.mu.Unlock()
+ if err != nil {
+ return err
+ }
+ path, err := ri.getJobReportPath()
+ if err != nil {
+ batchLogIf(ctx, err)
+ return err
+ }
+ return saveConfig(ctx, api, path, buf)
+ }
+ ri.mu.Unlock()
+ return nil
+}
+
+// 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
+// object which matches the given filters.
+func (ri *batchJobInfo) trackMultipleObjectVersions(info expireObjInfo, success bool) {
+ if ri == nil {
+ return
+ }
+
+ ri.mu.Lock()
+ defer ri.mu.Unlock()
+
+ if success {
+ ri.Bucket = info.Bucket
+ ri.Object = info.Name
+ ri.Objects += int64(info.NumVersions) - info.DeleteMarkerCount
+ ri.DeleteMarkers += info.DeleteMarkerCount
+ } else {
+ ri.ObjectsFailed += int64(info.NumVersions) - info.DeleteMarkerCount
+ ri.DeleteMarkersFailed += info.DeleteMarkerCount
+ }
+}
+
+func (ri *batchJobInfo) trackCurrentBucketObject(bucket string, info ObjectInfo, success bool, attempt int) {
+ if ri == nil {
+ return
+ }
+
+ ri.mu.Lock()
+ defer ri.mu.Unlock()
+
+ if success {
+ ri.Bucket = bucket
+ ri.Object = info.Name
+ }
+ ri.countItem(info.Size, info.DeleteMarker, success, attempt)
+}
+
+func (ri *batchJobInfo) trackCurrentBucketBatch(bucket string, batch []ObjectInfo) {
+ if ri == nil {
+ return
+ }
+
+ ri.mu.Lock()
+ defer ri.mu.Unlock()
+
+ ri.Bucket = bucket
+ for i := range batch {
+ ri.Object = batch[i].Name
+ ri.countItem(batch[i].Size, batch[i].DeleteMarker, true, 1)
+ }
+}
+
+// Start start the batch replication job, resumes if there was a pending job via "job.ID"
+func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job BatchJobRequest) error {
+ ri := &batchJobInfo{
+ JobID: job.ID,
+ JobType: string(job.Type()),
+ StartTime: job.Started,
+ }
+ if err := ri.loadOrInit(ctx, api, job); err != nil {
+ return err
+ }
+ if ri.Complete {
+ return nil
+ }
+ globalBatchJobsMetrics.save(job.ID, ri)
+ lastObject := ri.Object
+
+ retryAttempts := job.Replicate.Flags.Retry.Attempts
+ if retryAttempts <= 0 {
+ retryAttempts = batchReplJobDefaultRetries
+ }
+ delay := job.Replicate.Flags.Retry.Delay
+ if delay <= 0 {
+ delay = batchReplJobDefaultRetryDelay
+ }
+
+ rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
+
+ selectObj := func(info FileInfo) (ok bool) {
+ if r.Flags.Filter.OlderThan > 0 && time.Since(info.ModTime) < r.Flags.Filter.OlderThan.D() {
+ // skip all objects that are newer than specified older duration
+ return false
+ }
+
+ if r.Flags.Filter.NewerThan > 0 && time.Since(info.ModTime) >= r.Flags.Filter.NewerThan.D() {
+ // skip all objects that are older than specified newer duration
+ return false
+ }
+
+ if !r.Flags.Filter.CreatedAfter.IsZero() && r.Flags.Filter.CreatedAfter.After(info.ModTime) {
+ // skip all objects that are created before the specified time.
+ return false
+ }
+
+ if !r.Flags.Filter.CreatedBefore.IsZero() && r.Flags.Filter.CreatedBefore.Before(info.ModTime) {
+ // skip all objects that are created after the specified time.
+ return false
+ }
+
+ if len(r.Flags.Filter.Tags) > 0 {
+ // Only parse object tags if tags filter is specified.
+ tagMap := map[string]string{}
+ tagStr := info.Metadata[xhttp.AmzObjectTagging]
+ if len(tagStr) != 0 {
+ t, err := tags.ParseObjectTags(tagStr)
+ if err != nil {
+ return false
+ }
+ tagMap = t.ToMap()
+ }
+
+ for _, kv := range r.Flags.Filter.Tags {
+ for t, v := range tagMap {
+ if kv.Match(BatchJobKV{Key: t, Value: v}) {
+ return true
+ }
+ }
+ }
+
+ // None of the provided tags filter match skip the object
+ return false
+ }
+
+ if len(r.Flags.Filter.Metadata) > 0 {
+ for _, kv := range r.Flags.Filter.Metadata {
+ for k, v := range info.Metadata {
+ if !stringsHasPrefixFold(k, "x-amz-meta-") && !isStandardHeader(k) {
+ continue
+ }
+ // We only need to match x-amz-meta or standardHeaders
+ if kv.Match(BatchJobKV{Key: k, Value: v}) {
+ return true
+ }
+ }
+ }
+
+ // None of the provided metadata filters match skip the object.
+ return false
+ }
+
+ // if one of source or target is non MinIO, just replicate the top most version like `mc mirror`
+ isSourceOrTargetS3 := r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3
+ return !isSourceOrTargetS3 || info.IsLatest
+ }
+
+ u, err := url.Parse(r.Target.Endpoint)
+ if err != nil {
+ return err
+ }
+
+ cred := r.Target.Creds
+
+ c, err := minio.NewCore(u.Host, &minio.Options{
+ Creds: credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
+ Secure: u.Scheme == "https",
+ Transport: getRemoteInstanceTransport(),
+ BucketLookup: lookupStyle(r.Target.Path),
+ })
+ if err != nil {
+ return err
+ }
+
+ c.SetAppInfo("minio-"+batchJobPrefix, r.APIVersion+" "+job.ID)
+
+ retry := false
+ for attempts := 1; attempts <= retryAttempts; attempts++ {
+ attempts := attempts
+ var (
+ walkCh = make(chan itemOrErr[ObjectInfo], 100)
+ slowCh = make(chan itemOrErr[ObjectInfo], 100)
+ )
+
+ if r.Source.Snowball.Disable != nil && !*r.Source.Snowball.Disable && r.Source.Type.isMinio() && r.Target.Type.isMinio() {
+ go func() {
+ // Snowball currently needs the high level minio-go Client, not the Core one
+ cl, err := minio.New(u.Host, &minio.Options{
+ Creds: credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
+ Secure: u.Scheme == "https",
+ Transport: getRemoteInstanceTransport(),
+ BucketLookup: lookupStyle(r.Target.Path),
+ })
+ if err != nil {
+ batchLogOnceIf(ctx, err, job.ID+"minio.New")
+ return
+ }
+
+ // Already validated before arriving here
+ smallerThan, _ := humanize.ParseBytes(*r.Source.Snowball.SmallerThan)
+
+ batch := make([]ObjectInfo, 0, *r.Source.Snowball.Batch)
+ writeFn := func(batch []ObjectInfo) {
+ if len(batch) > 0 {
+ if err := r.writeAsArchive(ctx, api, cl, batch, r.Target.Prefix); err != nil {
+ batchLogOnceIf(ctx, err, job.ID+"writeAsArchive")
+ for _, b := range batch {
+ slowCh <- itemOrErr[ObjectInfo]{Item: b}
+ }
+ } else {
+ ri.trackCurrentBucketBatch(r.Source.Bucket, batch)
+ globalBatchJobsMetrics.save(job.ID, ri)
+ // persist in-memory state to disk after every 10secs.
+ batchLogOnceIf(ctx, ri.updateAfter(ctx, api, 10*time.Second, job), job.ID+"updateAfter")
+ }
+ }
+ }
+ for obj := range walkCh {
+ if obj.Item.DeleteMarker || !obj.Item.VersionPurgeStatus.Empty() || obj.Item.Size >= int64(smallerThan) {
+ slowCh <- obj
+ continue
+ }
+
+ batch = append(batch, obj.Item)
+
+ if len(batch) < *r.Source.Snowball.Batch {
+ continue
+ }
+ writeFn(batch)
+ batch = batch[:0]
+ }
+ writeFn(batch)
+ xioutil.SafeClose(slowCh)
+ }()
+ } else {
+ slowCh = walkCh
+ }
+
+ workerSize, err := strconv.Atoi(env.Get("_MINIO_BATCH_REPLICATION_WORKERS", strconv.Itoa(runtime.GOMAXPROCS(0)/2)))
+ if err != nil {
+ return err
+ }
+
+ wk, err := workers.New(workerSize)
+ if err != nil {
+ // invalid worker size.
+ return err
+ }
+
+ walkQuorum := env.Get("_MINIO_BATCH_REPLICATION_WALK_QUORUM", "strict")
+ if walkQuorum == "" {
+ walkQuorum = "strict"
+ }
+ ctx, cancelCause := context.WithCancelCause(ctx)
+ // one of source/target is s3, skip delete marker and all versions under the same object name.
+ s3Type := r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3
+
+ go func() {
+ prefixes := r.Source.Prefix.F()
+ if len(prefixes) == 0 {
+ prefixes = []string{""}
+ }
+ for _, prefix := range prefixes {
+ prefixWalkCh := make(chan itemOrErr[ObjectInfo], 100)
+ if err := api.Walk(ctx, r.Source.Bucket, prefix, prefixWalkCh, WalkOptions{
+ Marker: lastObject,
+ Filter: selectObj,
+ AskDisks: walkQuorum,
+ }); err != nil {
+ cancelCause(err)
+ xioutil.SafeClose(walkCh)
+ return
+ }
+ for obj := range prefixWalkCh {
+ walkCh <- obj
+ }
+ }
+ xioutil.SafeClose(walkCh)
+ }()
+
+ prevObj := ""
+
+ skipReplicate := false
+ for res := range slowCh {
+ if res.Err != nil {
+ ri.Failed = true
+ batchLogOnceIf(ctx, res.Err, job.ID+"res.Err")
+ continue
+ }
+ result := res.Item
+ if result.Name != prevObj {
+ prevObj = result.Name
+ skipReplicate = result.DeleteMarker && s3Type
+ }
+ if skipReplicate {
+ continue
+ }
+ wk.Take()
+ go func() {
+ defer wk.Give()
+
+ stopFn := globalBatchJobsMetrics.trace(batchJobMetricReplication, job.ID, attempts)
+ success := true
+ if err := r.ReplicateToTarget(ctx, api, c, result, retry); err != nil {
+ if minio.ToErrorResponse(err).Code == "PreconditionFailed" {
+ // pre-condition failed means we already have the object copied over.
+ return
+ }
+ // object must be deleted concurrently, allow these failures but do not count them
+ if isErrVersionNotFound(err) || isErrObjectNotFound(err) {
+ return
+ }
+ stopFn(result, err)
+ batchLogOnceIf(ctx, err, job.ID+"ReplicateToTarget")
+ success = false
+ } else {
+ stopFn(result, nil)
+ }
+ ri.trackCurrentBucketObject(r.Source.Bucket, result, success, attempts)
+ globalBatchJobsMetrics.save(job.ID, ri)
+ // persist in-memory state to disk after every 10secs.
+ batchLogOnceIf(ctx, ri.updateAfter(ctx, api, 10*time.Second, job), job.ID+"updateAfter2")
+
+ if wait := globalBatchConfig.ReplicationWait(); wait > 0 {
+ time.Sleep(wait)
+ }
+ }()
+ }
+ wk.Wait()
+ // Do not need to retry if we can't list objects on source.
+ if context.Cause(ctx) != nil {
+ return context.Cause(ctx)
+ }
+ ri.RetryAttempts = attempts
+ ri.Complete = ri.ObjectsFailed == 0
+ ri.Failed = ri.ObjectsFailed > 0
+
+ globalBatchJobsMetrics.save(job.ID, ri)
+ // persist in-memory state to disk.
+ batchLogOnceIf(ctx, ri.updateAfter(ctx, api, 0, job), job.ID+"updateAfter3")
+
+ if err := r.Notify(ctx, ri); err != nil {
+ batchLogOnceIf(ctx, fmt.Errorf("unable to notify %v", err), job.ID+"notify")
+ }
+
+ cancelCause(nil)
+ if ri.Failed {
+ ri.ObjectsFailed = 0
+ ri.Bucket = ""
+ ri.Object = ""
+ ri.Objects = 0
+ ri.BytesFailed = 0
+ ri.BytesTransferred = 0
+ retry = true // indicate we are retrying..
+ time.Sleep(delay + time.Duration(rnd.Float64()*float64(delay)))
+ continue
+ }
+
+ break
+ }
+
+ return nil
+}
+
+//msgp:ignore batchReplicationJobError
+type batchReplicationJobError struct {
+ Code string
+ Description string
+ HTTPStatusCode int
+ ObjectSize int64
+}
+
+func (e batchReplicationJobError) Error() string {
+ return e.Description
+}
+
+// Validate validates the job definition input
+func (r *BatchJobReplicateV1) Validate(ctx context.Context, job BatchJobRequest, o ObjectLayer) error {
+ if r == nil {
+ return nil
+ }
+
+ if r.APIVersion != batchReplJobAPIVersion {
+ return errInvalidArgument
+ }
+
+ if r.Source.Endpoint != "" && r.Target.Endpoint != "" {
+ return errInvalidArgument
+ }
+
+ if r.Source.Creds.Empty() && r.Target.Creds.Empty() {
+ return errInvalidArgument
+ }
+
+ if r.Source.Bucket == "" || r.Target.Bucket == "" {
+ return errInvalidArgument
+ }
+
+ var isRemoteToLocal bool
+ localBkt := r.Source.Bucket
+ if r.Source.Endpoint != "" {
+ localBkt = r.Target.Bucket
+ isRemoteToLocal = true
+ }
+ info, err := o.GetBucketInfo(ctx, localBkt, BucketOptions{})
+ if err != nil {
+ if isErrBucketNotFound(err) {
+ return batchReplicationJobError{
+ Code: "NoSuchSourceBucket",
+ Description: fmt.Sprintf("The specified bucket %s does not exist", localBkt),
+ HTTPStatusCode: http.StatusNotFound,
+ }
+ }
+ return err
+ }
+
+ if err := r.Source.Type.Validate(); err != nil {
+ return err
+ }
+ if err := r.Source.Snowball.Validate(); err != nil {
+ return err
+ }
+
+ if !r.Source.Creds.Empty() {
+ if err := r.Source.Creds.Validate(); err != nil {
+ return err
+ }
+ }
+ if r.Target.Endpoint == "" && !r.Target.Creds.Empty() {
+ return errInvalidArgument
+ }
+
+ if r.Source.Endpoint == "" && !r.Source.Creds.Empty() {
+ return errInvalidArgument
+ }
+
+ if r.Source.Endpoint != "" && !r.Source.Type.isMinio() && !r.Source.ValidPath() {
+ return errInvalidArgument
+ }
+
+ if r.Target.Endpoint != "" && !r.Target.Type.isMinio() && !r.Target.ValidPath() {
+ return errInvalidArgument
+ }
+
+ if !r.Target.Creds.Empty() {
+ if err := r.Target.Creds.Validate(); err != nil {
+ return err
+ }
+ }
+
+ if err := r.Target.Type.Validate(); err != nil {
+ return err
+ }
+
+ for _, tag := range r.Flags.Filter.Tags {
+ if err := tag.Validate(); err != nil {
+ return err
+ }
+ }
+
+ for _, meta := range r.Flags.Filter.Metadata {
+ if err := meta.Validate(); err != nil {
+ return err
+ }
+ }
+
+ if err := r.Flags.Retry.Validate(); err != nil {
+ return err
+ }
+
+ remoteEp := r.Target.Endpoint
+ remoteBkt := r.Target.Bucket
+ cred := r.Target.Creds
+ pathStyle := r.Target.Path
+
+ if r.Source.Endpoint != "" {
+ remoteEp = r.Source.Endpoint
+ cred = r.Source.Creds
+ remoteBkt = r.Source.Bucket
+ pathStyle = r.Source.Path
+ }
+
+ u, err := url.Parse(remoteEp)
+ if err != nil {
+ return err
+ }
+
+ c, err := minio.NewCore(u.Host, &minio.Options{
+ Creds: credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
+ Secure: u.Scheme == "https",
+ Transport: getRemoteInstanceTransport(),
+ BucketLookup: lookupStyle(pathStyle),
+ })
+ if err != nil {
+ return err
+ }
+ c.SetAppInfo("minio-"+batchJobPrefix, r.APIVersion+" "+job.ID)
+
+ vcfg, err := c.GetBucketVersioning(ctx, remoteBkt)
+ if err != nil {
+ if minio.ToErrorResponse(err).Code == "NoSuchBucket" {
+ return batchReplicationJobError{
+ Code: "NoSuchTargetBucket",
+ Description: "The specified target bucket does not exist",
+ HTTPStatusCode: http.StatusNotFound,
+ }
+ }
+ return err
+ }
+ // if both source and target are minio instances
+ minioType := r.Target.Type == BatchJobReplicateResourceMinIO && r.Source.Type == BatchJobReplicateResourceMinIO
+ // If source has versioning enabled, target must have versioning enabled
+ if minioType && ((info.Versioning && !vcfg.Enabled() && !isRemoteToLocal) || (!info.Versioning && vcfg.Enabled() && isRemoteToLocal)) {
+ return batchReplicationJobError{
+ Code: "InvalidBucketState",
+ Description: fmt.Sprintf("The source '%s' has versioning enabled, target '%s' must have versioning enabled",
+ r.Source.Bucket, r.Target.Bucket),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ }
+
+ r.clnt = c
+ return nil
+}
+
+// Type returns type of batch job, currently only supports 'replicate'
+func (j BatchJobRequest) Type() madmin.BatchJobType {
+ switch {
+ case j.Replicate != nil:
+ return madmin.BatchJobReplicate
+ case j.KeyRotate != nil:
+ return madmin.BatchJobKeyRotate
+ case j.Expire != nil:
+ return madmin.BatchJobExpire
+ }
+ return madmin.BatchJobType("unknown")
+}
+
+// Validate validates the current job, used by 'save()' before
+// persisting the job request
+func (j BatchJobRequest) Validate(ctx context.Context, o ObjectLayer) error {
+ switch {
+ case j.Replicate != nil:
+ return j.Replicate.Validate(ctx, j, o)
+ case j.KeyRotate != nil:
+ return j.KeyRotate.Validate(ctx, j, o)
+ case j.Expire != nil:
+ return j.Expire.Validate(ctx, j, o)
+ }
+ return errInvalidArgument
+}
+
+func (j BatchJobRequest) delete(ctx context.Context, api ObjectLayer) {
+ deleteConfig(ctx, api, getJobPath(j))
+}
+
+func (j BatchJobRequest) getJobReportPath() (string, error) {
+ var fileName string
+ switch {
+ case j.Replicate != nil:
+ fileName = batchReplName
+ case j.KeyRotate != nil:
+ fileName = batchKeyRotationName
+ case j.Expire != nil:
+ fileName = batchExpireName
+ default:
+ return "", errors.New("unknown job type")
+ }
+ return pathJoin(batchJobReportsPrefix, j.ID, fileName), nil
+}
+
+func (j *BatchJobRequest) save(ctx context.Context, api ObjectLayer) error {
+ if j.Replicate == nil && j.KeyRotate == nil && j.Expire == nil {
+ return errInvalidArgument
+ }
+
+ if err := j.Validate(ctx, api); err != nil {
+ return err
+ }
+
+ job, err := j.MarshalMsg(nil)
+ if err != nil {
+ return err
+ }
+
+ return saveConfig(ctx, api, getJobPath(*j), job)
+}
+
+func (j *BatchJobRequest) load(ctx context.Context, api ObjectLayer, name string) error {
+ if j == nil {
+ return nil
+ }
+
+ job, err := readConfig(ctx, api, name)
+ if err != nil {
+ if errors.Is(err, errConfigNotFound) || isErrObjectNotFound(err) {
+ err = errNoSuchJob
+ }
+ return err
+ }
+
+ _, err = j.UnmarshalMsg(job)
+ return err
+}
+
+func batchReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (putOpts minio.PutObjectOptions, isMP bool, err error) {
+ // TODO: support custom storage class for remote replication
+ putOpts, isMP, err = putReplicationOpts(ctx, "", objInfo)
+ if err != nil {
+ return putOpts, isMP, err
+ }
+ putOpts.Internal = minio.AdvancedPutOptions{
+ SourceVersionID: objInfo.VersionID,
+ SourceMTime: objInfo.ModTime,
+ SourceETag: objInfo.ETag,
+ ReplicationRequest: true,
+ }
+ return putOpts, isMP, nil
+}
+
+// ListBatchJobs - lists all currently active batch jobs, optionally takes {jobType}
+// input to list only active batch jobs of 'jobType'
+func (a adminAPIHandlers) ListBatchJobs(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.ListBatchJobsAction)
+ if objectAPI == nil {
+ return
+ }
+
+ jobType := r.Form.Get("jobType")
+
+ resultCh := make(chan itemOrErr[ObjectInfo])
+
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ if err := objectAPI.Walk(ctx, minioMetaBucket, batchJobPrefix, resultCh, WalkOptions{}); err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ listResult := madmin.ListBatchJobsResult{}
+ for result := range resultCh {
+ if result.Err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, result.Err), r.URL)
+ return
+ }
+ if strings.HasPrefix(result.Item.Name, batchJobReportsPrefix+slashSeparator) {
+ continue
+ }
+ req := &BatchJobRequest{}
+ if err := req.load(ctx, objectAPI, result.Item.Name); err != nil {
+ if !errors.Is(err, errNoSuchJob) {
+ batchLogIf(ctx, err)
+ }
+ continue
+ }
+
+ if jobType == string(req.Type()) || jobType == "" {
+ listResult.Jobs = append(listResult.Jobs, madmin.BatchJobResult{
+ ID: req.ID,
+ Type: req.Type(),
+ Started: req.Started,
+ User: req.User,
+ Elapsed: time.Since(req.Started),
+ })
+ }
+ }
+
+ batchLogIf(ctx, json.NewEncoder(w).Encode(&listResult))
+}
+
+// BatchJobStatus - returns the status of a batch job saved in the disk
+func (a adminAPIHandlers) BatchJobStatus(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.ListBatchJobsAction)
+ if objectAPI == nil {
+ return
+ }
+
+ jobID := r.Form.Get("jobId")
+ if jobID == "" {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, errInvalidArgument), r.URL)
+ return
+ }
+
+ req := BatchJobRequest{ID: jobID}
+ if i := strings.Index(jobID, "-"); i > 0 {
+ switch madmin.BatchJobType(jobID[:i]) {
+ case madmin.BatchJobReplicate:
+ req.Replicate = &BatchJobReplicateV1{}
+ case madmin.BatchJobKeyRotate:
+ req.KeyRotate = &BatchJobKeyRotateV1{}
+ case madmin.BatchJobExpire:
+ req.Expire = &BatchJobExpire{}
+ default:
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, errors.New("job ID format unrecognized")), r.URL)
+ return
+ }
+ }
+
+ ri := &batchJobInfo{}
+ if err := ri.load(ctx, objectAPI, req); err != nil {
+ if !errors.Is(err, errNoSuchJob) {
+ batchLogIf(ctx, err)
+ }
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ buf, err := json.Marshal(madmin.BatchJobStatus{LastMetric: ri.metric()})
+ if err != nil {
+ batchLogIf(ctx, err)
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ w.Write(buf)
+}
+
+var errNoSuchJob = errors.New("no such job")
+
+// DescribeBatchJob returns the currently active batch job definition
+func (a adminAPIHandlers) DescribeBatchJob(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.DescribeBatchJobAction)
+ if objectAPI == nil {
+ return
+ }
+
+ jobID := r.Form.Get("jobId")
+ if jobID == "" {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, errInvalidArgument), r.URL)
+ return
+ }
+
+ req := &BatchJobRequest{}
+ if err := req.load(ctx, objectAPI, pathJoin(batchJobPrefix, jobID)); err != nil {
+ if !errors.Is(err, errNoSuchJob) {
+ batchLogIf(ctx, err)
+ }
+
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Remove sensitive fields.
+ req.RedactSensitive()
+ buf, err := yaml.Marshal(req)
+ if err != nil {
+ batchLogIf(ctx, err)
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ w.Write(buf)
+}
+
+// StartBatchJob queue a new job for execution
+func (a adminAPIHandlers) StartBatchJob(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, creds := validateAdminReq(ctx, w, r, policy.StartBatchJobAction)
+ if objectAPI == nil {
+ return
+ }
+
+ buf, err := io.ReadAll(xioutil.HardLimitReader(r.Body, humanize.MiByte*4))
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ user := creds.AccessKey
+ if creds.ParentUser != "" {
+ user = creds.ParentUser
+ }
+
+ job := &BatchJobRequest{}
+ if err = yaml.Unmarshal(buf, job); err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Fill with default values
+ if job.Replicate != nil {
+ if job.Replicate.Source.Snowball.Disable == nil {
+ job.Replicate.Source.Snowball.Disable = ptr(false)
+ }
+ if job.Replicate.Source.Snowball.Batch == nil {
+ job.Replicate.Source.Snowball.Batch = ptr(100)
+ }
+ if job.Replicate.Source.Snowball.InMemory == nil {
+ job.Replicate.Source.Snowball.InMemory = ptr(true)
+ }
+ if job.Replicate.Source.Snowball.Compress == nil {
+ job.Replicate.Source.Snowball.Compress = ptr(false)
+ }
+ if job.Replicate.Source.Snowball.SmallerThan == nil {
+ job.Replicate.Source.Snowball.SmallerThan = ptr("5MiB")
+ }
+ if job.Replicate.Source.Snowball.SkipErrs == nil {
+ job.Replicate.Source.Snowball.SkipErrs = ptr(true)
+ }
+ }
+
+ // Validate the incoming job request
+ if err := job.Validate(ctx, objectAPI); err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ job.ID = fmt.Sprintf("%s-%s%s%d", job.Type(), shortuuid.New(), getKeySeparator(), GetProxyEndpointLocalIndex(globalProxyEndpoints))
+ job.User = user
+ job.Started = time.Now()
+
+ if err := job.save(ctx, objectAPI); err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if err = globalBatchJobPool.queueJob(job); err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ buf, err = json.Marshal(&madmin.BatchJobResult{
+ ID: job.ID,
+ Type: job.Type(),
+ Started: job.Started,
+ User: job.User,
+ })
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseJSON(w, buf)
+}
+
+// CancelBatchJob cancels a job in progress
+func (a adminAPIHandlers) CancelBatchJob(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.CancelBatchJobAction)
+ if objectAPI == nil {
+ return
+ }
+
+ jobID := r.Form.Get("id")
+ if jobID == "" {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, errInvalidArgument), r.URL)
+ return
+ }
+
+ if _, proxied, _ := proxyRequestByToken(ctx, w, r, jobID, true); proxied {
+ return
+ }
+
+ if err := globalBatchJobPool.canceler(jobID, true); err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrInvalidRequest, err), r.URL)
+ return
+ }
+
+ j := BatchJobRequest{
+ ID: jobID,
+ }
+
+ j.delete(ctx, objectAPI)
+
+ writeSuccessNoContent(w)
+}
+
+//msgp:ignore BatchJobPool
+
+// BatchJobPool batch job pool
+type BatchJobPool struct {
+ ctx context.Context
+ objLayer ObjectLayer
+ once sync.Once
+ mu sync.Mutex
+ jobCh chan *BatchJobRequest
+ jmu sync.Mutex // protects jobCancelers
+ jobCancelers map[string]context.CancelFunc
+ workerKillCh chan struct{}
+ workerSize int
+}
+
+var globalBatchJobPool *BatchJobPool
+
+// newBatchJobPool creates a pool of job manifest workers of specified size
+func newBatchJobPool(ctx context.Context, o ObjectLayer, workers int) *BatchJobPool {
+ jpool := &BatchJobPool{
+ ctx: ctx,
+ objLayer: o,
+ jobCh: make(chan *BatchJobRequest, 10000),
+ workerKillCh: make(chan struct{}, workers),
+ jobCancelers: make(map[string]context.CancelFunc),
+ }
+ jpool.ResizeWorkers(workers)
+
+ randomWait := func() time.Duration {
+ // randomWait depends on the number of nodes to avoid triggering resume and cleanups at the same time.
+ return time.Duration(rand.Float64() * float64(time.Duration(globalEndpoints.NEndpoints())*time.Hour))
+ }
+
+ go func() {
+ jpool.resume(randomWait)
+ jpool.cleanupReports(randomWait)
+ }()
+
+ return jpool
+}
+
+func (j *BatchJobPool) cleanupReports(randomWait func() time.Duration) {
+ t := time.NewTimer(randomWait())
+ defer t.Stop()
+
+ for {
+ select {
+ case <-GlobalContext.Done():
+ return
+ case <-t.C:
+ results := make(chan itemOrErr[ObjectInfo], 100)
+ ctx, cancel := context.WithCancel(j.ctx)
+ defer cancel()
+ if err := j.objLayer.Walk(ctx, minioMetaBucket, batchJobReportsPrefix, results, WalkOptions{}); err != nil {
+ batchLogIf(j.ctx, err)
+ t.Reset(randomWait())
+ continue
+ }
+ for result := range results {
+ if result.Err != nil {
+ batchLogIf(j.ctx, result.Err)
+ continue
+ }
+ ri := &batchJobInfo{}
+ if err := ri.loadByPath(ctx, j.objLayer, result.Item.Name); err != nil {
+ batchLogIf(ctx, err)
+ continue
+ }
+ if (ri.Complete || ri.Failed) && time.Since(ri.LastUpdate) > oldJobsExpiration {
+ deleteConfig(ctx, j.objLayer, result.Item.Name)
+ }
+ }
+
+ t.Reset(randomWait())
+ }
+ }
+}
+
+func (j *BatchJobPool) resume(randomWait func() time.Duration) {
+ time.Sleep(randomWait())
+
+ results := make(chan itemOrErr[ObjectInfo], 100)
+ ctx, cancel := context.WithCancel(j.ctx)
+ defer cancel()
+ if err := j.objLayer.Walk(ctx, minioMetaBucket, batchJobPrefix, results, WalkOptions{}); err != nil {
+ batchLogIf(j.ctx, err)
+ return
+ }
+ for result := range results {
+ if result.Err != nil {
+ batchLogIf(j.ctx, result.Err)
+ continue
+ }
+ if strings.HasPrefix(result.Item.Name, batchJobReportsPrefix+slashSeparator) {
+ continue
+ }
+ // ignore batch-replicate.bin and batch-rotate.bin entries
+ if strings.HasSuffix(result.Item.Name, slashSeparator) {
+ continue
+ }
+ req := &BatchJobRequest{}
+ if err := req.load(ctx, j.objLayer, result.Item.Name); err != nil {
+ batchLogIf(ctx, err)
+ continue
+ }
+ _, nodeIdx := parseRequestToken(req.ID)
+ if nodeIdx > -1 && GetProxyEndpointLocalIndex(globalProxyEndpoints) != nodeIdx {
+ // This job doesn't belong on this node.
+ continue
+ }
+ if err := j.queueJob(req); err != nil {
+ batchLogIf(ctx, err)
+ continue
+ }
+ }
+}
+
+// AddWorker adds a replication worker to the pool
+func (j *BatchJobPool) AddWorker() {
+ if j == nil {
+ return
+ }
+ for {
+ select {
+ case <-j.ctx.Done():
+ return
+ case job, ok := <-j.jobCh:
+ if !ok {
+ return
+ }
+ switch {
+ case job.Replicate != nil:
+ if job.Replicate.RemoteToLocal() {
+ if err := job.Replicate.StartFromSource(job.ctx, j.objLayer, *job); err != nil {
+ if !isErrBucketNotFound(err) {
+ batchLogIf(j.ctx, err)
+ j.canceler(job.ID, false)
+ continue
+ }
+ // Bucket not found proceed to delete such a job.
+ }
+ } else {
+ if err := job.Replicate.Start(job.ctx, j.objLayer, *job); err != nil {
+ if !isErrBucketNotFound(err) {
+ batchLogIf(j.ctx, err)
+ j.canceler(job.ID, false)
+ continue
+ }
+ // Bucket not found proceed to delete such a job.
+ }
+ }
+ case job.KeyRotate != nil:
+ if err := job.KeyRotate.Start(job.ctx, j.objLayer, *job); err != nil {
+ if !isErrBucketNotFound(err) {
+ batchLogIf(j.ctx, err)
+ continue
+ }
+ }
+ case job.Expire != nil:
+ if err := job.Expire.Start(job.ctx, j.objLayer, *job); err != nil {
+ if !isErrBucketNotFound(err) {
+ batchLogIf(j.ctx, err)
+ continue
+ }
+ }
+ }
+ j.canceler(job.ID, false)
+ case <-j.workerKillCh:
+ return
+ }
+ }
+}
+
+// ResizeWorkers sets replication workers pool to new size
+func (j *BatchJobPool) ResizeWorkers(n int) {
+ if j == nil {
+ return
+ }
+
+ j.mu.Lock()
+ defer j.mu.Unlock()
+
+ for j.workerSize < n {
+ j.workerSize++
+ go j.AddWorker()
+ }
+ for j.workerSize > n {
+ j.workerSize--
+ go func() { j.workerKillCh <- struct{}{} }()
+ }
+}
+
+func (j *BatchJobPool) queueJob(req *BatchJobRequest) error {
+ if j == nil {
+ return errInvalidArgument
+ }
+ jctx, jcancel := context.WithCancel(j.ctx)
+ j.jmu.Lock()
+ j.jobCancelers[req.ID] = jcancel
+ j.jmu.Unlock()
+ req.ctx = jctx
+
+ select {
+ case <-j.ctx.Done():
+ j.once.Do(func() {
+ xioutil.SafeClose(j.jobCh)
+ })
+ case j.jobCh <- req:
+ default:
+ return fmt.Errorf("batch job queue is currently full please try again later %#v", req)
+ }
+ return nil
+}
+
+// delete canceler from the map, cancel job if requested
+func (j *BatchJobPool) canceler(jobID string, cancel bool) error {
+ if j == nil {
+ return errInvalidArgument
+ }
+ j.jmu.Lock()
+ defer j.jmu.Unlock()
+ if canceler, ok := j.jobCancelers[jobID]; ok {
+ if cancel {
+ canceler()
+ }
+ }
+ if cancel {
+ delete(j.jobCancelers, jobID)
+ }
+ return nil
+}
+
+//msgp:ignore batchJobMetrics
+type batchJobMetrics struct {
+ sync.RWMutex
+ metrics map[string]*batchJobInfo
+}
+
+//msgp:ignore batchJobMetric
+//go:generate stringer -type=batchJobMetric -trimprefix=batchJobMetric $GOFILE
+type batchJobMetric uint8
+
+const (
+ batchJobMetricReplication batchJobMetric = iota
+ batchJobMetricKeyRotation
+ batchJobMetricExpire
+)
+
+func batchJobTrace(d batchJobMetric, job string, startTime time.Time, duration time.Duration, info objTraceInfoer, attempts int, err error) madmin.TraceInfo {
+ var errStr string
+ if err != nil {
+ errStr = err.Error()
+ }
+ traceType := madmin.TraceBatchReplication
+ switch d {
+ case batchJobMetricKeyRotation:
+ traceType = madmin.TraceBatchKeyRotation
+ case batchJobMetricExpire:
+ traceType = madmin.TraceBatchExpire
+ }
+ funcName := fmt.Sprintf("%s() (job-name=%s)", d.String(), job)
+ if attempts > 0 {
+ funcName = fmt.Sprintf("%s() (job-name=%s,attempts=%s)", d.String(), job, humanize.Ordinal(attempts))
+ }
+ return madmin.TraceInfo{
+ TraceType: traceType,
+ Time: startTime,
+ NodeName: globalLocalNodeName,
+ FuncName: funcName,
+ Duration: duration,
+ Path: fmt.Sprintf("%s (versionID=%s)", info.TraceObjName(), info.TraceVersionID()),
+ Error: errStr,
+ }
+}
+
+func (ri *batchJobInfo) metric() madmin.JobMetric {
+ m := madmin.JobMetric{
+ JobID: ri.JobID,
+ JobType: ri.JobType,
+ StartTime: ri.StartTime,
+ LastUpdate: ri.LastUpdate,
+ RetryAttempts: ri.RetryAttempts,
+ Complete: ri.Complete,
+ Failed: ri.Failed,
+ }
+
+ switch ri.JobType {
+ case string(madmin.BatchJobReplicate):
+ m.Replicate = &madmin.ReplicateInfo{
+ Bucket: ri.Bucket,
+ Object: ri.Object,
+ Objects: ri.Objects,
+ DeleteMarkers: ri.DeleteMarkers,
+ ObjectsFailed: ri.ObjectsFailed,
+ DeleteMarkersFailed: ri.DeleteMarkersFailed,
+ BytesTransferred: ri.BytesTransferred,
+ BytesFailed: ri.BytesFailed,
+ }
+ case string(madmin.BatchJobKeyRotate):
+ m.KeyRotate = &madmin.KeyRotationInfo{
+ Bucket: ri.Bucket,
+ Object: ri.Object,
+ Objects: ri.Objects,
+ ObjectsFailed: ri.ObjectsFailed,
+ }
+ case string(madmin.BatchJobExpire):
+ m.Expired = &madmin.ExpirationInfo{
+ Bucket: ri.Bucket,
+ Object: ri.Object,
+ Objects: ri.Objects,
+ DeleteMarkers: ri.DeleteMarkers,
+ ObjectsFailed: ri.ObjectsFailed,
+ DeleteMarkersFailed: ri.DeleteMarkersFailed,
+ }
+ }
+
+ return m
+}
+
+func (m *batchJobMetrics) report(jobID string) (metrics *madmin.BatchJobMetrics) {
+ metrics = &madmin.BatchJobMetrics{CollectedAt: time.Now(), Jobs: make(map[string]madmin.JobMetric)}
+ m.RLock()
+ defer m.RUnlock()
+
+ if jobID != "" {
+ if job, ok := m.metrics[jobID]; ok {
+ metrics.Jobs[jobID] = job.metric()
+ }
+ return metrics
+ }
+
+ for id, job := range m.metrics {
+ metrics.Jobs[id] = job.metric()
+ }
+ return metrics
+}
+
+// keep job metrics for some time after the job is completed
+// in-case some one wants to look at the older results.
+func (m *batchJobMetrics) purgeJobMetrics() {
+ t := time.NewTicker(6 * time.Hour)
+ defer t.Stop()
+
+ for {
+ select {
+ case <-GlobalContext.Done():
+ return
+ case <-t.C:
+ var toDeleteJobMetrics []string
+ m.RLock()
+ for id, metrics := range m.metrics {
+ if time.Since(metrics.LastUpdate) > oldJobsExpiration && (metrics.Complete || metrics.Failed) {
+ toDeleteJobMetrics = append(toDeleteJobMetrics, id)
+ }
+ }
+ m.RUnlock()
+ for _, jobID := range toDeleteJobMetrics {
+ 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) {
+ m.Lock()
+ defer m.Unlock()
+
+ delete(m.metrics, jobID)
+}
+
+func (m *batchJobMetrics) save(jobID string, ri *batchJobInfo) {
+ m.Lock()
+ defer m.Unlock()
+
+ m.metrics[jobID] = ri.clone()
+}
+
+type objTraceInfoer interface {
+ TraceObjName() string
+ TraceVersionID() string
+}
+
+// TraceObjName returns name of object being traced
+func (td ObjectToDelete) TraceObjName() string {
+ return td.ObjectName
+}
+
+// TraceVersionID returns version-id of object being traced
+func (td ObjectToDelete) TraceVersionID() string {
+ return td.VersionID
+}
+
+// TraceObjName returns name of object being traced
+func (oi ObjectInfo) TraceObjName() string {
+ return oi.Name
+}
+
+// TraceVersionID returns version-id of object being traced
+func (oi ObjectInfo) TraceVersionID() string {
+ return oi.VersionID
+}
+
+func (m *batchJobMetrics) trace(d batchJobMetric, job string, attempts int) func(info objTraceInfoer, err error) {
+ startTime := time.Now()
+ return func(info objTraceInfoer, err error) {
+ duration := time.Since(startTime)
+ if globalTrace.NumSubscribers(madmin.TraceBatch) > 0 {
+ globalTrace.Publish(batchJobTrace(d, job, startTime, duration, info, attempts, err))
+ return
+ }
+ switch d {
+ case batchJobMetricReplication:
+ if globalTrace.NumSubscribers(madmin.TraceBatchReplication) > 0 {
+ globalTrace.Publish(batchJobTrace(d, job, startTime, duration, info, attempts, err))
+ }
+ case batchJobMetricKeyRotation:
+ if globalTrace.NumSubscribers(madmin.TraceBatchKeyRotation) > 0 {
+ globalTrace.Publish(batchJobTrace(d, job, startTime, duration, info, attempts, err))
+ }
+ case batchJobMetricExpire:
+ if globalTrace.NumSubscribers(madmin.TraceBatchExpire) > 0 {
+ globalTrace.Publish(batchJobTrace(d, job, startTime, duration, info, attempts, err))
+ }
+ }
+ }
+}
+
+func lookupStyle(s string) minio.BucketLookupType {
+ var lookup minio.BucketLookupType
+ switch s {
+ case "on":
+ lookup = minio.BucketLookupPath
+ case "off":
+ lookup = minio.BucketLookupDNS
+ default:
+ lookup = minio.BucketLookupAuto
+ }
+ return lookup
+}
+
+// BatchJobPrefix - to support prefix field yaml unmarshalling with string or slice of strings
+type BatchJobPrefix []string
+
+var _ yaml.Unmarshaler = &BatchJobPrefix{}
+
+// UnmarshalYAML - to support prefix field yaml unmarshalling with string or slice of strings
+func (b *BatchJobPrefix) UnmarshalYAML(value *yaml.Node) error {
+ // try slice first
+ tmpSlice := []string{}
+ if err := value.Decode(&tmpSlice); err == nil {
+ *b = tmpSlice
+ return nil
+ }
+ // try string
+ tmpStr := ""
+ if err := value.Decode(&tmpStr); err == nil {
+ *b = []string{tmpStr}
+ return nil
+ }
+ return fmt.Errorf("unable to decode %s", value.Value)
+}
+
+// F - return prefix(es) as slice
+func (b *BatchJobPrefix) F() []string {
+ return *b
+}
diff --git a/cmd/batch-handlers_gen.go b/cmd/batch-handlers_gen.go
new file mode 100644
index 00000000..ad29932b
--- /dev/null
+++ b/cmd/batch-handlers_gen.go
@@ -0,0 +1,952 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "github.com/tinylib/msgp/msgp"
+)
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchJobPrefix) DecodeMsg(dc *msgp.Reader) (err error) {
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if cap((*z)) >= int(zb0002) {
+ (*z) = (*z)[:zb0002]
+ } else {
+ (*z) = make(BatchJobPrefix, zb0002)
+ }
+ for zb0001 := range *z {
+ (*z)[zb0001], err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, zb0001)
+ return
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z BatchJobPrefix) EncodeMsg(en *msgp.Writer) (err error) {
+ err = en.WriteArrayHeader(uint32(len(z)))
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0003 := range z {
+ err = en.WriteString(z[zb0003])
+ if err != nil {
+ err = msgp.WrapError(err, zb0003)
+ return
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z BatchJobPrefix) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ o = msgp.AppendArrayHeader(o, uint32(len(z)))
+ for zb0003 := range z {
+ o = msgp.AppendString(o, z[zb0003])
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BatchJobPrefix) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if cap((*z)) >= int(zb0002) {
+ (*z) = (*z)[:zb0002]
+ } else {
+ (*z) = make(BatchJobPrefix, zb0002)
+ }
+ for zb0001 := range *z {
+ (*z)[zb0001], bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, zb0001)
+ return
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z BatchJobPrefix) Msgsize() (s int) {
+ s = msgp.ArrayHeaderSize
+ for zb0003 := range z {
+ s += msgp.StringPrefixSize + len(z[zb0003])
+ }
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchJobRequest) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ID":
+ z.ID, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "ID")
+ return
+ }
+ case "User":
+ z.User, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "User")
+ return
+ }
+ case "Started":
+ z.Started, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "Started")
+ return
+ }
+ case "Replicate":
+ if dc.IsNil() {
+ err = dc.ReadNil()
+ if err != nil {
+ err = msgp.WrapError(err, "Replicate")
+ return
+ }
+ z.Replicate = nil
+ } else {
+ if z.Replicate == nil {
+ z.Replicate = new(BatchJobReplicateV1)
+ }
+ err = z.Replicate.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Replicate")
+ return
+ }
+ }
+ case "KeyRotate":
+ if dc.IsNil() {
+ err = dc.ReadNil()
+ if err != nil {
+ err = msgp.WrapError(err, "KeyRotate")
+ return
+ }
+ z.KeyRotate = nil
+ } else {
+ if z.KeyRotate == nil {
+ z.KeyRotate = new(BatchJobKeyRotateV1)
+ }
+ err = z.KeyRotate.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "KeyRotate")
+ return
+ }
+ }
+ case "Expire":
+ if dc.IsNil() {
+ err = dc.ReadNil()
+ if err != nil {
+ err = msgp.WrapError(err, "Expire")
+ return
+ }
+ z.Expire = nil
+ } else {
+ if z.Expire == nil {
+ z.Expire = new(BatchJobExpire)
+ }
+ err = z.Expire.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Expire")
+ return
+ }
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *BatchJobRequest) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 6
+ // write "ID"
+ err = en.Append(0x86, 0xa2, 0x49, 0x44)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.ID)
+ if err != nil {
+ err = msgp.WrapError(err, "ID")
+ return
+ }
+ // write "User"
+ err = en.Append(0xa4, 0x55, 0x73, 0x65, 0x72)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.User)
+ if err != nil {
+ err = msgp.WrapError(err, "User")
+ return
+ }
+ // write "Started"
+ err = en.Append(0xa7, 0x53, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.Started)
+ if err != nil {
+ err = msgp.WrapError(err, "Started")
+ return
+ }
+ // write "Replicate"
+ err = en.Append(0xa9, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65)
+ if err != nil {
+ return
+ }
+ if z.Replicate == nil {
+ err = en.WriteNil()
+ if err != nil {
+ return
+ }
+ } else {
+ err = z.Replicate.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Replicate")
+ return
+ }
+ }
+ // write "KeyRotate"
+ err = en.Append(0xa9, 0x4b, 0x65, 0x79, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65)
+ if err != nil {
+ return
+ }
+ if z.KeyRotate == nil {
+ err = en.WriteNil()
+ if err != nil {
+ return
+ }
+ } else {
+ err = z.KeyRotate.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "KeyRotate")
+ return
+ }
+ }
+ // write "Expire"
+ err = en.Append(0xa6, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65)
+ if err != nil {
+ return
+ }
+ if z.Expire == nil {
+ err = en.WriteNil()
+ if err != nil {
+ return
+ }
+ } else {
+ err = z.Expire.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Expire")
+ return
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *BatchJobRequest) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 6
+ // string "ID"
+ o = append(o, 0x86, 0xa2, 0x49, 0x44)
+ o = msgp.AppendString(o, z.ID)
+ // string "User"
+ o = append(o, 0xa4, 0x55, 0x73, 0x65, 0x72)
+ o = msgp.AppendString(o, z.User)
+ // string "Started"
+ o = append(o, 0xa7, 0x53, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64)
+ o = msgp.AppendTime(o, z.Started)
+ // string "Replicate"
+ o = append(o, 0xa9, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65)
+ if z.Replicate == nil {
+ o = msgp.AppendNil(o)
+ } else {
+ o, err = z.Replicate.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Replicate")
+ return
+ }
+ }
+ // string "KeyRotate"
+ o = append(o, 0xa9, 0x4b, 0x65, 0x79, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65)
+ if z.KeyRotate == nil {
+ o = msgp.AppendNil(o)
+ } else {
+ o, err = z.KeyRotate.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "KeyRotate")
+ return
+ }
+ }
+ // string "Expire"
+ o = append(o, 0xa6, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65)
+ if z.Expire == nil {
+ o = msgp.AppendNil(o)
+ } else {
+ o, err = z.Expire.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Expire")
+ return
+ }
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BatchJobRequest) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ID":
+ z.ID, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ID")
+ return
+ }
+ case "User":
+ z.User, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "User")
+ return
+ }
+ case "Started":
+ z.Started, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Started")
+ return
+ }
+ case "Replicate":
+ if msgp.IsNil(bts) {
+ bts, err = msgp.ReadNilBytes(bts)
+ if err != nil {
+ return
+ }
+ z.Replicate = nil
+ } else {
+ if z.Replicate == nil {
+ z.Replicate = new(BatchJobReplicateV1)
+ }
+ bts, err = z.Replicate.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Replicate")
+ return
+ }
+ }
+ case "KeyRotate":
+ if msgp.IsNil(bts) {
+ bts, err = msgp.ReadNilBytes(bts)
+ if err != nil {
+ return
+ }
+ z.KeyRotate = nil
+ } else {
+ if z.KeyRotate == nil {
+ z.KeyRotate = new(BatchJobKeyRotateV1)
+ }
+ bts, err = z.KeyRotate.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "KeyRotate")
+ return
+ }
+ }
+ case "Expire":
+ if msgp.IsNil(bts) {
+ bts, err = msgp.ReadNilBytes(bts)
+ if err != nil {
+ return
+ }
+ z.Expire = nil
+ } else {
+ if z.Expire == nil {
+ z.Expire = new(BatchJobExpire)
+ }
+ bts, err = z.Expire.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Expire")
+ return
+ }
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *BatchJobRequest) Msgsize() (s int) {
+ s = 1 + 3 + msgp.StringPrefixSize + len(z.ID) + 5 + msgp.StringPrefixSize + len(z.User) + 8 + msgp.TimeSize + 10
+ if z.Replicate == nil {
+ s += msgp.NilSize
+ } else {
+ s += z.Replicate.Msgsize()
+ }
+ s += 10
+ if z.KeyRotate == nil {
+ s += msgp.NilSize
+ } else {
+ s += z.KeyRotate.Msgsize()
+ }
+ s += 7
+ if z.Expire == nil {
+ s += msgp.NilSize
+ } else {
+ s += z.Expire.Msgsize()
+ }
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *batchJobInfo) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "v":
+ z.Version, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "Version")
+ return
+ }
+ case "jid":
+ z.JobID, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "JobID")
+ return
+ }
+ case "jt":
+ z.JobType, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "JobType")
+ return
+ }
+ case "st":
+ z.StartTime, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "StartTime")
+ return
+ }
+ case "lu":
+ z.LastUpdate, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "LastUpdate")
+ return
+ }
+ case "ra":
+ z.RetryAttempts, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "RetryAttempts")
+ return
+ }
+ case "at":
+ z.Attempts, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "Attempts")
+ return
+ }
+ case "cmp":
+ z.Complete, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "Complete")
+ return
+ }
+ case "fld":
+ z.Failed, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "Failed")
+ return
+ }
+ case "lbkt":
+ z.Bucket, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "lobj":
+ z.Object, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ case "ob":
+ z.Objects, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "Objects")
+ return
+ }
+ case "dm":
+ z.DeleteMarkers, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "DeleteMarkers")
+ return
+ }
+ case "obf":
+ z.ObjectsFailed, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "ObjectsFailed")
+ return
+ }
+ case "dmf":
+ z.DeleteMarkersFailed, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "DeleteMarkersFailed")
+ return
+ }
+ case "bt":
+ z.BytesTransferred, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "BytesTransferred")
+ return
+ }
+ case "bf":
+ z.BytesFailed, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "BytesFailed")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *batchJobInfo) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 17
+ // write "v"
+ err = en.Append(0xde, 0x0, 0x11, 0xa1, 0x76)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.Version)
+ if err != nil {
+ err = msgp.WrapError(err, "Version")
+ return
+ }
+ // write "jid"
+ err = en.Append(0xa3, 0x6a, 0x69, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.JobID)
+ if err != nil {
+ err = msgp.WrapError(err, "JobID")
+ return
+ }
+ // write "jt"
+ err = en.Append(0xa2, 0x6a, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.JobType)
+ if err != nil {
+ err = msgp.WrapError(err, "JobType")
+ return
+ }
+ // write "st"
+ err = en.Append(0xa2, 0x73, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.StartTime)
+ if err != nil {
+ err = msgp.WrapError(err, "StartTime")
+ return
+ }
+ // write "lu"
+ err = en.Append(0xa2, 0x6c, 0x75)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.LastUpdate)
+ if err != nil {
+ err = msgp.WrapError(err, "LastUpdate")
+ return
+ }
+ // write "ra"
+ err = en.Append(0xa2, 0x72, 0x61)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.RetryAttempts)
+ if err != nil {
+ err = msgp.WrapError(err, "RetryAttempts")
+ return
+ }
+ // write "at"
+ err = en.Append(0xa2, 0x61, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.Attempts)
+ if err != nil {
+ err = msgp.WrapError(err, "Attempts")
+ return
+ }
+ // write "cmp"
+ err = en.Append(0xa3, 0x63, 0x6d, 0x70)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.Complete)
+ if err != nil {
+ err = msgp.WrapError(err, "Complete")
+ return
+ }
+ // write "fld"
+ err = en.Append(0xa3, 0x66, 0x6c, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.Failed)
+ if err != nil {
+ err = msgp.WrapError(err, "Failed")
+ return
+ }
+ // write "lbkt"
+ err = en.Append(0xa4, 0x6c, 0x62, 0x6b, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Bucket)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ // write "lobj"
+ err = en.Append(0xa4, 0x6c, 0x6f, 0x62, 0x6a)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Object)
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ // write "ob"
+ err = en.Append(0xa2, 0x6f, 0x62)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.Objects)
+ if err != nil {
+ err = msgp.WrapError(err, "Objects")
+ return
+ }
+ // write "dm"
+ err = en.Append(0xa2, 0x64, 0x6d)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.DeleteMarkers)
+ if err != nil {
+ err = msgp.WrapError(err, "DeleteMarkers")
+ return
+ }
+ // write "obf"
+ err = en.Append(0xa3, 0x6f, 0x62, 0x66)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.ObjectsFailed)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjectsFailed")
+ return
+ }
+ // write "dmf"
+ err = en.Append(0xa3, 0x64, 0x6d, 0x66)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.DeleteMarkersFailed)
+ if err != nil {
+ err = msgp.WrapError(err, "DeleteMarkersFailed")
+ return
+ }
+ // write "bt"
+ err = en.Append(0xa2, 0x62, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.BytesTransferred)
+ if err != nil {
+ err = msgp.WrapError(err, "BytesTransferred")
+ return
+ }
+ // write "bf"
+ err = en.Append(0xa2, 0x62, 0x66)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.BytesFailed)
+ if err != nil {
+ err = msgp.WrapError(err, "BytesFailed")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *batchJobInfo) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 17
+ // string "v"
+ o = append(o, 0xde, 0x0, 0x11, 0xa1, 0x76)
+ o = msgp.AppendInt(o, z.Version)
+ // string "jid"
+ o = append(o, 0xa3, 0x6a, 0x69, 0x64)
+ o = msgp.AppendString(o, z.JobID)
+ // string "jt"
+ o = append(o, 0xa2, 0x6a, 0x74)
+ o = msgp.AppendString(o, z.JobType)
+ // string "st"
+ o = append(o, 0xa2, 0x73, 0x74)
+ o = msgp.AppendTime(o, z.StartTime)
+ // string "lu"
+ o = append(o, 0xa2, 0x6c, 0x75)
+ o = msgp.AppendTime(o, z.LastUpdate)
+ // string "ra"
+ o = append(o, 0xa2, 0x72, 0x61)
+ o = msgp.AppendInt(o, z.RetryAttempts)
+ // string "at"
+ o = append(o, 0xa2, 0x61, 0x74)
+ o = msgp.AppendInt(o, z.Attempts)
+ // string "cmp"
+ o = append(o, 0xa3, 0x63, 0x6d, 0x70)
+ o = msgp.AppendBool(o, z.Complete)
+ // string "fld"
+ o = append(o, 0xa3, 0x66, 0x6c, 0x64)
+ o = msgp.AppendBool(o, z.Failed)
+ // string "lbkt"
+ o = append(o, 0xa4, 0x6c, 0x62, 0x6b, 0x74)
+ o = msgp.AppendString(o, z.Bucket)
+ // string "lobj"
+ o = append(o, 0xa4, 0x6c, 0x6f, 0x62, 0x6a)
+ o = msgp.AppendString(o, z.Object)
+ // string "ob"
+ o = append(o, 0xa2, 0x6f, 0x62)
+ o = msgp.AppendInt64(o, z.Objects)
+ // string "dm"
+ o = append(o, 0xa2, 0x64, 0x6d)
+ o = msgp.AppendInt64(o, z.DeleteMarkers)
+ // string "obf"
+ o = append(o, 0xa3, 0x6f, 0x62, 0x66)
+ o = msgp.AppendInt64(o, z.ObjectsFailed)
+ // string "dmf"
+ o = append(o, 0xa3, 0x64, 0x6d, 0x66)
+ o = msgp.AppendInt64(o, z.DeleteMarkersFailed)
+ // string "bt"
+ o = append(o, 0xa2, 0x62, 0x74)
+ o = msgp.AppendInt64(o, z.BytesTransferred)
+ // string "bf"
+ o = append(o, 0xa2, 0x62, 0x66)
+ o = msgp.AppendInt64(o, z.BytesFailed)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *batchJobInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "v":
+ z.Version, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Version")
+ return
+ }
+ case "jid":
+ z.JobID, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "JobID")
+ return
+ }
+ case "jt":
+ z.JobType, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "JobType")
+ return
+ }
+ case "st":
+ z.StartTime, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "StartTime")
+ return
+ }
+ case "lu":
+ z.LastUpdate, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "LastUpdate")
+ return
+ }
+ case "ra":
+ z.RetryAttempts, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "RetryAttempts")
+ return
+ }
+ case "at":
+ z.Attempts, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Attempts")
+ return
+ }
+ case "cmp":
+ z.Complete, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Complete")
+ return
+ }
+ case "fld":
+ z.Failed, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Failed")
+ return
+ }
+ case "lbkt":
+ z.Bucket, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "lobj":
+ z.Object, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ case "ob":
+ z.Objects, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Objects")
+ return
+ }
+ case "dm":
+ z.DeleteMarkers, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "DeleteMarkers")
+ return
+ }
+ case "obf":
+ z.ObjectsFailed, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjectsFailed")
+ return
+ }
+ case "dmf":
+ z.DeleteMarkersFailed, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "DeleteMarkersFailed")
+ return
+ }
+ case "bt":
+ z.BytesTransferred, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "BytesTransferred")
+ return
+ }
+ case "bf":
+ z.BytesFailed, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "BytesFailed")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *batchJobInfo) Msgsize() (s int) {
+ s = 3 + 2 + msgp.IntSize + 4 + msgp.StringPrefixSize + len(z.JobID) + 3 + msgp.StringPrefixSize + len(z.JobType) + 3 + msgp.TimeSize + 3 + msgp.TimeSize + 3 + msgp.IntSize + 3 + msgp.IntSize + 4 + msgp.BoolSize + 4 + msgp.BoolSize + 5 + msgp.StringPrefixSize + len(z.Bucket) + 5 + msgp.StringPrefixSize + len(z.Object) + 3 + msgp.Int64Size + 3 + msgp.Int64Size + 4 + msgp.Int64Size + 4 + msgp.Int64Size + 3 + msgp.Int64Size + 3 + msgp.Int64Size
+ return
+}
diff --git a/cmd/batch-handlers_gen_test.go b/cmd/batch-handlers_gen_test.go
new file mode 100644
index 00000000..be48e477
--- /dev/null
+++ b/cmd/batch-handlers_gen_test.go
@@ -0,0 +1,349 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+func TestMarshalUnmarshalBatchJobPrefix(t *testing.T) {
+ v := BatchJobPrefix{}
+ 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 BenchmarkMarshalMsgBatchJobPrefix(b *testing.B) {
+ v := BatchJobPrefix{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBatchJobPrefix(b *testing.B) {
+ v := BatchJobPrefix{}
+ 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 BenchmarkUnmarshalBatchJobPrefix(b *testing.B) {
+ v := BatchJobPrefix{}
+ 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 TestEncodeDecodeBatchJobPrefix(t *testing.T) {
+ v := BatchJobPrefix{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBatchJobPrefix Msgsize() is inaccurate")
+ }
+
+ vn := BatchJobPrefix{}
+ 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 BenchmarkEncodeBatchJobPrefix(b *testing.B) {
+ v := BatchJobPrefix{}
+ 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 BenchmarkDecodeBatchJobPrefix(b *testing.B) {
+ v := BatchJobPrefix{}
+ 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 TestMarshalUnmarshalBatchJobRequest(t *testing.T) {
+ v := BatchJobRequest{}
+ 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 BenchmarkMarshalMsgBatchJobRequest(b *testing.B) {
+ v := BatchJobRequest{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBatchJobRequest(b *testing.B) {
+ v := BatchJobRequest{}
+ 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 BenchmarkUnmarshalBatchJobRequest(b *testing.B) {
+ v := BatchJobRequest{}
+ 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 TestEncodeDecodeBatchJobRequest(t *testing.T) {
+ v := BatchJobRequest{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBatchJobRequest Msgsize() is inaccurate")
+ }
+
+ vn := BatchJobRequest{}
+ 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 BenchmarkEncodeBatchJobRequest(b *testing.B) {
+ v := BatchJobRequest{}
+ 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 BenchmarkDecodeBatchJobRequest(b *testing.B) {
+ v := BatchJobRequest{}
+ 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 TestMarshalUnmarshalbatchJobInfo(t *testing.T) {
+ v := batchJobInfo{}
+ 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 BenchmarkMarshalMsgbatchJobInfo(b *testing.B) {
+ v := batchJobInfo{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgbatchJobInfo(b *testing.B) {
+ v := batchJobInfo{}
+ 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 BenchmarkUnmarshalbatchJobInfo(b *testing.B) {
+ v := batchJobInfo{}
+ 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 TestEncodeDecodebatchJobInfo(t *testing.T) {
+ v := batchJobInfo{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodebatchJobInfo Msgsize() is inaccurate")
+ }
+
+ vn := batchJobInfo{}
+ 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 BenchmarkEncodebatchJobInfo(b *testing.B) {
+ v := batchJobInfo{}
+ 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 BenchmarkDecodebatchJobInfo(b *testing.B) {
+ v := batchJobInfo{}
+ 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)
+ }
+ }
+}
diff --git a/cmd/batch-handlers_test.go b/cmd/batch-handlers_test.go
new file mode 100644
index 00000000..213e7670
--- /dev/null
+++ b/cmd/batch-handlers_test.go
@@ -0,0 +1,75 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "slices"
+ "testing"
+
+ "gopkg.in/yaml.v3"
+)
+
+func TestBatchJobPrefix_UnmarshalYAML(t *testing.T) {
+ type args struct {
+ yamlStr string
+ }
+ type PrefixTemp struct {
+ Prefix BatchJobPrefix `yaml:"prefix"`
+ }
+ tests := []struct {
+ name string
+ b PrefixTemp
+ args args
+ want []string
+ wantErr bool
+ }{
+ {
+ name: "test1",
+ b: PrefixTemp{},
+ args: args{
+ yamlStr: `
+prefix: "foo"
+`,
+ },
+ want: []string{"foo"},
+ wantErr: false,
+ },
+ {
+ name: "test2",
+ b: PrefixTemp{},
+ args: args{
+ yamlStr: `
+prefix:
+ - "foo"
+ - "bar"
+`,
+ },
+ want: []string{"foo", "bar"},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if err := yaml.Unmarshal([]byte(tt.args.yamlStr), &tt.b); (err != nil) != tt.wantErr {
+ t.Errorf("UnmarshalYAML() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ if !slices.Equal(tt.b.Prefix.F(), tt.want) {
+ t.Errorf("UnmarshalYAML() = %v, want %v", tt.b.Prefix.F(), tt.want)
+ }
+ })
+ }
+}
diff --git a/cmd/batch-job-common-types.go b/cmd/batch-job-common-types.go
new file mode 100644
index 00000000..d02a71dd
--- /dev/null
+++ b/cmd/batch-job-common-types.go
@@ -0,0 +1,290 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/dustin/go-humanize"
+ "github.com/minio/pkg/v3/wildcard"
+ "gopkg.in/yaml.v3"
+)
+
+//go:generate msgp -file $GOFILE
+//msgp:ignore BatchJobYamlErr
+
+// BatchJobYamlErr can be used to return yaml validation errors with line,
+// column information guiding user to fix syntax errors
+type BatchJobYamlErr struct {
+ line, col int
+ msg string
+}
+
+// message returns the error message excluding line, col information.
+// Intended to be used in unit tests.
+func (b BatchJobYamlErr) message() string {
+ return b.msg
+}
+
+// Error implements Error interface
+func (b BatchJobYamlErr) Error() string {
+ return fmt.Sprintf("%s\n Hint: error near line: %d, col: %d", b.msg, b.line, b.col)
+}
+
+// BatchJobKV is a key-value data type which supports wildcard matching
+type BatchJobKV struct {
+ line, col int
+ Key string `yaml:"key" json:"key"`
+ Value string `yaml:"value" json:"value"`
+}
+
+var _ yaml.Unmarshaler = &BatchJobKV{}
+
+// UnmarshalYAML - BatchJobKV extends default unmarshal to extract line, col information.
+func (kv *BatchJobKV) UnmarshalYAML(val *yaml.Node) error {
+ type jobKV BatchJobKV
+ var tmp jobKV
+ err := val.Decode(&tmp)
+ if err != nil {
+ return err
+ }
+ *kv = BatchJobKV(tmp)
+ kv.line, kv.col = val.Line, val.Column
+ return nil
+}
+
+// Validate returns an error if key is empty
+func (kv BatchJobKV) Validate() error {
+ if kv.Key == "" {
+ return BatchJobYamlErr{
+ line: kv.line,
+ col: kv.col,
+ msg: "key can't be empty",
+ }
+ }
+ return nil
+}
+
+// Empty indicates if kv is not set
+func (kv BatchJobKV) Empty() bool {
+ return kv.Key == "" && kv.Value == ""
+}
+
+// Match matches input kv with kv, value will be wildcard matched depending on the user input
+func (kv BatchJobKV) Match(ikv BatchJobKV) bool {
+ if kv.Empty() {
+ return true
+ }
+ if strings.EqualFold(kv.Key, ikv.Key) {
+ return wildcard.Match(kv.Value, ikv.Value)
+ }
+ return false
+}
+
+// BatchJobNotification stores notification endpoint and token information.
+// Used by batch jobs to notify of their status.
+type BatchJobNotification struct {
+ line, col int
+ Endpoint string `yaml:"endpoint" json:"endpoint"`
+ Token string `yaml:"token" json:"token"`
+}
+
+var _ yaml.Unmarshaler = &BatchJobNotification{}
+
+// UnmarshalYAML - BatchJobNotification extends unmarshal to extract line, column information
+func (b *BatchJobNotification) UnmarshalYAML(val *yaml.Node) error {
+ type notification BatchJobNotification
+ var tmp notification
+ err := val.Decode(&tmp)
+ if err != nil {
+ return err
+ }
+
+ *b = BatchJobNotification(tmp)
+ b.line, b.col = val.Line, val.Column
+ return nil
+}
+
+// BatchJobRetry stores retry configuration used in the event of failures.
+type BatchJobRetry struct {
+ line, col int
+ Attempts int `yaml:"attempts" json:"attempts"` // number of retry attempts
+ Delay time.Duration `yaml:"delay" json:"delay"` // delay between each retries
+}
+
+var _ yaml.Unmarshaler = &BatchJobRetry{}
+
+// UnmarshalYAML - BatchJobRetry extends unmarshal to extract line, column information
+func (r *BatchJobRetry) UnmarshalYAML(val *yaml.Node) error {
+ type retry BatchJobRetry
+ var tmp retry
+ err := val.Decode(&tmp)
+ if err != nil {
+ return err
+ }
+
+ *r = BatchJobRetry(tmp)
+ r.line, r.col = val.Line, val.Column
+ return nil
+}
+
+// Validate validates input replicate retries.
+func (r BatchJobRetry) Validate() error {
+ if r.Attempts < 0 {
+ return BatchJobYamlErr{
+ line: r.line,
+ col: r.col,
+ msg: "Invalid arguments specified",
+ }
+ }
+
+ if r.Delay < 0 {
+ return BatchJobYamlErr{
+ line: r.line,
+ col: r.col,
+ msg: "Invalid arguments specified",
+ }
+ }
+
+ return nil
+}
+
+// # snowball based archive transfer is by default enabled when source
+// # is local and target is remote which is also minio.
+// snowball:
+// disable: false # optionally turn-off snowball archive transfer
+// batch: 100 # upto this many objects per archive
+// inmemory: true # indicates if the archive must be staged locally or in-memory
+// compress: true # S2/Snappy compressed archive
+// smallerThan: 5MiB # create archive for all objects smaller than 5MiB
+// skipErrs: false # skips any source side read() errors
+
+// BatchJobSnowball describes the snowball feature when replicating objects from a local source to a remote target
+type BatchJobSnowball struct {
+ line, col int
+ Disable *bool `yaml:"disable" json:"disable"`
+ Batch *int `yaml:"batch" json:"batch"`
+ InMemory *bool `yaml:"inmemory" json:"inmemory"`
+ Compress *bool `yaml:"compress" json:"compress"`
+ SmallerThan *string `yaml:"smallerThan" json:"smallerThan"`
+ SkipErrs *bool `yaml:"skipErrs" json:"skipErrs"`
+}
+
+var _ yaml.Unmarshaler = &BatchJobSnowball{}
+
+// UnmarshalYAML - BatchJobSnowball extends unmarshal to extract line, column information
+func (b *BatchJobSnowball) UnmarshalYAML(val *yaml.Node) error {
+ type snowball BatchJobSnowball
+ var tmp snowball
+ err := val.Decode(&tmp)
+ if err != nil {
+ return err
+ }
+
+ *b = BatchJobSnowball(tmp)
+ b.line, b.col = val.Line, val.Column
+ return nil
+}
+
+// Validate the snowball parameters in the job description
+func (b BatchJobSnowball) Validate() error {
+ if *b.Batch <= 0 {
+ return BatchJobYamlErr{
+ line: b.line,
+ col: b.col,
+ msg: "batch number should be non positive zero",
+ }
+ }
+ _, err := humanize.ParseBytes(*b.SmallerThan)
+ if err != nil {
+ return BatchJobYamlErr{
+ line: b.line,
+ col: b.col,
+ msg: err.Error(),
+ }
+ }
+ return nil
+}
+
+// BatchJobSizeFilter supports size based filters - LesserThan and GreaterThan
+type BatchJobSizeFilter struct {
+ line, col int
+ UpperBound BatchJobSize `yaml:"lessThan" json:"lessThan"`
+ LowerBound BatchJobSize `yaml:"greaterThan" json:"greaterThan"`
+}
+
+// UnmarshalYAML - BatchJobSizeFilter extends unmarshal to extract line, column information
+func (sf *BatchJobSizeFilter) UnmarshalYAML(val *yaml.Node) error {
+ type sizeFilter BatchJobSizeFilter
+ var tmp sizeFilter
+ err := val.Decode(&tmp)
+ if err != nil {
+ return err
+ }
+
+ *sf = BatchJobSizeFilter(tmp)
+ sf.line, sf.col = val.Line, val.Column
+ return nil
+}
+
+// InRange returns true in the following cases and false otherwise,
+// - sf.LowerBound < sz, when sf.LowerBound alone is specified
+// - sz < sf.UpperBound, when sf.UpperBound alone is specified
+// - sf.LowerBound < sz < sf.UpperBound when both are specified,
+func (sf BatchJobSizeFilter) InRange(sz int64) bool {
+ if sf.UpperBound > 0 && sz > int64(sf.UpperBound) {
+ return false
+ }
+
+ if sf.LowerBound > 0 && sz < int64(sf.LowerBound) {
+ return false
+ }
+ return true
+}
+
+// Validate checks if sf is a valid batch-job size filter
+func (sf BatchJobSizeFilter) Validate() error {
+ if sf.LowerBound > 0 && sf.UpperBound > 0 && sf.LowerBound >= sf.UpperBound {
+ return BatchJobYamlErr{
+ line: sf.line,
+ col: sf.col,
+ msg: "invalid batch-job size filter",
+ }
+ }
+ return nil
+}
+
+// BatchJobSize supports humanized byte values in yaml files type BatchJobSize uint64
+type BatchJobSize int64
+
+// UnmarshalYAML to parse humanized byte values
+func (s *BatchJobSize) UnmarshalYAML(unmarshal func(any) error) error {
+ var batchExpireSz string
+ err := unmarshal(&batchExpireSz)
+ if err != nil {
+ return err
+ }
+ sz, err := humanize.ParseBytes(batchExpireSz)
+ if err != nil {
+ return err
+ }
+ *s = BatchJobSize(sz)
+ return nil
+}
diff --git a/cmd/batch-job-common-types_gen.go b/cmd/batch-job-common-types_gen.go
new file mode 100644
index 00000000..12edddf4
--- /dev/null
+++ b/cmd/batch-job-common-types_gen.go
@@ -0,0 +1,1054 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "github.com/tinylib/msgp/msgp"
+)
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchJobKV) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Key":
+ z.Key, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Key")
+ return
+ }
+ case "Value":
+ z.Value, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Value")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z BatchJobKV) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 2
+ // write "Key"
+ err = en.Append(0x82, 0xa3, 0x4b, 0x65, 0x79)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Key)
+ if err != nil {
+ err = msgp.WrapError(err, "Key")
+ return
+ }
+ // write "Value"
+ err = en.Append(0xa5, 0x56, 0x61, 0x6c, 0x75, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Value)
+ if err != nil {
+ err = msgp.WrapError(err, "Value")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z BatchJobKV) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 2
+ // string "Key"
+ o = append(o, 0x82, 0xa3, 0x4b, 0x65, 0x79)
+ o = msgp.AppendString(o, z.Key)
+ // string "Value"
+ o = append(o, 0xa5, 0x56, 0x61, 0x6c, 0x75, 0x65)
+ o = msgp.AppendString(o, z.Value)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BatchJobKV) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Key":
+ z.Key, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Key")
+ return
+ }
+ case "Value":
+ z.Value, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Value")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z BatchJobKV) Msgsize() (s int) {
+ s = 1 + 4 + msgp.StringPrefixSize + len(z.Key) + 6 + msgp.StringPrefixSize + len(z.Value)
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchJobNotification) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Endpoint":
+ z.Endpoint, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Endpoint")
+ return
+ }
+ case "Token":
+ z.Token, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Token")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z BatchJobNotification) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 2
+ // write "Endpoint"
+ err = en.Append(0x82, 0xa8, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Endpoint)
+ if err != nil {
+ err = msgp.WrapError(err, "Endpoint")
+ return
+ }
+ // write "Token"
+ err = en.Append(0xa5, 0x54, 0x6f, 0x6b, 0x65, 0x6e)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Token)
+ if err != nil {
+ err = msgp.WrapError(err, "Token")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z BatchJobNotification) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 2
+ // string "Endpoint"
+ o = append(o, 0x82, 0xa8, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74)
+ o = msgp.AppendString(o, z.Endpoint)
+ // string "Token"
+ o = append(o, 0xa5, 0x54, 0x6f, 0x6b, 0x65, 0x6e)
+ o = msgp.AppendString(o, z.Token)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BatchJobNotification) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Endpoint":
+ z.Endpoint, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Endpoint")
+ return
+ }
+ case "Token":
+ z.Token, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Token")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z BatchJobNotification) Msgsize() (s int) {
+ s = 1 + 9 + msgp.StringPrefixSize + len(z.Endpoint) + 6 + msgp.StringPrefixSize + len(z.Token)
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchJobRetry) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Attempts":
+ z.Attempts, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "Attempts")
+ return
+ }
+ case "Delay":
+ z.Delay, err = dc.ReadDuration()
+ if err != nil {
+ err = msgp.WrapError(err, "Delay")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z BatchJobRetry) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 2
+ // write "Attempts"
+ err = en.Append(0x82, 0xa8, 0x41, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.Attempts)
+ if err != nil {
+ err = msgp.WrapError(err, "Attempts")
+ return
+ }
+ // write "Delay"
+ err = en.Append(0xa5, 0x44, 0x65, 0x6c, 0x61, 0x79)
+ if err != nil {
+ return
+ }
+ err = en.WriteDuration(z.Delay)
+ if err != nil {
+ err = msgp.WrapError(err, "Delay")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z BatchJobRetry) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 2
+ // string "Attempts"
+ o = append(o, 0x82, 0xa8, 0x41, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x73)
+ o = msgp.AppendInt(o, z.Attempts)
+ // string "Delay"
+ o = append(o, 0xa5, 0x44, 0x65, 0x6c, 0x61, 0x79)
+ o = msgp.AppendDuration(o, z.Delay)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BatchJobRetry) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Attempts":
+ z.Attempts, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Attempts")
+ return
+ }
+ case "Delay":
+ z.Delay, bts, err = msgp.ReadDurationBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Delay")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z BatchJobRetry) Msgsize() (s int) {
+ s = 1 + 9 + msgp.IntSize + 6 + msgp.DurationSize
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchJobSize) DecodeMsg(dc *msgp.Reader) (err error) {
+ {
+ var zb0001 int64
+ zb0001, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ (*z) = BatchJobSize(zb0001)
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z BatchJobSize) EncodeMsg(en *msgp.Writer) (err error) {
+ err = en.WriteInt64(int64(z))
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z BatchJobSize) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ o = msgp.AppendInt64(o, int64(z))
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BatchJobSize) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ {
+ var zb0001 int64
+ zb0001, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ (*z) = BatchJobSize(zb0001)
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z BatchJobSize) Msgsize() (s int) {
+ s = msgp.Int64Size
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchJobSizeFilter) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "UpperBound":
+ {
+ var zb0002 int64
+ zb0002, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "UpperBound")
+ return
+ }
+ z.UpperBound = BatchJobSize(zb0002)
+ }
+ case "LowerBound":
+ {
+ var zb0003 int64
+ zb0003, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "LowerBound")
+ return
+ }
+ z.LowerBound = BatchJobSize(zb0003)
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z BatchJobSizeFilter) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 2
+ // write "UpperBound"
+ err = en.Append(0x82, 0xaa, 0x55, 0x70, 0x70, 0x65, 0x72, 0x42, 0x6f, 0x75, 0x6e, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(int64(z.UpperBound))
+ if err != nil {
+ err = msgp.WrapError(err, "UpperBound")
+ return
+ }
+ // write "LowerBound"
+ err = en.Append(0xaa, 0x4c, 0x6f, 0x77, 0x65, 0x72, 0x42, 0x6f, 0x75, 0x6e, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(int64(z.LowerBound))
+ if err != nil {
+ err = msgp.WrapError(err, "LowerBound")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z BatchJobSizeFilter) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 2
+ // string "UpperBound"
+ o = append(o, 0x82, 0xaa, 0x55, 0x70, 0x70, 0x65, 0x72, 0x42, 0x6f, 0x75, 0x6e, 0x64)
+ o = msgp.AppendInt64(o, int64(z.UpperBound))
+ // string "LowerBound"
+ o = append(o, 0xaa, 0x4c, 0x6f, 0x77, 0x65, 0x72, 0x42, 0x6f, 0x75, 0x6e, 0x64)
+ o = msgp.AppendInt64(o, int64(z.LowerBound))
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BatchJobSizeFilter) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "UpperBound":
+ {
+ var zb0002 int64
+ zb0002, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "UpperBound")
+ return
+ }
+ z.UpperBound = BatchJobSize(zb0002)
+ }
+ case "LowerBound":
+ {
+ var zb0003 int64
+ zb0003, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "LowerBound")
+ return
+ }
+ z.LowerBound = BatchJobSize(zb0003)
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z BatchJobSizeFilter) Msgsize() (s int) {
+ s = 1 + 11 + msgp.Int64Size + 11 + msgp.Int64Size
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchJobSnowball) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Disable":
+ if dc.IsNil() {
+ err = dc.ReadNil()
+ if err != nil {
+ err = msgp.WrapError(err, "Disable")
+ return
+ }
+ z.Disable = nil
+ } else {
+ if z.Disable == nil {
+ z.Disable = new(bool)
+ }
+ *z.Disable, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "Disable")
+ return
+ }
+ }
+ case "Batch":
+ if dc.IsNil() {
+ err = dc.ReadNil()
+ if err != nil {
+ err = msgp.WrapError(err, "Batch")
+ return
+ }
+ z.Batch = nil
+ } else {
+ if z.Batch == nil {
+ z.Batch = new(int)
+ }
+ *z.Batch, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "Batch")
+ return
+ }
+ }
+ case "InMemory":
+ if dc.IsNil() {
+ err = dc.ReadNil()
+ if err != nil {
+ err = msgp.WrapError(err, "InMemory")
+ return
+ }
+ z.InMemory = nil
+ } else {
+ if z.InMemory == nil {
+ z.InMemory = new(bool)
+ }
+ *z.InMemory, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "InMemory")
+ return
+ }
+ }
+ case "Compress":
+ if dc.IsNil() {
+ err = dc.ReadNil()
+ if err != nil {
+ err = msgp.WrapError(err, "Compress")
+ return
+ }
+ z.Compress = nil
+ } else {
+ if z.Compress == nil {
+ z.Compress = new(bool)
+ }
+ *z.Compress, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "Compress")
+ return
+ }
+ }
+ case "SmallerThan":
+ if dc.IsNil() {
+ err = dc.ReadNil()
+ if err != nil {
+ err = msgp.WrapError(err, "SmallerThan")
+ return
+ }
+ z.SmallerThan = nil
+ } else {
+ if z.SmallerThan == nil {
+ z.SmallerThan = new(string)
+ }
+ *z.SmallerThan, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "SmallerThan")
+ return
+ }
+ }
+ case "SkipErrs":
+ if dc.IsNil() {
+ err = dc.ReadNil()
+ if err != nil {
+ err = msgp.WrapError(err, "SkipErrs")
+ return
+ }
+ z.SkipErrs = nil
+ } else {
+ if z.SkipErrs == nil {
+ z.SkipErrs = new(bool)
+ }
+ *z.SkipErrs, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "SkipErrs")
+ return
+ }
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *BatchJobSnowball) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 6
+ // write "Disable"
+ err = en.Append(0x86, 0xa7, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65)
+ if err != nil {
+ return
+ }
+ if z.Disable == nil {
+ err = en.WriteNil()
+ if err != nil {
+ return
+ }
+ } else {
+ err = en.WriteBool(*z.Disable)
+ if err != nil {
+ err = msgp.WrapError(err, "Disable")
+ return
+ }
+ }
+ // write "Batch"
+ err = en.Append(0xa5, 0x42, 0x61, 0x74, 0x63, 0x68)
+ if err != nil {
+ return
+ }
+ if z.Batch == nil {
+ err = en.WriteNil()
+ if err != nil {
+ return
+ }
+ } else {
+ err = en.WriteInt(*z.Batch)
+ if err != nil {
+ err = msgp.WrapError(err, "Batch")
+ return
+ }
+ }
+ // write "InMemory"
+ err = en.Append(0xa8, 0x49, 0x6e, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79)
+ if err != nil {
+ return
+ }
+ if z.InMemory == nil {
+ err = en.WriteNil()
+ if err != nil {
+ return
+ }
+ } else {
+ err = en.WriteBool(*z.InMemory)
+ if err != nil {
+ err = msgp.WrapError(err, "InMemory")
+ return
+ }
+ }
+ // write "Compress"
+ err = en.Append(0xa8, 0x43, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73)
+ if err != nil {
+ return
+ }
+ if z.Compress == nil {
+ err = en.WriteNil()
+ if err != nil {
+ return
+ }
+ } else {
+ err = en.WriteBool(*z.Compress)
+ if err != nil {
+ err = msgp.WrapError(err, "Compress")
+ return
+ }
+ }
+ // write "SmallerThan"
+ err = en.Append(0xab, 0x53, 0x6d, 0x61, 0x6c, 0x6c, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e)
+ if err != nil {
+ return
+ }
+ if z.SmallerThan == nil {
+ err = en.WriteNil()
+ if err != nil {
+ return
+ }
+ } else {
+ err = en.WriteString(*z.SmallerThan)
+ if err != nil {
+ err = msgp.WrapError(err, "SmallerThan")
+ return
+ }
+ }
+ // write "SkipErrs"
+ err = en.Append(0xa8, 0x53, 0x6b, 0x69, 0x70, 0x45, 0x72, 0x72, 0x73)
+ if err != nil {
+ return
+ }
+ if z.SkipErrs == nil {
+ err = en.WriteNil()
+ if err != nil {
+ return
+ }
+ } else {
+ err = en.WriteBool(*z.SkipErrs)
+ if err != nil {
+ err = msgp.WrapError(err, "SkipErrs")
+ return
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *BatchJobSnowball) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 6
+ // string "Disable"
+ o = append(o, 0x86, 0xa7, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65)
+ if z.Disable == nil {
+ o = msgp.AppendNil(o)
+ } else {
+ o = msgp.AppendBool(o, *z.Disable)
+ }
+ // string "Batch"
+ o = append(o, 0xa5, 0x42, 0x61, 0x74, 0x63, 0x68)
+ if z.Batch == nil {
+ o = msgp.AppendNil(o)
+ } else {
+ o = msgp.AppendInt(o, *z.Batch)
+ }
+ // string "InMemory"
+ o = append(o, 0xa8, 0x49, 0x6e, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79)
+ if z.InMemory == nil {
+ o = msgp.AppendNil(o)
+ } else {
+ o = msgp.AppendBool(o, *z.InMemory)
+ }
+ // string "Compress"
+ o = append(o, 0xa8, 0x43, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73)
+ if z.Compress == nil {
+ o = msgp.AppendNil(o)
+ } else {
+ o = msgp.AppendBool(o, *z.Compress)
+ }
+ // string "SmallerThan"
+ o = append(o, 0xab, 0x53, 0x6d, 0x61, 0x6c, 0x6c, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e)
+ if z.SmallerThan == nil {
+ o = msgp.AppendNil(o)
+ } else {
+ o = msgp.AppendString(o, *z.SmallerThan)
+ }
+ // string "SkipErrs"
+ o = append(o, 0xa8, 0x53, 0x6b, 0x69, 0x70, 0x45, 0x72, 0x72, 0x73)
+ if z.SkipErrs == nil {
+ o = msgp.AppendNil(o)
+ } else {
+ o = msgp.AppendBool(o, *z.SkipErrs)
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BatchJobSnowball) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Disable":
+ if msgp.IsNil(bts) {
+ bts, err = msgp.ReadNilBytes(bts)
+ if err != nil {
+ return
+ }
+ z.Disable = nil
+ } else {
+ if z.Disable == nil {
+ z.Disable = new(bool)
+ }
+ *z.Disable, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Disable")
+ return
+ }
+ }
+ case "Batch":
+ if msgp.IsNil(bts) {
+ bts, err = msgp.ReadNilBytes(bts)
+ if err != nil {
+ return
+ }
+ z.Batch = nil
+ } else {
+ if z.Batch == nil {
+ z.Batch = new(int)
+ }
+ *z.Batch, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Batch")
+ return
+ }
+ }
+ case "InMemory":
+ if msgp.IsNil(bts) {
+ bts, err = msgp.ReadNilBytes(bts)
+ if err != nil {
+ return
+ }
+ z.InMemory = nil
+ } else {
+ if z.InMemory == nil {
+ z.InMemory = new(bool)
+ }
+ *z.InMemory, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "InMemory")
+ return
+ }
+ }
+ case "Compress":
+ if msgp.IsNil(bts) {
+ bts, err = msgp.ReadNilBytes(bts)
+ if err != nil {
+ return
+ }
+ z.Compress = nil
+ } else {
+ if z.Compress == nil {
+ z.Compress = new(bool)
+ }
+ *z.Compress, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Compress")
+ return
+ }
+ }
+ case "SmallerThan":
+ if msgp.IsNil(bts) {
+ bts, err = msgp.ReadNilBytes(bts)
+ if err != nil {
+ return
+ }
+ z.SmallerThan = nil
+ } else {
+ if z.SmallerThan == nil {
+ z.SmallerThan = new(string)
+ }
+ *z.SmallerThan, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "SmallerThan")
+ return
+ }
+ }
+ case "SkipErrs":
+ if msgp.IsNil(bts) {
+ bts, err = msgp.ReadNilBytes(bts)
+ if err != nil {
+ return
+ }
+ z.SkipErrs = nil
+ } else {
+ if z.SkipErrs == nil {
+ z.SkipErrs = new(bool)
+ }
+ *z.SkipErrs, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "SkipErrs")
+ return
+ }
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *BatchJobSnowball) Msgsize() (s int) {
+ s = 1 + 8
+ if z.Disable == nil {
+ s += msgp.NilSize
+ } else {
+ s += msgp.BoolSize
+ }
+ s += 6
+ if z.Batch == nil {
+ s += msgp.NilSize
+ } else {
+ s += msgp.IntSize
+ }
+ s += 9
+ if z.InMemory == nil {
+ s += msgp.NilSize
+ } else {
+ s += msgp.BoolSize
+ }
+ s += 9
+ if z.Compress == nil {
+ s += msgp.NilSize
+ } else {
+ s += msgp.BoolSize
+ }
+ s += 12
+ if z.SmallerThan == nil {
+ s += msgp.NilSize
+ } else {
+ s += msgp.StringPrefixSize + len(*z.SmallerThan)
+ }
+ s += 9
+ if z.SkipErrs == nil {
+ s += msgp.NilSize
+ } else {
+ s += msgp.BoolSize
+ }
+ return
+}
diff --git a/cmd/batch-job-common-types_gen_test.go b/cmd/batch-job-common-types_gen_test.go
new file mode 100644
index 00000000..3e0684a8
--- /dev/null
+++ b/cmd/batch-job-common-types_gen_test.go
@@ -0,0 +1,575 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+func TestMarshalUnmarshalBatchJobKV(t *testing.T) {
+ v := BatchJobKV{}
+ 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 BenchmarkMarshalMsgBatchJobKV(b *testing.B) {
+ v := BatchJobKV{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBatchJobKV(b *testing.B) {
+ v := BatchJobKV{}
+ 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 BenchmarkUnmarshalBatchJobKV(b *testing.B) {
+ v := BatchJobKV{}
+ 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 TestEncodeDecodeBatchJobKV(t *testing.T) {
+ v := BatchJobKV{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBatchJobKV Msgsize() is inaccurate")
+ }
+
+ vn := BatchJobKV{}
+ 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 BenchmarkEncodeBatchJobKV(b *testing.B) {
+ v := BatchJobKV{}
+ 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 BenchmarkDecodeBatchJobKV(b *testing.B) {
+ v := BatchJobKV{}
+ 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 TestMarshalUnmarshalBatchJobNotification(t *testing.T) {
+ v := BatchJobNotification{}
+ 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 BenchmarkMarshalMsgBatchJobNotification(b *testing.B) {
+ v := BatchJobNotification{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBatchJobNotification(b *testing.B) {
+ v := BatchJobNotification{}
+ 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 BenchmarkUnmarshalBatchJobNotification(b *testing.B) {
+ v := BatchJobNotification{}
+ 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 TestEncodeDecodeBatchJobNotification(t *testing.T) {
+ v := BatchJobNotification{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBatchJobNotification Msgsize() is inaccurate")
+ }
+
+ vn := BatchJobNotification{}
+ 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 BenchmarkEncodeBatchJobNotification(b *testing.B) {
+ v := BatchJobNotification{}
+ 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 BenchmarkDecodeBatchJobNotification(b *testing.B) {
+ v := BatchJobNotification{}
+ 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 TestMarshalUnmarshalBatchJobRetry(t *testing.T) {
+ v := BatchJobRetry{}
+ 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 BenchmarkMarshalMsgBatchJobRetry(b *testing.B) {
+ v := BatchJobRetry{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBatchJobRetry(b *testing.B) {
+ v := BatchJobRetry{}
+ 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 BenchmarkUnmarshalBatchJobRetry(b *testing.B) {
+ v := BatchJobRetry{}
+ 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 TestEncodeDecodeBatchJobRetry(t *testing.T) {
+ v := BatchJobRetry{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBatchJobRetry Msgsize() is inaccurate")
+ }
+
+ vn := BatchJobRetry{}
+ 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 BenchmarkEncodeBatchJobRetry(b *testing.B) {
+ v := BatchJobRetry{}
+ 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 BenchmarkDecodeBatchJobRetry(b *testing.B) {
+ v := BatchJobRetry{}
+ 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 TestMarshalUnmarshalBatchJobSizeFilter(t *testing.T) {
+ v := BatchJobSizeFilter{}
+ 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 BenchmarkMarshalMsgBatchJobSizeFilter(b *testing.B) {
+ v := BatchJobSizeFilter{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBatchJobSizeFilter(b *testing.B) {
+ v := BatchJobSizeFilter{}
+ 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 BenchmarkUnmarshalBatchJobSizeFilter(b *testing.B) {
+ v := BatchJobSizeFilter{}
+ 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 TestEncodeDecodeBatchJobSizeFilter(t *testing.T) {
+ v := BatchJobSizeFilter{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBatchJobSizeFilter Msgsize() is inaccurate")
+ }
+
+ vn := BatchJobSizeFilter{}
+ 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 BenchmarkEncodeBatchJobSizeFilter(b *testing.B) {
+ v := BatchJobSizeFilter{}
+ 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 BenchmarkDecodeBatchJobSizeFilter(b *testing.B) {
+ v := BatchJobSizeFilter{}
+ 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 TestMarshalUnmarshalBatchJobSnowball(t *testing.T) {
+ v := BatchJobSnowball{}
+ 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 BenchmarkMarshalMsgBatchJobSnowball(b *testing.B) {
+ v := BatchJobSnowball{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBatchJobSnowball(b *testing.B) {
+ v := BatchJobSnowball{}
+ 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 BenchmarkUnmarshalBatchJobSnowball(b *testing.B) {
+ v := BatchJobSnowball{}
+ 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 TestEncodeDecodeBatchJobSnowball(t *testing.T) {
+ v := BatchJobSnowball{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBatchJobSnowball Msgsize() is inaccurate")
+ }
+
+ vn := BatchJobSnowball{}
+ 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 BenchmarkEncodeBatchJobSnowball(b *testing.B) {
+ v := BatchJobSnowball{}
+ 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 BenchmarkDecodeBatchJobSnowball(b *testing.B) {
+ v := BatchJobSnowball{}
+ 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)
+ }
+ }
+}
diff --git a/cmd/batch-job-common-types_test.go b/cmd/batch-job-common-types_test.go
new file mode 100644
index 00000000..5281c121
--- /dev/null
+++ b/cmd/batch-job-common-types_test.go
@@ -0,0 +1,148 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestBatchJobSizeInRange(t *testing.T) {
+ tests := []struct {
+ objSize int64
+ sizeFilter BatchJobSizeFilter
+ want bool
+ }{
+ {
+ // 1Mib < 2Mib < 10MiB -> in range
+ objSize: 2 << 20,
+ sizeFilter: BatchJobSizeFilter{
+ UpperBound: 10 << 20,
+ LowerBound: 1 << 20,
+ },
+ want: true,
+ },
+ {
+ // 2KiB < 1 MiB -> out of range from left
+ objSize: 2 << 10,
+ sizeFilter: BatchJobSizeFilter{
+ UpperBound: 10 << 20,
+ LowerBound: 1 << 20,
+ },
+ want: false,
+ },
+ {
+ // 11MiB > 10 MiB -> out of range from right
+ objSize: 11 << 20,
+ sizeFilter: BatchJobSizeFilter{
+ UpperBound: 10 << 20,
+ LowerBound: 1 << 20,
+ },
+ want: false,
+ },
+ {
+ // 2MiB < 10MiB -> in range
+ objSize: 2 << 20,
+ sizeFilter: BatchJobSizeFilter{
+ UpperBound: 10 << 20,
+ },
+ want: true,
+ },
+ {
+ // 2MiB > 1MiB -> in range
+ objSize: 2 << 20,
+ sizeFilter: BatchJobSizeFilter{
+ LowerBound: 1 << 20,
+ },
+ want: true,
+ },
+ }
+
+ for i, test := range tests {
+ t.Run(fmt.Sprintf("test-%d", i+1), func(t *testing.T) {
+ if got := test.sizeFilter.InRange(test.objSize); got != test.want {
+ t.Fatalf("Expected %v but got %v", test.want, got)
+ }
+ })
+ }
+}
+
+func TestBatchJobSizeValidate(t *testing.T) {
+ errInvalidBatchJobSizeFilter := BatchJobYamlErr{
+ msg: "invalid batch-job size filter",
+ }
+
+ tests := []struct {
+ sizeFilter BatchJobSizeFilter
+ err error
+ }{
+ {
+ // Unspecified size filter is a valid filter
+ sizeFilter: BatchJobSizeFilter{
+ UpperBound: 0,
+ LowerBound: 0,
+ },
+ err: nil,
+ },
+ {
+ sizeFilter: BatchJobSizeFilter{
+ UpperBound: 0,
+ LowerBound: 1 << 20,
+ },
+ err: nil,
+ },
+ {
+ sizeFilter: BatchJobSizeFilter{
+ UpperBound: 10 << 20,
+ LowerBound: 0,
+ },
+ err: nil,
+ },
+ {
+ // LowerBound > UpperBound -> empty range
+ sizeFilter: BatchJobSizeFilter{
+ UpperBound: 1 << 20,
+ LowerBound: 10 << 20,
+ },
+ err: errInvalidBatchJobSizeFilter,
+ },
+ {
+ // LowerBound == UpperBound -> empty range
+ sizeFilter: BatchJobSizeFilter{
+ UpperBound: 1 << 20,
+ LowerBound: 1 << 20,
+ },
+ err: errInvalidBatchJobSizeFilter,
+ },
+ }
+ for i, test := range tests {
+ t.Run(fmt.Sprintf("test-%d", i+1), func(t *testing.T) {
+ err := test.sizeFilter.Validate()
+ if err != nil {
+ gotErr := err.(BatchJobYamlErr)
+ testErr := test.err.(BatchJobYamlErr)
+ if gotErr.message() != testErr.message() {
+ t.Fatalf("Expected %v but got %v", test.err, err)
+ }
+ }
+ if err == nil && test.err != nil {
+ t.Fatalf("Expected %v but got nil", test.err)
+ }
+ })
+ }
+}
diff --git a/cmd/batch-replicate.go b/cmd/batch-replicate.go
new file mode 100644
index 00000000..37a1834d
--- /dev/null
+++ b/cmd/batch-replicate.go
@@ -0,0 +1,184 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "time"
+
+ miniogo "github.com/minio/minio-go/v7"
+ "github.com/minio/minio/internal/auth"
+ "github.com/minio/pkg/v3/xtime"
+)
+
+//go:generate msgp -file $GOFILE
+
+// replicate:
+// # source of the objects to be replicated
+// source:
+// type: "minio"
+// bucket: "testbucket"
+// prefix: "spark/"
+//
+// # optional flags based filtering criteria
+// # for source objects
+// flags:
+// filter:
+// newerThan: "7d"
+// olderThan: "7d"
+// createdAfter: "date"
+// createdBefore: "date"
+// tags:
+// - key: "name"
+// value: "value*"
+// metadata:
+// - key: "content-type"
+// value: "image/*"
+// notify:
+// endpoint: "https://splunk-hec.dev.com"
+// token: "Splunk ..." # e.g. "Bearer token"
+//
+// # target where the objects must be replicated
+// target:
+// type: "minio"
+// bucket: "testbucket1"
+// endpoint: "https://play.min.io"
+// path: "on"
+// credentials:
+// accessKey: "minioadmin"
+// secretKey: "minioadmin"
+// sessionToken: ""
+
+// BatchReplicateFilter holds all the filters currently supported for batch replication
+type BatchReplicateFilter struct {
+ NewerThan xtime.Duration `yaml:"newerThan,omitempty" json:"newerThan"`
+ OlderThan xtime.Duration `yaml:"olderThan,omitempty" json:"olderThan"`
+ CreatedAfter time.Time `yaml:"createdAfter,omitempty" json:"createdAfter"`
+ CreatedBefore time.Time `yaml:"createdBefore,omitempty" json:"createdBefore"`
+ Tags []BatchJobKV `yaml:"tags,omitempty" json:"tags"`
+ Metadata []BatchJobKV `yaml:"metadata,omitempty" json:"metadata"`
+}
+
+// BatchJobReplicateFlags various configurations for replication job definition currently includes
+// - filter
+// - notify
+// - retry
+type BatchJobReplicateFlags struct {
+ Filter BatchReplicateFilter `yaml:"filter" json:"filter"`
+ Notify BatchJobNotification `yaml:"notify" json:"notify"`
+ Retry BatchJobRetry `yaml:"retry" json:"retry"`
+}
+
+// BatchJobReplicateResourceType defines the type of batch jobs
+type BatchJobReplicateResourceType string
+
+// Validate validates if the replicate resource type is recognized and supported
+func (t BatchJobReplicateResourceType) Validate() error {
+ switch t {
+ case BatchJobReplicateResourceMinIO:
+ case BatchJobReplicateResourceS3:
+ default:
+ return errInvalidArgument
+ }
+ return nil
+}
+
+func (t BatchJobReplicateResourceType) isMinio() bool {
+ return t == BatchJobReplicateResourceMinIO
+}
+
+// Different types of batch jobs..
+const (
+ BatchJobReplicateResourceMinIO BatchJobReplicateResourceType = "minio"
+ BatchJobReplicateResourceS3 BatchJobReplicateResourceType = "s3"
+
+ // add future targets
+)
+
+// BatchJobReplicateCredentials access credentials for batch replication it may
+// be either for target or source.
+type BatchJobReplicateCredentials struct {
+ AccessKey string `xml:"AccessKeyId" json:"accessKey,omitempty" yaml:"accessKey"`
+ SecretKey string `xml:"SecretAccessKey" json:"secretKey,omitempty" yaml:"secretKey"`
+ SessionToken string `xml:"SessionToken" json:"sessionToken,omitempty" yaml:"sessionToken"`
+}
+
+// Empty indicates if credentials are not set
+func (c BatchJobReplicateCredentials) Empty() bool {
+ return c.AccessKey == "" && c.SecretKey == "" && c.SessionToken == ""
+}
+
+// Validate validates if credentials are valid
+func (c BatchJobReplicateCredentials) Validate() error {
+ if !auth.IsAccessKeyValid(c.AccessKey) || !auth.IsSecretKeyValid(c.SecretKey) {
+ return errInvalidArgument
+ }
+ return nil
+}
+
+// BatchJobReplicateTarget describes target element of the replication job that receives
+// the filtered data from source
+type BatchJobReplicateTarget struct {
+ Type BatchJobReplicateResourceType `yaml:"type" json:"type"`
+ Bucket string `yaml:"bucket" json:"bucket"`
+ Prefix string `yaml:"prefix" json:"prefix"`
+ Endpoint string `yaml:"endpoint" json:"endpoint"`
+ Path string `yaml:"path" json:"path"`
+ Creds BatchJobReplicateCredentials `yaml:"credentials" json:"credentials"`
+}
+
+// ValidPath returns true if path is valid
+func (t BatchJobReplicateTarget) ValidPath() bool {
+ return t.Path == "on" || t.Path == "off" || t.Path == "auto" || t.Path == ""
+}
+
+// BatchJobReplicateSource describes source element of the replication job that is
+// the source of the data for the target
+type BatchJobReplicateSource struct {
+ Type BatchJobReplicateResourceType `yaml:"type" json:"type"`
+ Bucket string `yaml:"bucket" json:"bucket"`
+ Prefix BatchJobPrefix `yaml:"prefix" json:"prefix"`
+ Endpoint string `yaml:"endpoint" json:"endpoint"`
+ Path string `yaml:"path" json:"path"`
+ Creds BatchJobReplicateCredentials `yaml:"credentials" json:"credentials"`
+ Snowball BatchJobSnowball `yaml:"snowball" json:"snowball"`
+}
+
+// ValidPath returns true if path is valid
+func (s BatchJobReplicateSource) ValidPath() bool {
+ switch s.Path {
+ case "on", "off", "auto", "":
+ return true
+ default:
+ return false
+ }
+}
+
+// BatchJobReplicateV1 v1 of batch job replication
+type BatchJobReplicateV1 struct {
+ APIVersion string `yaml:"apiVersion" json:"apiVersion"`
+ Flags BatchJobReplicateFlags `yaml:"flags" json:"flags"`
+ Target BatchJobReplicateTarget `yaml:"target" json:"target"`
+ Source BatchJobReplicateSource `yaml:"source" json:"source"`
+
+ clnt *miniogo.Core `msg:"-"`
+}
+
+// RemoteToLocal returns true if source is remote and target is local
+func (r BatchJobReplicateV1) RemoteToLocal() bool {
+ return !r.Source.Creds.Empty()
+}
diff --git a/cmd/batch-replicate_gen.go b/cmd/batch-replicate_gen.go
new file mode 100644
index 00000000..16f3eed4
--- /dev/null
+++ b/cmd/batch-replicate_gen.go
@@ -0,0 +1,1718 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "github.com/tinylib/msgp/msgp"
+)
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchJobReplicateCredentials) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "AccessKey":
+ z.AccessKey, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "AccessKey")
+ return
+ }
+ case "SecretKey":
+ z.SecretKey, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "SecretKey")
+ return
+ }
+ case "SessionToken":
+ z.SessionToken, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "SessionToken")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z BatchJobReplicateCredentials) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 3
+ // write "AccessKey"
+ err = en.Append(0x83, 0xa9, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.AccessKey)
+ if err != nil {
+ err = msgp.WrapError(err, "AccessKey")
+ return
+ }
+ // write "SecretKey"
+ err = en.Append(0xa9, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.SecretKey)
+ if err != nil {
+ err = msgp.WrapError(err, "SecretKey")
+ return
+ }
+ // write "SessionToken"
+ err = en.Append(0xac, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.SessionToken)
+ if err != nil {
+ err = msgp.WrapError(err, "SessionToken")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z BatchJobReplicateCredentials) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 3
+ // string "AccessKey"
+ o = append(o, 0x83, 0xa9, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79)
+ o = msgp.AppendString(o, z.AccessKey)
+ // string "SecretKey"
+ o = append(o, 0xa9, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79)
+ o = msgp.AppendString(o, z.SecretKey)
+ // string "SessionToken"
+ o = append(o, 0xac, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e)
+ o = msgp.AppendString(o, z.SessionToken)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BatchJobReplicateCredentials) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "AccessKey":
+ z.AccessKey, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AccessKey")
+ return
+ }
+ case "SecretKey":
+ z.SecretKey, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "SecretKey")
+ return
+ }
+ case "SessionToken":
+ z.SessionToken, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "SessionToken")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z BatchJobReplicateCredentials) Msgsize() (s int) {
+ s = 1 + 10 + msgp.StringPrefixSize + len(z.AccessKey) + 10 + msgp.StringPrefixSize + len(z.SecretKey) + 13 + msgp.StringPrefixSize + len(z.SessionToken)
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchJobReplicateFlags) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Filter":
+ err = z.Filter.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Filter")
+ return
+ }
+ case "Notify":
+ err = z.Notify.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Notify")
+ return
+ }
+ case "Retry":
+ err = z.Retry.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Retry")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *BatchJobReplicateFlags) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 3
+ // write "Filter"
+ err = en.Append(0x83, 0xa6, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72)
+ if err != nil {
+ return
+ }
+ err = z.Filter.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Filter")
+ return
+ }
+ // write "Notify"
+ err = en.Append(0xa6, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x79)
+ if err != nil {
+ return
+ }
+ err = z.Notify.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Notify")
+ return
+ }
+ // write "Retry"
+ err = en.Append(0xa5, 0x52, 0x65, 0x74, 0x72, 0x79)
+ if err != nil {
+ return
+ }
+ err = z.Retry.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Retry")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *BatchJobReplicateFlags) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 3
+ // string "Filter"
+ o = append(o, 0x83, 0xa6, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72)
+ o, err = z.Filter.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Filter")
+ return
+ }
+ // string "Notify"
+ o = append(o, 0xa6, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x79)
+ o, err = z.Notify.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Notify")
+ return
+ }
+ // string "Retry"
+ o = append(o, 0xa5, 0x52, 0x65, 0x74, 0x72, 0x79)
+ o, err = z.Retry.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Retry")
+ return
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BatchJobReplicateFlags) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Filter":
+ bts, err = z.Filter.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Filter")
+ return
+ }
+ case "Notify":
+ bts, err = z.Notify.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Notify")
+ return
+ }
+ case "Retry":
+ bts, err = z.Retry.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Retry")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *BatchJobReplicateFlags) Msgsize() (s int) {
+ s = 1 + 7 + z.Filter.Msgsize() + 7 + z.Notify.Msgsize() + 6 + z.Retry.Msgsize()
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchJobReplicateResourceType) DecodeMsg(dc *msgp.Reader) (err error) {
+ {
+ var zb0001 string
+ zb0001, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ (*z) = BatchJobReplicateResourceType(zb0001)
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z BatchJobReplicateResourceType) 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 BatchJobReplicateResourceType) 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 *BatchJobReplicateResourceType) 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) = BatchJobReplicateResourceType(zb0001)
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z BatchJobReplicateResourceType) Msgsize() (s int) {
+ s = msgp.StringPrefixSize + len(string(z))
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchJobReplicateSource) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Type":
+ {
+ var zb0002 string
+ zb0002, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Type")
+ return
+ }
+ z.Type = BatchJobReplicateResourceType(zb0002)
+ }
+ case "Bucket":
+ z.Bucket, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "Prefix":
+ err = z.Prefix.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefix")
+ return
+ }
+ case "Endpoint":
+ z.Endpoint, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Endpoint")
+ return
+ }
+ case "Path":
+ z.Path, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Path")
+ return
+ }
+ case "Creds":
+ var zb0003 uint32
+ zb0003, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Creds")
+ return
+ }
+ for zb0003 > 0 {
+ zb0003--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err, "Creds")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "AccessKey":
+ z.Creds.AccessKey, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Creds", "AccessKey")
+ return
+ }
+ case "SecretKey":
+ z.Creds.SecretKey, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Creds", "SecretKey")
+ return
+ }
+ case "SessionToken":
+ z.Creds.SessionToken, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Creds", "SessionToken")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err, "Creds")
+ return
+ }
+ }
+ }
+ case "Snowball":
+ err = z.Snowball.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Snowball")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *BatchJobReplicateSource) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 7
+ // write "Type"
+ err = en.Append(0x87, 0xa4, 0x54, 0x79, 0x70, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(string(z.Type))
+ if err != nil {
+ err = msgp.WrapError(err, "Type")
+ return
+ }
+ // write "Bucket"
+ err = en.Append(0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Bucket)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ // write "Prefix"
+ err = en.Append(0xa6, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78)
+ if err != nil {
+ return
+ }
+ err = z.Prefix.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefix")
+ return
+ }
+ // write "Endpoint"
+ err = en.Append(0xa8, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Endpoint)
+ if err != nil {
+ err = msgp.WrapError(err, "Endpoint")
+ return
+ }
+ // write "Path"
+ err = en.Append(0xa4, 0x50, 0x61, 0x74, 0x68)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Path)
+ if err != nil {
+ err = msgp.WrapError(err, "Path")
+ return
+ }
+ // write "Creds"
+ err = en.Append(0xa5, 0x43, 0x72, 0x65, 0x64, 0x73)
+ if err != nil {
+ return
+ }
+ // map header, size 3
+ // write "AccessKey"
+ err = en.Append(0x83, 0xa9, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Creds.AccessKey)
+ if err != nil {
+ err = msgp.WrapError(err, "Creds", "AccessKey")
+ return
+ }
+ // write "SecretKey"
+ err = en.Append(0xa9, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Creds.SecretKey)
+ if err != nil {
+ err = msgp.WrapError(err, "Creds", "SecretKey")
+ return
+ }
+ // write "SessionToken"
+ err = en.Append(0xac, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Creds.SessionToken)
+ if err != nil {
+ err = msgp.WrapError(err, "Creds", "SessionToken")
+ return
+ }
+ // write "Snowball"
+ err = en.Append(0xa8, 0x53, 0x6e, 0x6f, 0x77, 0x62, 0x61, 0x6c, 0x6c)
+ if err != nil {
+ return
+ }
+ err = z.Snowball.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Snowball")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *BatchJobReplicateSource) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 7
+ // string "Type"
+ o = append(o, 0x87, 0xa4, 0x54, 0x79, 0x70, 0x65)
+ o = msgp.AppendString(o, string(z.Type))
+ // string "Bucket"
+ o = append(o, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ o = msgp.AppendString(o, z.Bucket)
+ // string "Prefix"
+ o = append(o, 0xa6, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78)
+ o, err = z.Prefix.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefix")
+ return
+ }
+ // string "Endpoint"
+ o = append(o, 0xa8, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74)
+ o = msgp.AppendString(o, z.Endpoint)
+ // string "Path"
+ o = append(o, 0xa4, 0x50, 0x61, 0x74, 0x68)
+ o = msgp.AppendString(o, z.Path)
+ // string "Creds"
+ o = append(o, 0xa5, 0x43, 0x72, 0x65, 0x64, 0x73)
+ // map header, size 3
+ // string "AccessKey"
+ o = append(o, 0x83, 0xa9, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79)
+ o = msgp.AppendString(o, z.Creds.AccessKey)
+ // string "SecretKey"
+ o = append(o, 0xa9, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79)
+ o = msgp.AppendString(o, z.Creds.SecretKey)
+ // string "SessionToken"
+ o = append(o, 0xac, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e)
+ o = msgp.AppendString(o, z.Creds.SessionToken)
+ // string "Snowball"
+ o = append(o, 0xa8, 0x53, 0x6e, 0x6f, 0x77, 0x62, 0x61, 0x6c, 0x6c)
+ o, err = z.Snowball.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Snowball")
+ return
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BatchJobReplicateSource) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Type":
+ {
+ var zb0002 string
+ zb0002, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Type")
+ return
+ }
+ z.Type = BatchJobReplicateResourceType(zb0002)
+ }
+ case "Bucket":
+ z.Bucket, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "Prefix":
+ bts, err = z.Prefix.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefix")
+ return
+ }
+ case "Endpoint":
+ z.Endpoint, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Endpoint")
+ return
+ }
+ case "Path":
+ z.Path, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Path")
+ return
+ }
+ case "Creds":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Creds")
+ return
+ }
+ for zb0003 > 0 {
+ zb0003--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Creds")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "AccessKey":
+ z.Creds.AccessKey, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Creds", "AccessKey")
+ return
+ }
+ case "SecretKey":
+ z.Creds.SecretKey, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Creds", "SecretKey")
+ return
+ }
+ case "SessionToken":
+ z.Creds.SessionToken, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Creds", "SessionToken")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Creds")
+ return
+ }
+ }
+ }
+ case "Snowball":
+ bts, err = z.Snowball.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Snowball")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *BatchJobReplicateSource) Msgsize() (s int) {
+ s = 1 + 5 + msgp.StringPrefixSize + len(string(z.Type)) + 7 + msgp.StringPrefixSize + len(z.Bucket) + 7 + z.Prefix.Msgsize() + 9 + msgp.StringPrefixSize + len(z.Endpoint) + 5 + msgp.StringPrefixSize + len(z.Path) + 6 + 1 + 10 + msgp.StringPrefixSize + len(z.Creds.AccessKey) + 10 + msgp.StringPrefixSize + len(z.Creds.SecretKey) + 13 + msgp.StringPrefixSize + len(z.Creds.SessionToken) + 9 + z.Snowball.Msgsize()
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchJobReplicateTarget) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Type":
+ {
+ var zb0002 string
+ zb0002, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Type")
+ return
+ }
+ z.Type = BatchJobReplicateResourceType(zb0002)
+ }
+ case "Bucket":
+ z.Bucket, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "Prefix":
+ z.Prefix, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Prefix")
+ return
+ }
+ case "Endpoint":
+ z.Endpoint, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Endpoint")
+ return
+ }
+ case "Path":
+ z.Path, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Path")
+ return
+ }
+ case "Creds":
+ var zb0003 uint32
+ zb0003, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Creds")
+ return
+ }
+ for zb0003 > 0 {
+ zb0003--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err, "Creds")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "AccessKey":
+ z.Creds.AccessKey, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Creds", "AccessKey")
+ return
+ }
+ case "SecretKey":
+ z.Creds.SecretKey, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Creds", "SecretKey")
+ return
+ }
+ case "SessionToken":
+ z.Creds.SessionToken, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Creds", "SessionToken")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err, "Creds")
+ return
+ }
+ }
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *BatchJobReplicateTarget) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 6
+ // write "Type"
+ err = en.Append(0x86, 0xa4, 0x54, 0x79, 0x70, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(string(z.Type))
+ if err != nil {
+ err = msgp.WrapError(err, "Type")
+ return
+ }
+ // write "Bucket"
+ err = en.Append(0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Bucket)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ // write "Prefix"
+ err = en.Append(0xa6, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Prefix)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefix")
+ return
+ }
+ // write "Endpoint"
+ err = en.Append(0xa8, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Endpoint)
+ if err != nil {
+ err = msgp.WrapError(err, "Endpoint")
+ return
+ }
+ // write "Path"
+ err = en.Append(0xa4, 0x50, 0x61, 0x74, 0x68)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Path)
+ if err != nil {
+ err = msgp.WrapError(err, "Path")
+ return
+ }
+ // write "Creds"
+ err = en.Append(0xa5, 0x43, 0x72, 0x65, 0x64, 0x73)
+ if err != nil {
+ return
+ }
+ // map header, size 3
+ // write "AccessKey"
+ err = en.Append(0x83, 0xa9, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Creds.AccessKey)
+ if err != nil {
+ err = msgp.WrapError(err, "Creds", "AccessKey")
+ return
+ }
+ // write "SecretKey"
+ err = en.Append(0xa9, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Creds.SecretKey)
+ if err != nil {
+ err = msgp.WrapError(err, "Creds", "SecretKey")
+ return
+ }
+ // write "SessionToken"
+ err = en.Append(0xac, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Creds.SessionToken)
+ if err != nil {
+ err = msgp.WrapError(err, "Creds", "SessionToken")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *BatchJobReplicateTarget) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 6
+ // string "Type"
+ o = append(o, 0x86, 0xa4, 0x54, 0x79, 0x70, 0x65)
+ o = msgp.AppendString(o, string(z.Type))
+ // string "Bucket"
+ o = append(o, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ o = msgp.AppendString(o, z.Bucket)
+ // string "Prefix"
+ o = append(o, 0xa6, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78)
+ o = msgp.AppendString(o, z.Prefix)
+ // string "Endpoint"
+ o = append(o, 0xa8, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74)
+ o = msgp.AppendString(o, z.Endpoint)
+ // string "Path"
+ o = append(o, 0xa4, 0x50, 0x61, 0x74, 0x68)
+ o = msgp.AppendString(o, z.Path)
+ // string "Creds"
+ o = append(o, 0xa5, 0x43, 0x72, 0x65, 0x64, 0x73)
+ // map header, size 3
+ // string "AccessKey"
+ o = append(o, 0x83, 0xa9, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79)
+ o = msgp.AppendString(o, z.Creds.AccessKey)
+ // string "SecretKey"
+ o = append(o, 0xa9, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79)
+ o = msgp.AppendString(o, z.Creds.SecretKey)
+ // string "SessionToken"
+ o = append(o, 0xac, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e)
+ o = msgp.AppendString(o, z.Creds.SessionToken)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BatchJobReplicateTarget) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Type":
+ {
+ var zb0002 string
+ zb0002, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Type")
+ return
+ }
+ z.Type = BatchJobReplicateResourceType(zb0002)
+ }
+ case "Bucket":
+ z.Bucket, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "Prefix":
+ z.Prefix, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefix")
+ return
+ }
+ case "Endpoint":
+ z.Endpoint, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Endpoint")
+ return
+ }
+ case "Path":
+ z.Path, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Path")
+ return
+ }
+ case "Creds":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Creds")
+ return
+ }
+ for zb0003 > 0 {
+ zb0003--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Creds")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "AccessKey":
+ z.Creds.AccessKey, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Creds", "AccessKey")
+ return
+ }
+ case "SecretKey":
+ z.Creds.SecretKey, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Creds", "SecretKey")
+ return
+ }
+ case "SessionToken":
+ z.Creds.SessionToken, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Creds", "SessionToken")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Creds")
+ return
+ }
+ }
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *BatchJobReplicateTarget) Msgsize() (s int) {
+ s = 1 + 5 + msgp.StringPrefixSize + len(string(z.Type)) + 7 + msgp.StringPrefixSize + len(z.Bucket) + 7 + msgp.StringPrefixSize + len(z.Prefix) + 9 + msgp.StringPrefixSize + len(z.Endpoint) + 5 + msgp.StringPrefixSize + len(z.Path) + 6 + 1 + 10 + msgp.StringPrefixSize + len(z.Creds.AccessKey) + 10 + msgp.StringPrefixSize + len(z.Creds.SecretKey) + 13 + msgp.StringPrefixSize + len(z.Creds.SessionToken)
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchJobReplicateV1) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "APIVersion":
+ z.APIVersion, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "APIVersion")
+ return
+ }
+ case "Flags":
+ var zb0002 uint32
+ zb0002, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Flags")
+ return
+ }
+ for zb0002 > 0 {
+ zb0002--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err, "Flags")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Filter":
+ err = z.Flags.Filter.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Filter")
+ return
+ }
+ case "Notify":
+ err = z.Flags.Notify.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Notify")
+ return
+ }
+ case "Retry":
+ err = z.Flags.Retry.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Retry")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err, "Flags")
+ return
+ }
+ }
+ }
+ case "Target":
+ err = z.Target.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Target")
+ return
+ }
+ case "Source":
+ err = z.Source.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Source")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *BatchJobReplicateV1) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 4
+ // write "APIVersion"
+ err = en.Append(0x84, 0xaa, 0x41, 0x50, 0x49, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.APIVersion)
+ if err != nil {
+ err = msgp.WrapError(err, "APIVersion")
+ return
+ }
+ // write "Flags"
+ err = en.Append(0xa5, 0x46, 0x6c, 0x61, 0x67, 0x73)
+ if err != nil {
+ return
+ }
+ // map header, size 3
+ // write "Filter"
+ err = en.Append(0x83, 0xa6, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72)
+ if err != nil {
+ return
+ }
+ err = z.Flags.Filter.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Filter")
+ return
+ }
+ // write "Notify"
+ err = en.Append(0xa6, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x79)
+ if err != nil {
+ return
+ }
+ err = z.Flags.Notify.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Notify")
+ return
+ }
+ // write "Retry"
+ err = en.Append(0xa5, 0x52, 0x65, 0x74, 0x72, 0x79)
+ if err != nil {
+ return
+ }
+ err = z.Flags.Retry.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Retry")
+ return
+ }
+ // write "Target"
+ err = en.Append(0xa6, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74)
+ if err != nil {
+ return
+ }
+ err = z.Target.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Target")
+ return
+ }
+ // write "Source"
+ err = en.Append(0xa6, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65)
+ if err != nil {
+ return
+ }
+ err = z.Source.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Source")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *BatchJobReplicateV1) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 4
+ // string "APIVersion"
+ o = append(o, 0x84, 0xaa, 0x41, 0x50, 0x49, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e)
+ o = msgp.AppendString(o, z.APIVersion)
+ // string "Flags"
+ o = append(o, 0xa5, 0x46, 0x6c, 0x61, 0x67, 0x73)
+ // map header, size 3
+ // string "Filter"
+ o = append(o, 0x83, 0xa6, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72)
+ o, err = z.Flags.Filter.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Filter")
+ return
+ }
+ // string "Notify"
+ o = append(o, 0xa6, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x79)
+ o, err = z.Flags.Notify.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Notify")
+ return
+ }
+ // string "Retry"
+ o = append(o, 0xa5, 0x52, 0x65, 0x74, 0x72, 0x79)
+ o, err = z.Flags.Retry.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Retry")
+ return
+ }
+ // string "Target"
+ o = append(o, 0xa6, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74)
+ o, err = z.Target.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Target")
+ return
+ }
+ // string "Source"
+ o = append(o, 0xa6, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65)
+ o, err = z.Source.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Source")
+ return
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BatchJobReplicateV1) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "APIVersion":
+ z.APIVersion, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "APIVersion")
+ return
+ }
+ case "Flags":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags")
+ return
+ }
+ for zb0002 > 0 {
+ zb0002--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Filter":
+ bts, err = z.Flags.Filter.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Filter")
+ return
+ }
+ case "Notify":
+ bts, err = z.Flags.Notify.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Notify")
+ return
+ }
+ case "Retry":
+ bts, err = z.Flags.Retry.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Retry")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags")
+ return
+ }
+ }
+ }
+ case "Target":
+ bts, err = z.Target.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Target")
+ return
+ }
+ case "Source":
+ bts, err = z.Source.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Source")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *BatchJobReplicateV1) Msgsize() (s int) {
+ s = 1 + 11 + msgp.StringPrefixSize + len(z.APIVersion) + 6 + 1 + 7 + z.Flags.Filter.Msgsize() + 7 + z.Flags.Notify.Msgsize() + 6 + z.Flags.Retry.Msgsize() + 7 + z.Target.Msgsize() + 7 + z.Source.Msgsize()
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchReplicateFilter) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "NewerThan":
+ err = z.NewerThan.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "NewerThan")
+ return
+ }
+ case "OlderThan":
+ err = z.OlderThan.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "OlderThan")
+ return
+ }
+ case "CreatedAfter":
+ z.CreatedAfter, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "CreatedAfter")
+ return
+ }
+ case "CreatedBefore":
+ z.CreatedBefore, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "CreatedBefore")
+ return
+ }
+ case "Tags":
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Tags")
+ return
+ }
+ if cap(z.Tags) >= int(zb0002) {
+ z.Tags = (z.Tags)[:zb0002]
+ } else {
+ z.Tags = make([]BatchJobKV, zb0002)
+ }
+ for za0001 := range z.Tags {
+ err = z.Tags[za0001].DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Tags", za0001)
+ return
+ }
+ }
+ case "Metadata":
+ var zb0003 uint32
+ zb0003, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Metadata")
+ return
+ }
+ if cap(z.Metadata) >= int(zb0003) {
+ z.Metadata = (z.Metadata)[:zb0003]
+ } else {
+ z.Metadata = make([]BatchJobKV, zb0003)
+ }
+ for za0002 := range z.Metadata {
+ err = z.Metadata[za0002].DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Metadata", za0002)
+ return
+ }
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *BatchReplicateFilter) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 6
+ // write "NewerThan"
+ err = en.Append(0x86, 0xa9, 0x4e, 0x65, 0x77, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e)
+ if err != nil {
+ return
+ }
+ err = z.NewerThan.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "NewerThan")
+ return
+ }
+ // write "OlderThan"
+ err = en.Append(0xa9, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e)
+ if err != nil {
+ return
+ }
+ err = z.OlderThan.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "OlderThan")
+ return
+ }
+ // write "CreatedAfter"
+ err = en.Append(0xac, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x66, 0x74, 0x65, 0x72)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.CreatedAfter)
+ if err != nil {
+ err = msgp.WrapError(err, "CreatedAfter")
+ return
+ }
+ // write "CreatedBefore"
+ err = en.Append(0xad, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.CreatedBefore)
+ if err != nil {
+ err = msgp.WrapError(err, "CreatedBefore")
+ return
+ }
+ // write "Tags"
+ err = en.Append(0xa4, 0x54, 0x61, 0x67, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(len(z.Tags)))
+ if err != nil {
+ err = msgp.WrapError(err, "Tags")
+ return
+ }
+ for za0001 := range z.Tags {
+ err = z.Tags[za0001].EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Tags", za0001)
+ return
+ }
+ }
+ // write "Metadata"
+ err = en.Append(0xa8, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(len(z.Metadata)))
+ if err != nil {
+ err = msgp.WrapError(err, "Metadata")
+ return
+ }
+ for za0002 := range z.Metadata {
+ err = z.Metadata[za0002].EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Metadata", za0002)
+ return
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *BatchReplicateFilter) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 6
+ // string "NewerThan"
+ o = append(o, 0x86, 0xa9, 0x4e, 0x65, 0x77, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e)
+ o, err = z.NewerThan.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "NewerThan")
+ return
+ }
+ // string "OlderThan"
+ o = append(o, 0xa9, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e)
+ o, err = z.OlderThan.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "OlderThan")
+ return
+ }
+ // string "CreatedAfter"
+ o = append(o, 0xac, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x66, 0x74, 0x65, 0x72)
+ o = msgp.AppendTime(o, z.CreatedAfter)
+ // string "CreatedBefore"
+ o = append(o, 0xad, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65)
+ o = msgp.AppendTime(o, z.CreatedBefore)
+ // string "Tags"
+ o = append(o, 0xa4, 0x54, 0x61, 0x67, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.Tags)))
+ for za0001 := range z.Tags {
+ o, err = z.Tags[za0001].MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Tags", za0001)
+ return
+ }
+ }
+ // string "Metadata"
+ o = append(o, 0xa8, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.Metadata)))
+ for za0002 := range z.Metadata {
+ o, err = z.Metadata[za0002].MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Metadata", za0002)
+ return
+ }
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BatchReplicateFilter) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "NewerThan":
+ bts, err = z.NewerThan.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "NewerThan")
+ return
+ }
+ case "OlderThan":
+ bts, err = z.OlderThan.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "OlderThan")
+ return
+ }
+ case "CreatedAfter":
+ z.CreatedAfter, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "CreatedAfter")
+ return
+ }
+ case "CreatedBefore":
+ z.CreatedBefore, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "CreatedBefore")
+ return
+ }
+ case "Tags":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Tags")
+ return
+ }
+ if cap(z.Tags) >= int(zb0002) {
+ z.Tags = (z.Tags)[:zb0002]
+ } else {
+ z.Tags = make([]BatchJobKV, zb0002)
+ }
+ for za0001 := range z.Tags {
+ bts, err = z.Tags[za0001].UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Tags", za0001)
+ return
+ }
+ }
+ case "Metadata":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Metadata")
+ return
+ }
+ if cap(z.Metadata) >= int(zb0003) {
+ z.Metadata = (z.Metadata)[:zb0003]
+ } else {
+ z.Metadata = make([]BatchJobKV, zb0003)
+ }
+ for za0002 := range z.Metadata {
+ bts, err = z.Metadata[za0002].UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Metadata", za0002)
+ return
+ }
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *BatchReplicateFilter) Msgsize() (s int) {
+ s = 1 + 10 + z.NewerThan.Msgsize() + 10 + z.OlderThan.Msgsize() + 13 + msgp.TimeSize + 14 + msgp.TimeSize + 5 + msgp.ArrayHeaderSize
+ for za0001 := range z.Tags {
+ s += z.Tags[za0001].Msgsize()
+ }
+ s += 9 + msgp.ArrayHeaderSize
+ for za0002 := range z.Metadata {
+ s += z.Metadata[za0002].Msgsize()
+ }
+ return
+}
diff --git a/cmd/batch-replicate_gen_test.go b/cmd/batch-replicate_gen_test.go
new file mode 100644
index 00000000..68ab1865
--- /dev/null
+++ b/cmd/batch-replicate_gen_test.go
@@ -0,0 +1,688 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+func TestMarshalUnmarshalBatchJobReplicateCredentials(t *testing.T) {
+ v := BatchJobReplicateCredentials{}
+ 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 BenchmarkMarshalMsgBatchJobReplicateCredentials(b *testing.B) {
+ v := BatchJobReplicateCredentials{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBatchJobReplicateCredentials(b *testing.B) {
+ v := BatchJobReplicateCredentials{}
+ 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 BenchmarkUnmarshalBatchJobReplicateCredentials(b *testing.B) {
+ v := BatchJobReplicateCredentials{}
+ 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 TestEncodeDecodeBatchJobReplicateCredentials(t *testing.T) {
+ v := BatchJobReplicateCredentials{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBatchJobReplicateCredentials Msgsize() is inaccurate")
+ }
+
+ vn := BatchJobReplicateCredentials{}
+ 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 BenchmarkEncodeBatchJobReplicateCredentials(b *testing.B) {
+ v := BatchJobReplicateCredentials{}
+ 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 BenchmarkDecodeBatchJobReplicateCredentials(b *testing.B) {
+ v := BatchJobReplicateCredentials{}
+ 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 TestMarshalUnmarshalBatchJobReplicateFlags(t *testing.T) {
+ v := BatchJobReplicateFlags{}
+ 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 BenchmarkMarshalMsgBatchJobReplicateFlags(b *testing.B) {
+ v := BatchJobReplicateFlags{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBatchJobReplicateFlags(b *testing.B) {
+ v := BatchJobReplicateFlags{}
+ 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 BenchmarkUnmarshalBatchJobReplicateFlags(b *testing.B) {
+ v := BatchJobReplicateFlags{}
+ 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 TestEncodeDecodeBatchJobReplicateFlags(t *testing.T) {
+ v := BatchJobReplicateFlags{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBatchJobReplicateFlags Msgsize() is inaccurate")
+ }
+
+ vn := BatchJobReplicateFlags{}
+ 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 BenchmarkEncodeBatchJobReplicateFlags(b *testing.B) {
+ v := BatchJobReplicateFlags{}
+ 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 BenchmarkDecodeBatchJobReplicateFlags(b *testing.B) {
+ v := BatchJobReplicateFlags{}
+ 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 TestMarshalUnmarshalBatchJobReplicateSource(t *testing.T) {
+ v := BatchJobReplicateSource{}
+ 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 BenchmarkMarshalMsgBatchJobReplicateSource(b *testing.B) {
+ v := BatchJobReplicateSource{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBatchJobReplicateSource(b *testing.B) {
+ v := BatchJobReplicateSource{}
+ 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 BenchmarkUnmarshalBatchJobReplicateSource(b *testing.B) {
+ v := BatchJobReplicateSource{}
+ 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 TestEncodeDecodeBatchJobReplicateSource(t *testing.T) {
+ v := BatchJobReplicateSource{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBatchJobReplicateSource Msgsize() is inaccurate")
+ }
+
+ vn := BatchJobReplicateSource{}
+ 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 BenchmarkEncodeBatchJobReplicateSource(b *testing.B) {
+ v := BatchJobReplicateSource{}
+ 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 BenchmarkDecodeBatchJobReplicateSource(b *testing.B) {
+ v := BatchJobReplicateSource{}
+ 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 TestMarshalUnmarshalBatchJobReplicateTarget(t *testing.T) {
+ v := BatchJobReplicateTarget{}
+ 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 BenchmarkMarshalMsgBatchJobReplicateTarget(b *testing.B) {
+ v := BatchJobReplicateTarget{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBatchJobReplicateTarget(b *testing.B) {
+ v := BatchJobReplicateTarget{}
+ 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 BenchmarkUnmarshalBatchJobReplicateTarget(b *testing.B) {
+ v := BatchJobReplicateTarget{}
+ 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 TestEncodeDecodeBatchJobReplicateTarget(t *testing.T) {
+ v := BatchJobReplicateTarget{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBatchJobReplicateTarget Msgsize() is inaccurate")
+ }
+
+ vn := BatchJobReplicateTarget{}
+ 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 BenchmarkEncodeBatchJobReplicateTarget(b *testing.B) {
+ v := BatchJobReplicateTarget{}
+ 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 BenchmarkDecodeBatchJobReplicateTarget(b *testing.B) {
+ v := BatchJobReplicateTarget{}
+ 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 TestMarshalUnmarshalBatchJobReplicateV1(t *testing.T) {
+ v := BatchJobReplicateV1{}
+ 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 BenchmarkMarshalMsgBatchJobReplicateV1(b *testing.B) {
+ v := BatchJobReplicateV1{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBatchJobReplicateV1(b *testing.B) {
+ v := BatchJobReplicateV1{}
+ 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 BenchmarkUnmarshalBatchJobReplicateV1(b *testing.B) {
+ v := BatchJobReplicateV1{}
+ 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 TestEncodeDecodeBatchJobReplicateV1(t *testing.T) {
+ v := BatchJobReplicateV1{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBatchJobReplicateV1 Msgsize() is inaccurate")
+ }
+
+ vn := BatchJobReplicateV1{}
+ 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 BenchmarkEncodeBatchJobReplicateV1(b *testing.B) {
+ v := BatchJobReplicateV1{}
+ 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 BenchmarkDecodeBatchJobReplicateV1(b *testing.B) {
+ v := BatchJobReplicateV1{}
+ 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 TestMarshalUnmarshalBatchReplicateFilter(t *testing.T) {
+ v := BatchReplicateFilter{}
+ 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 BenchmarkMarshalMsgBatchReplicateFilter(b *testing.B) {
+ v := BatchReplicateFilter{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBatchReplicateFilter(b *testing.B) {
+ v := BatchReplicateFilter{}
+ 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 BenchmarkUnmarshalBatchReplicateFilter(b *testing.B) {
+ v := BatchReplicateFilter{}
+ 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 TestEncodeDecodeBatchReplicateFilter(t *testing.T) {
+ v := BatchReplicateFilter{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBatchReplicateFilter Msgsize() is inaccurate")
+ }
+
+ vn := BatchReplicateFilter{}
+ 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 BenchmarkEncodeBatchReplicateFilter(b *testing.B) {
+ v := BatchReplicateFilter{}
+ 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 BenchmarkDecodeBatchReplicateFilter(b *testing.B) {
+ v := BatchReplicateFilter{}
+ 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)
+ }
+ }
+}
diff --git a/cmd/batch-replicate_test.go b/cmd/batch-replicate_test.go
new file mode 100644
index 00000000..e84c59cf
--- /dev/null
+++ b/cmd/batch-replicate_test.go
@@ -0,0 +1,182 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "slices"
+ "testing"
+
+ "gopkg.in/yaml.v3"
+)
+
+func TestParseBatchJobReplicate(t *testing.T) {
+ replicateYaml := `
+replicate:
+ apiVersion: v1
+ # source of the objects to be replicated
+ source:
+ type: minio # valid values are "s3" or "minio"
+ bucket: mytest
+ prefix: object-prefix1 # 'PREFIX' is optional
+ # If your source is the 'local' alias specified to 'mc batch start', then the 'endpoint' and 'credentials' fields are optional and can be omitted
+ # Either the 'source' or 'remote' *must* be the "local" deployment
+# endpoint: "http://127.0.0.1:9000"
+# # path: "on|off|auto" # "on" enables path-style bucket lookup. "off" enables virtual host (DNS)-style bucket lookup. Defaults to "auto"
+# credentials:
+# accessKey: minioadmin # Required
+# secretKey: minioadmin # Required
+# # sessionToken: SESSION-TOKEN # Optional only available when rotating credentials are used
+ snowball: # automatically activated if the source is local
+ disable: true # optionally turn-off snowball archive transfer
+# batch: 100 # upto this many objects per archive
+# inmemory: true # indicates if the archive must be staged locally or in-memory
+# compress: false # S2/Snappy compressed archive
+# smallerThan: 5MiB # create archive for all objects smaller than 5MiB
+# skipErrs: false # skips any source side read() errors
+
+ # target where the objects must be replicated
+ target:
+ type: minio # valid values are "s3" or "minio"
+ bucket: mytest
+ prefix: stage # 'PREFIX' is optional
+ # If your source is the 'local' alias specified to 'mc batch start', then the 'endpoint' and 'credentials' fields are optional and can be omitted
+
+ # Either the 'source' or 'remote' *must* be the "local" deployment
+ endpoint: "http://127.0.0.1:9001"
+ # path: "on|off|auto" # "on" enables path-style bucket lookup. "off" enables virtual host (DNS)-style bucket lookup. Defaults to "auto"
+ credentials:
+ accessKey: minioadmin
+ secretKey: minioadmin
+ # sessionToken: SESSION-TOKEN # Optional only available when rotating credentials are used
+
+ # NOTE: All flags are optional
+ # - filtering criteria only applies for all source objects match the criteria
+ # - configurable notification endpoints
+ # - configurable retries for the job (each retry skips successfully previously replaced objects)
+ flags:
+ filter:
+ newerThan: "7d10h31s" # match objects newer than this value (e.g. 7d10h31s)
+ olderThan: "7d" # match objects older than this value (e.g. 7d10h31s)
+# createdAfter: "date" # match objects created after "date"
+# createdBefore: "date" # match objects created before "date"
+
+ ## NOTE: tags are not supported when "source" is remote.
+ tags:
+ - key: "name"
+ value: "pick*" # match objects with tag 'name', with all values starting with 'pick'
+
+ metadata:
+ - key: "content-type"
+ value: "image/*" # match objects with 'content-type', with all values starting with 'image/'
+
+# notify:
+# endpoint: "https://notify.endpoint" # notification endpoint to receive job status events
+# token: "Bearer xxxxx" # optional authentication token for the notification endpoint
+#
+# retry:
+# attempts: 10 # number of retries for the job before giving up
+# delay: "500ms" # least amount of delay between each retry
+
+`
+ var job BatchJobRequest
+ err := yaml.Unmarshal([]byte(replicateYaml), &job)
+ if err != nil {
+ t.Fatal("Failed to parse batch-job-replicate yaml", err)
+ }
+ if !slices.Equal(job.Replicate.Source.Prefix.F(), []string{"object-prefix1"}) {
+ t.Fatal("Failed to parse batch-job-replicate yaml", err)
+ }
+ multiPrefixReplicateYaml := `
+replicate:
+ apiVersion: v1
+ # source of the objects to be replicated
+ source:
+ type: minio # valid values are "s3" or "minio"
+ bucket: mytest
+ prefix: # 'PREFIX' is optional
+ - object-prefix1
+ - object-prefix2
+ # If your source is the 'local' alias specified to 'mc batch start', then the 'endpoint' and 'credentials' fields are optional and can be omitted
+ # Either the 'source' or 'remote' *must* be the "local" deployment
+# endpoint: "http://127.0.0.1:9000"
+# # path: "on|off|auto" # "on" enables path-style bucket lookup. "off" enables virtual host (DNS)-style bucket lookup. Defaults to "auto"
+# credentials:
+# accessKey: minioadmin # Required
+# secretKey: minioadmin # Required
+# # sessionToken: SESSION-TOKEN # Optional only available when rotating credentials are used
+ snowball: # automatically activated if the source is local
+ disable: true # optionally turn-off snowball archive transfer
+# batch: 100 # upto this many objects per archive
+# inmemory: true # indicates if the archive must be staged locally or in-memory
+# compress: false # S2/Snappy compressed archive
+# smallerThan: 5MiB # create archive for all objects smaller than 5MiB
+# skipErrs: false # skips any source side read() errors
+
+ # target where the objects must be replicated
+ target:
+ type: minio # valid values are "s3" or "minio"
+ bucket: mytest
+ prefix: stage # 'PREFIX' is optional
+ # If your source is the 'local' alias specified to 'mc batch start', then the 'endpoint' and 'credentials' fields are optional and can be omitted
+
+ # Either the 'source' or 'remote' *must* be the "local" deployment
+ endpoint: "http://127.0.0.1:9001"
+ # path: "on|off|auto" # "on" enables path-style bucket lookup. "off" enables virtual host (DNS)-style bucket lookup. Defaults to "auto"
+ credentials:
+ accessKey: minioadmin
+ secretKey: minioadmin
+ # sessionToken: SESSION-TOKEN # Optional only available when rotating credentials are used
+
+ # NOTE: All flags are optional
+ # - filtering criteria only applies for all source objects match the criteria
+ # - configurable notification endpoints
+ # - configurable retries for the job (each retry skips successfully previously replaced objects)
+ flags:
+ filter:
+ newerThan: "7d10h31s" # match objects newer than this value (e.g. 7d10h31s)
+ olderThan: "7d" # match objects older than this value (e.g. 7d10h31s)
+# createdAfter: "date" # match objects created after "date"
+# createdBefore: "date" # match objects created before "date"
+
+ ## NOTE: tags are not supported when "source" is remote.
+ tags:
+ - key: "name"
+ value: "pick*" # match objects with tag 'name', with all values starting with 'pick'
+
+ metadata:
+ - key: "content-type"
+ value: "image/*" # match objects with 'content-type', with all values starting with 'image/'
+
+# notify:
+# endpoint: "https://notify.endpoint" # notification endpoint to receive job status events
+# token: "Bearer xxxxx" # optional authentication token for the notification endpoint
+#
+# retry:
+# attempts: 10 # number of retries for the job before giving up
+# delay: "500ms" # least amount of delay between each retry
+
+`
+ var multiPrefixJob BatchJobRequest
+ err = yaml.Unmarshal([]byte(multiPrefixReplicateYaml), &multiPrefixJob)
+ if err != nil {
+ t.Fatal("Failed to parse batch-job-replicate yaml", err)
+ }
+ if !slices.Equal(multiPrefixJob.Replicate.Source.Prefix.F(), []string{"object-prefix1", "object-prefix2"}) {
+ t.Fatal("Failed to parse batch-job-replicate yaml")
+ }
+}
diff --git a/cmd/batch-rotate.go b/cmd/batch-rotate.go
new file mode 100644
index 00000000..3e8f18fa
--- /dev/null
+++ b/cmd/batch-rotate.go
@@ -0,0 +1,494 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/base64"
+ "fmt"
+ "maps"
+ "math/rand"
+ "net/http"
+ "runtime"
+ "strconv"
+ "strings"
+ "time"
+
+ jsoniter "github.com/json-iterator/go"
+ "github.com/minio/minio-go/v7/pkg/tags"
+ "github.com/minio/minio/internal/crypto"
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/minio/internal/kms"
+ "github.com/minio/pkg/v3/env"
+ "github.com/minio/pkg/v3/workers"
+)
+
+// keyrotate:
+// apiVersion: v1
+// bucket: BUCKET
+// prefix: PREFIX
+// encryption:
+// type: sse-s3 # valid values are sse-s3 and sse-kms
+// key: # valid only for sse-kms
+// context: # valid only for sse-kms
+// # optional flags based filtering criteria
+// # for all objects
+// flags:
+// filter:
+// newerThan: "7d" # match objects newer than this value (e.g. 7d10h31s)
+// olderThan: "7d" # match objects older than this value (e.g. 7d10h31s)
+// createdAfter: "date" # match objects created after "date"
+// createdBefore: "date" # match objects created before "date"
+// tags:
+// - key: "name"
+// value: "pick*" # match objects with tag 'name', with all values starting with 'pick'
+// metadata:
+// - key: "content-type"
+// value: "image/*" # match objects with 'content-type', with all values starting with 'image/'
+// kmskey: "key-id" # match objects with KMS key-id (applicable only for sse-kms)
+// notify:
+// endpoint: "https://notify.endpoint" # notification endpoint to receive job status events
+// token: "Bearer xxxxx" # optional authentication token for the notification endpoint
+
+// retry:
+// attempts: 10 # number of retries for the job before giving up
+// delay: "500ms" # least amount of delay between each retry
+
+//go:generate msgp -file $GOFILE -unexported
+
+// BatchKeyRotationType defines key rotation type
+type BatchKeyRotationType string
+
+const (
+ sses3 BatchKeyRotationType = "sse-s3"
+ ssekms BatchKeyRotationType = "sse-kms"
+)
+
+// BatchJobKeyRotateEncryption defines key rotation encryption options passed
+type BatchJobKeyRotateEncryption struct {
+ Type BatchKeyRotationType `yaml:"type" json:"type"`
+ Key string `yaml:"key" json:"key"`
+ Context string `yaml:"context" json:"context"`
+ kmsContext kms.Context `msg:"-"`
+}
+
+// Validate validates input key rotation encryption options.
+func (e BatchJobKeyRotateEncryption) Validate() error {
+ if e.Type != sses3 && e.Type != ssekms {
+ return errInvalidArgument
+ }
+ spaces := strings.HasPrefix(e.Key, " ") || strings.HasSuffix(e.Key, " ")
+ if e.Type == ssekms && spaces {
+ return crypto.ErrInvalidEncryptionKeyID
+ }
+
+ if e.Type == ssekms && GlobalKMS != nil {
+ ctx := kms.Context{}
+ if e.Context != "" {
+ b, err := base64.StdEncoding.DecodeString(e.Context)
+ if err != nil {
+ return err
+ }
+
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ if err := json.Unmarshal(b, &ctx); err != nil {
+ return err
+ }
+ }
+ e.kmsContext = kms.Context{}
+ maps.Copy(e.kmsContext, ctx)
+ ctx["MinIO batch API"] = "batchrotate" // Context for a test key operation
+ if _, err := GlobalKMS.GenerateKey(GlobalContext, &kms.GenerateKeyRequest{Name: e.Key, AssociatedData: ctx}); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// BatchKeyRotateFilter holds all the filters currently supported for batch replication
+type BatchKeyRotateFilter struct {
+ NewerThan time.Duration `yaml:"newerThan,omitempty" json:"newerThan"`
+ OlderThan time.Duration `yaml:"olderThan,omitempty" json:"olderThan"`
+ CreatedAfter time.Time `yaml:"createdAfter,omitempty" json:"createdAfter"`
+ CreatedBefore time.Time `yaml:"createdBefore,omitempty" json:"createdBefore"`
+ Tags []BatchJobKV `yaml:"tags,omitempty" json:"tags"`
+ Metadata []BatchJobKV `yaml:"metadata,omitempty" json:"metadata"`
+ KMSKeyID string `yaml:"kmskeyid" json:"kmskey"`
+}
+
+// BatchKeyRotateNotification success or failure notification endpoint for each job attempts
+type BatchKeyRotateNotification struct {
+ Endpoint string `yaml:"endpoint" json:"endpoint"`
+ Token string `yaml:"token" json:"token"`
+}
+
+// BatchJobKeyRotateFlags various configurations for replication job definition currently includes
+// - filter
+// - notify
+// - retry
+type BatchJobKeyRotateFlags struct {
+ Filter BatchKeyRotateFilter `yaml:"filter" json:"filter"`
+ Notify BatchJobNotification `yaml:"notify" json:"notify"`
+ Retry BatchJobRetry `yaml:"retry" json:"retry"`
+}
+
+// BatchJobKeyRotateV1 v1 of batch key rotation job
+type BatchJobKeyRotateV1 struct {
+ APIVersion string `yaml:"apiVersion" json:"apiVersion"`
+ Flags BatchJobKeyRotateFlags `yaml:"flags" json:"flags"`
+ Bucket string `yaml:"bucket" json:"bucket"`
+ Prefix string `yaml:"prefix" json:"prefix"`
+ Encryption BatchJobKeyRotateEncryption `yaml:"encryption" json:"encryption"`
+}
+
+// Notify notifies notification endpoint if configured regarding job failure or success.
+func (r BatchJobKeyRotateV1) Notify(ctx context.Context, ri *batchJobInfo) error {
+ return notifyEndpoint(ctx, ri, r.Flags.Notify.Endpoint, r.Flags.Notify.Token)
+}
+
+// KeyRotate rotates encryption key of an object
+func (r *BatchJobKeyRotateV1) KeyRotate(ctx context.Context, api ObjectLayer, objInfo ObjectInfo) error {
+ srcBucket := r.Bucket
+ srcObject := objInfo.Name
+
+ if objInfo.DeleteMarker || !objInfo.VersionPurgeStatus.Empty() {
+ return nil
+ }
+ sseKMS := crypto.S3KMS.IsEncrypted(objInfo.UserDefined)
+ sseS3 := crypto.S3.IsEncrypted(objInfo.UserDefined)
+ if !sseKMS && !sseS3 { // neither sse-s3 nor sse-kms disallowed
+ return errInvalidEncryptionParameters
+ }
+ if sseKMS && r.Encryption.Type == sses3 { // previously encrypted with sse-kms, now sse-s3 disallowed
+ return errInvalidEncryptionParameters
+ }
+ versioned := globalBucketVersioningSys.PrefixEnabled(srcBucket, srcObject)
+ versionSuspended := globalBucketVersioningSys.PrefixSuspended(srcBucket, srcObject)
+
+ lock := api.NewNSLock(r.Bucket, objInfo.Name)
+ lkctx, err := lock.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ return err
+ }
+ ctx = lkctx.Context()
+ defer lock.Unlock(lkctx)
+
+ opts := ObjectOptions{
+ VersionID: objInfo.VersionID,
+ Versioned: versioned,
+ VersionSuspended: versionSuspended,
+ NoLock: true,
+ }
+ obj, err := api.GetObjectInfo(ctx, r.Bucket, objInfo.Name, opts)
+ if err != nil {
+ return err
+ }
+ oi := obj.Clone()
+ var (
+ newKeyID string
+ newKeyContext kms.Context
+ )
+ encMetadata := make(map[string]string)
+ for k, v := range oi.UserDefined {
+ if stringsHasPrefixFold(k, ReservedMetadataPrefixLower) {
+ encMetadata[k] = v
+ }
+ }
+
+ if (sseKMS || sseS3) && r.Encryption.Type == ssekms {
+ if err = r.Encryption.Validate(); err != nil {
+ return err
+ }
+ newKeyID = strings.TrimPrefix(r.Encryption.Key, crypto.ARNPrefix)
+ newKeyContext = r.Encryption.kmsContext
+ }
+ if err = rotateKey(ctx, []byte{}, newKeyID, []byte{}, r.Bucket, oi.Name, encMetadata, newKeyContext); err != nil {
+ return err
+ }
+
+ // Since we are rotating the keys, make sure to update the metadata.
+ oi.metadataOnly = true
+ oi.keyRotation = true
+ maps.Copy(oi.UserDefined, encMetadata)
+ if _, err := api.CopyObject(ctx, r.Bucket, oi.Name, r.Bucket, oi.Name, oi, ObjectOptions{
+ VersionID: oi.VersionID,
+ }, ObjectOptions{
+ VersionID: oi.VersionID,
+ NoLock: true,
+ }); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+const (
+ batchKeyRotationName = "batch-rotate.bin"
+ batchKeyRotationFormat = 1
+ batchKeyRotateVersionV1 = 1
+ batchKeyRotateVersion = batchKeyRotateVersionV1
+ batchKeyRotateAPIVersion = "v1"
+ batchKeyRotateJobDefaultRetries = 3
+ batchKeyRotateJobDefaultRetryDelay = 25 * time.Millisecond
+)
+
+// Start the batch key rottion job, resumes if there was a pending job via "job.ID"
+func (r *BatchJobKeyRotateV1) Start(ctx context.Context, api ObjectLayer, job BatchJobRequest) error {
+ ri := &batchJobInfo{
+ JobID: job.ID,
+ JobType: string(job.Type()),
+ StartTime: job.Started,
+ }
+ if err := ri.loadOrInit(ctx, api, job); err != nil {
+ return err
+ }
+ if ri.Complete {
+ return nil
+ }
+
+ globalBatchJobsMetrics.save(job.ID, ri)
+ lastObject := ri.Object
+
+ retryAttempts := job.KeyRotate.Flags.Retry.Attempts
+ if retryAttempts <= 0 {
+ retryAttempts = batchKeyRotateJobDefaultRetries
+ }
+ delay := job.KeyRotate.Flags.Retry.Delay
+ if delay <= 0 {
+ delay = batchKeyRotateJobDefaultRetryDelay
+ }
+
+ rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
+
+ selectObj := func(info FileInfo) (ok bool) {
+ if r.Flags.Filter.OlderThan > 0 && time.Since(info.ModTime) < r.Flags.Filter.OlderThan {
+ // skip all objects that are newer than specified older duration
+ return false
+ }
+
+ if r.Flags.Filter.NewerThan > 0 && time.Since(info.ModTime) >= r.Flags.Filter.NewerThan {
+ // skip all objects that are older than specified newer duration
+ return false
+ }
+
+ if !r.Flags.Filter.CreatedAfter.IsZero() && r.Flags.Filter.CreatedAfter.Before(info.ModTime) {
+ // skip all objects that are created before the specified time.
+ return false
+ }
+
+ if !r.Flags.Filter.CreatedBefore.IsZero() && r.Flags.Filter.CreatedBefore.After(info.ModTime) {
+ // skip all objects that are created after the specified time.
+ return false
+ }
+
+ if len(r.Flags.Filter.Tags) > 0 {
+ // Only parse object tags if tags filter is specified.
+ tagMap := map[string]string{}
+ tagStr := info.Metadata[xhttp.AmzObjectTagging]
+ if len(tagStr) != 0 {
+ t, err := tags.ParseObjectTags(tagStr)
+ if err != nil {
+ return false
+ }
+ tagMap = t.ToMap()
+ }
+
+ for _, kv := range r.Flags.Filter.Tags {
+ for t, v := range tagMap {
+ if kv.Match(BatchJobKV{Key: t, Value: v}) {
+ return true
+ }
+ }
+ }
+
+ // None of the provided tags filter match skip the object
+ return false
+ }
+
+ if len(r.Flags.Filter.Metadata) > 0 {
+ for _, kv := range r.Flags.Filter.Metadata {
+ for k, v := range info.Metadata {
+ if !stringsHasPrefixFold(k, "x-amz-meta-") && !isStandardHeader(k) {
+ continue
+ }
+ // We only need to match x-amz-meta or standardHeaders
+ if kv.Match(BatchJobKV{Key: k, Value: v}) {
+ return true
+ }
+ }
+ }
+
+ // None of the provided metadata filters match skip the object.
+ return false
+ }
+ if r.Flags.Filter.KMSKeyID != "" {
+ if v, ok := info.Metadata[xhttp.AmzServerSideEncryptionKmsID]; ok && strings.TrimPrefix(v, crypto.ARNPrefix) != r.Flags.Filter.KMSKeyID {
+ return false
+ }
+ }
+ return true
+ }
+
+ workerSize, err := strconv.Atoi(env.Get("_MINIO_BATCH_KEYROTATION_WORKERS", strconv.Itoa(runtime.GOMAXPROCS(0)/2)))
+ if err != nil {
+ return err
+ }
+
+ wk, err := workers.New(workerSize)
+ if err != nil {
+ // invalid worker size.
+ return err
+ }
+
+ ctx, cancel := context.WithCancel(ctx)
+
+ results := make(chan itemOrErr[ObjectInfo], 100)
+ if err := api.Walk(ctx, r.Bucket, r.Prefix, results, WalkOptions{
+ Marker: lastObject,
+ Filter: selectObj,
+ }); err != nil {
+ cancel()
+ // Do not need to retry if we can't list objects on source.
+ return err
+ }
+ failed := false
+ for res := range results {
+ if res.Err != nil {
+ failed = true
+ batchLogIf(ctx, res.Err)
+ break
+ }
+ result := res.Item
+ sseKMS := crypto.S3KMS.IsEncrypted(result.UserDefined)
+ sseS3 := crypto.S3.IsEncrypted(result.UserDefined)
+ if !sseKMS && !sseS3 { // neither sse-s3 nor sse-kms disallowed
+ continue
+ }
+ wk.Take()
+ go func() {
+ defer wk.Give()
+ for attempts := 1; attempts <= retryAttempts; attempts++ {
+ stopFn := globalBatchJobsMetrics.trace(batchJobMetricKeyRotation, job.ID, attempts)
+ success := true
+ if err := r.KeyRotate(ctx, api, result); err != nil {
+ stopFn(result, err)
+ batchLogIf(ctx, err)
+ success = false
+ if attempts >= retryAttempts {
+ auditOptions := AuditLogOptions{
+ Event: "KeyRotate",
+ APIName: "StartBatchJob",
+ Bucket: result.Bucket,
+ Object: result.Name,
+ VersionID: result.VersionID,
+ Error: err.Error(),
+ }
+ auditLogInternal(ctx, auditOptions)
+ }
+ } else {
+ stopFn(result, nil)
+ }
+ ri.trackCurrentBucketObject(r.Bucket, result, success, attempts)
+ globalBatchJobsMetrics.save(job.ID, ri)
+ // persist in-memory state to disk after every 10secs.
+ batchLogIf(ctx, ri.updateAfter(ctx, api, 10*time.Second, job))
+ if success {
+ break
+ }
+ if delay > 0 {
+ time.Sleep(delay + time.Duration(rnd.Float64()*float64(delay)))
+ }
+ }
+
+ if wait := globalBatchConfig.KeyRotationWait(); wait > 0 {
+ time.Sleep(wait)
+ }
+ }()
+ }
+ wk.Wait()
+
+ ri.Complete = !failed && ri.ObjectsFailed == 0
+ ri.Failed = failed || ri.ObjectsFailed > 0
+ globalBatchJobsMetrics.save(job.ID, ri)
+ // persist in-memory state to disk.
+ batchLogIf(ctx, ri.updateAfter(ctx, api, 0, job))
+
+ if err := r.Notify(ctx, ri); err != nil {
+ batchLogIf(ctx, fmt.Errorf("unable to notify %v", err))
+ }
+
+ cancel()
+ return nil
+}
+
+//msgp:ignore batchKeyRotationJobError
+type batchKeyRotationJobError struct {
+ Code string
+ Description string
+ HTTPStatusCode int
+}
+
+func (e batchKeyRotationJobError) Error() string {
+ return e.Description
+}
+
+// Validate validates the job definition input
+func (r *BatchJobKeyRotateV1) Validate(ctx context.Context, job BatchJobRequest, o ObjectLayer) error {
+ if r == nil {
+ return nil
+ }
+
+ if r.APIVersion != batchKeyRotateAPIVersion {
+ return errInvalidArgument
+ }
+
+ if r.Bucket == "" {
+ return errInvalidArgument
+ }
+
+ if _, err := o.GetBucketInfo(ctx, r.Bucket, BucketOptions{}); err != nil {
+ if isErrBucketNotFound(err) {
+ return batchKeyRotationJobError{
+ Code: "NoSuchSourceBucket",
+ Description: "The specified source bucket does not exist",
+ HTTPStatusCode: http.StatusNotFound,
+ }
+ }
+ return err
+ }
+ if GlobalKMS == nil {
+ return errKMSNotConfigured
+ }
+ if err := r.Encryption.Validate(); err != nil {
+ return err
+ }
+
+ for _, tag := range r.Flags.Filter.Tags {
+ if err := tag.Validate(); err != nil {
+ return err
+ }
+ }
+
+ for _, meta := range r.Flags.Filter.Metadata {
+ if err := meta.Validate(); err != nil {
+ return err
+ }
+ }
+
+ return r.Flags.Retry.Validate()
+}
diff --git a/cmd/batch-rotate_gen.go b/cmd/batch-rotate_gen.go
new file mode 100644
index 00000000..e714e1c8
--- /dev/null
+++ b/cmd/batch-rotate_gen.go
@@ -0,0 +1,1178 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "github.com/tinylib/msgp/msgp"
+)
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchJobKeyRotateEncryption) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Type":
+ {
+ var zb0002 string
+ zb0002, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Type")
+ return
+ }
+ z.Type = BatchKeyRotationType(zb0002)
+ }
+ case "Key":
+ z.Key, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Key")
+ return
+ }
+ case "Context":
+ z.Context, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Context")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z BatchJobKeyRotateEncryption) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 3
+ // write "Type"
+ err = en.Append(0x83, 0xa4, 0x54, 0x79, 0x70, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(string(z.Type))
+ if err != nil {
+ err = msgp.WrapError(err, "Type")
+ return
+ }
+ // write "Key"
+ err = en.Append(0xa3, 0x4b, 0x65, 0x79)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Key)
+ if err != nil {
+ err = msgp.WrapError(err, "Key")
+ return
+ }
+ // write "Context"
+ err = en.Append(0xa7, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Context)
+ if err != nil {
+ err = msgp.WrapError(err, "Context")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z BatchJobKeyRotateEncryption) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 3
+ // string "Type"
+ o = append(o, 0x83, 0xa4, 0x54, 0x79, 0x70, 0x65)
+ o = msgp.AppendString(o, string(z.Type))
+ // string "Key"
+ o = append(o, 0xa3, 0x4b, 0x65, 0x79)
+ o = msgp.AppendString(o, z.Key)
+ // string "Context"
+ o = append(o, 0xa7, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74)
+ o = msgp.AppendString(o, z.Context)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BatchJobKeyRotateEncryption) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Type":
+ {
+ var zb0002 string
+ zb0002, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Type")
+ return
+ }
+ z.Type = BatchKeyRotationType(zb0002)
+ }
+ case "Key":
+ z.Key, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Key")
+ return
+ }
+ case "Context":
+ z.Context, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Context")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z BatchJobKeyRotateEncryption) Msgsize() (s int) {
+ s = 1 + 5 + msgp.StringPrefixSize + len(string(z.Type)) + 4 + msgp.StringPrefixSize + len(z.Key) + 8 + msgp.StringPrefixSize + len(z.Context)
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchJobKeyRotateFlags) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Filter":
+ err = z.Filter.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Filter")
+ return
+ }
+ case "Notify":
+ err = z.Notify.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Notify")
+ return
+ }
+ case "Retry":
+ err = z.Retry.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Retry")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *BatchJobKeyRotateFlags) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 3
+ // write "Filter"
+ err = en.Append(0x83, 0xa6, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72)
+ if err != nil {
+ return
+ }
+ err = z.Filter.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Filter")
+ return
+ }
+ // write "Notify"
+ err = en.Append(0xa6, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x79)
+ if err != nil {
+ return
+ }
+ err = z.Notify.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Notify")
+ return
+ }
+ // write "Retry"
+ err = en.Append(0xa5, 0x52, 0x65, 0x74, 0x72, 0x79)
+ if err != nil {
+ return
+ }
+ err = z.Retry.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Retry")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *BatchJobKeyRotateFlags) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 3
+ // string "Filter"
+ o = append(o, 0x83, 0xa6, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72)
+ o, err = z.Filter.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Filter")
+ return
+ }
+ // string "Notify"
+ o = append(o, 0xa6, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x79)
+ o, err = z.Notify.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Notify")
+ return
+ }
+ // string "Retry"
+ o = append(o, 0xa5, 0x52, 0x65, 0x74, 0x72, 0x79)
+ o, err = z.Retry.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Retry")
+ return
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BatchJobKeyRotateFlags) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Filter":
+ bts, err = z.Filter.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Filter")
+ return
+ }
+ case "Notify":
+ bts, err = z.Notify.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Notify")
+ return
+ }
+ case "Retry":
+ bts, err = z.Retry.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Retry")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *BatchJobKeyRotateFlags) Msgsize() (s int) {
+ s = 1 + 7 + z.Filter.Msgsize() + 7 + z.Notify.Msgsize() + 6 + z.Retry.Msgsize()
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchJobKeyRotateV1) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "APIVersion":
+ z.APIVersion, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "APIVersion")
+ return
+ }
+ case "Flags":
+ var zb0002 uint32
+ zb0002, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Flags")
+ return
+ }
+ for zb0002 > 0 {
+ zb0002--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err, "Flags")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Filter":
+ err = z.Flags.Filter.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Filter")
+ return
+ }
+ case "Notify":
+ err = z.Flags.Notify.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Notify")
+ return
+ }
+ case "Retry":
+ err = z.Flags.Retry.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Retry")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err, "Flags")
+ return
+ }
+ }
+ }
+ case "Bucket":
+ z.Bucket, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "Prefix":
+ z.Prefix, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Prefix")
+ return
+ }
+ case "Encryption":
+ err = z.Encryption.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Encryption")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *BatchJobKeyRotateV1) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 5
+ // write "APIVersion"
+ err = en.Append(0x85, 0xaa, 0x41, 0x50, 0x49, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.APIVersion)
+ if err != nil {
+ err = msgp.WrapError(err, "APIVersion")
+ return
+ }
+ // write "Flags"
+ err = en.Append(0xa5, 0x46, 0x6c, 0x61, 0x67, 0x73)
+ if err != nil {
+ return
+ }
+ // map header, size 3
+ // write "Filter"
+ err = en.Append(0x83, 0xa6, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72)
+ if err != nil {
+ return
+ }
+ err = z.Flags.Filter.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Filter")
+ return
+ }
+ // write "Notify"
+ err = en.Append(0xa6, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x79)
+ if err != nil {
+ return
+ }
+ err = z.Flags.Notify.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Notify")
+ return
+ }
+ // write "Retry"
+ err = en.Append(0xa5, 0x52, 0x65, 0x74, 0x72, 0x79)
+ if err != nil {
+ return
+ }
+ err = z.Flags.Retry.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Retry")
+ return
+ }
+ // write "Bucket"
+ err = en.Append(0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Bucket)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ // write "Prefix"
+ err = en.Append(0xa6, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Prefix)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefix")
+ return
+ }
+ // write "Encryption"
+ err = en.Append(0xaa, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e)
+ if err != nil {
+ return
+ }
+ err = z.Encryption.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Encryption")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *BatchJobKeyRotateV1) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 5
+ // string "APIVersion"
+ o = append(o, 0x85, 0xaa, 0x41, 0x50, 0x49, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e)
+ o = msgp.AppendString(o, z.APIVersion)
+ // string "Flags"
+ o = append(o, 0xa5, 0x46, 0x6c, 0x61, 0x67, 0x73)
+ // map header, size 3
+ // string "Filter"
+ o = append(o, 0x83, 0xa6, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72)
+ o, err = z.Flags.Filter.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Filter")
+ return
+ }
+ // string "Notify"
+ o = append(o, 0xa6, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x79)
+ o, err = z.Flags.Notify.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Notify")
+ return
+ }
+ // string "Retry"
+ o = append(o, 0xa5, 0x52, 0x65, 0x74, 0x72, 0x79)
+ o, err = z.Flags.Retry.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Retry")
+ return
+ }
+ // string "Bucket"
+ o = append(o, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ o = msgp.AppendString(o, z.Bucket)
+ // string "Prefix"
+ o = append(o, 0xa6, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78)
+ o = msgp.AppendString(o, z.Prefix)
+ // string "Encryption"
+ o = append(o, 0xaa, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e)
+ o, err = z.Encryption.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Encryption")
+ return
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BatchJobKeyRotateV1) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "APIVersion":
+ z.APIVersion, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "APIVersion")
+ return
+ }
+ case "Flags":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags")
+ return
+ }
+ for zb0002 > 0 {
+ zb0002--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Filter":
+ bts, err = z.Flags.Filter.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Filter")
+ return
+ }
+ case "Notify":
+ bts, err = z.Flags.Notify.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Notify")
+ return
+ }
+ case "Retry":
+ bts, err = z.Flags.Retry.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags", "Retry")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Flags")
+ return
+ }
+ }
+ }
+ case "Bucket":
+ z.Bucket, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "Prefix":
+ z.Prefix, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefix")
+ return
+ }
+ case "Encryption":
+ bts, err = z.Encryption.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Encryption")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *BatchJobKeyRotateV1) Msgsize() (s int) {
+ s = 1 + 11 + msgp.StringPrefixSize + len(z.APIVersion) + 6 + 1 + 7 + z.Flags.Filter.Msgsize() + 7 + z.Flags.Notify.Msgsize() + 6 + z.Flags.Retry.Msgsize() + 7 + msgp.StringPrefixSize + len(z.Bucket) + 7 + msgp.StringPrefixSize + len(z.Prefix) + 11 + z.Encryption.Msgsize()
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchKeyRotateFilter) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "NewerThan":
+ z.NewerThan, err = dc.ReadDuration()
+ if err != nil {
+ err = msgp.WrapError(err, "NewerThan")
+ return
+ }
+ case "OlderThan":
+ z.OlderThan, err = dc.ReadDuration()
+ if err != nil {
+ err = msgp.WrapError(err, "OlderThan")
+ return
+ }
+ case "CreatedAfter":
+ z.CreatedAfter, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "CreatedAfter")
+ return
+ }
+ case "CreatedBefore":
+ z.CreatedBefore, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "CreatedBefore")
+ return
+ }
+ case "Tags":
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Tags")
+ return
+ }
+ if cap(z.Tags) >= int(zb0002) {
+ z.Tags = (z.Tags)[:zb0002]
+ } else {
+ z.Tags = make([]BatchJobKV, zb0002)
+ }
+ for za0001 := range z.Tags {
+ err = z.Tags[za0001].DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Tags", za0001)
+ return
+ }
+ }
+ case "Metadata":
+ var zb0003 uint32
+ zb0003, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Metadata")
+ return
+ }
+ if cap(z.Metadata) >= int(zb0003) {
+ z.Metadata = (z.Metadata)[:zb0003]
+ } else {
+ z.Metadata = make([]BatchJobKV, zb0003)
+ }
+ for za0002 := range z.Metadata {
+ err = z.Metadata[za0002].DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Metadata", za0002)
+ return
+ }
+ }
+ case "KMSKeyID":
+ z.KMSKeyID, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "KMSKeyID")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *BatchKeyRotateFilter) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 7
+ // write "NewerThan"
+ err = en.Append(0x87, 0xa9, 0x4e, 0x65, 0x77, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e)
+ if err != nil {
+ return
+ }
+ err = en.WriteDuration(z.NewerThan)
+ if err != nil {
+ err = msgp.WrapError(err, "NewerThan")
+ return
+ }
+ // write "OlderThan"
+ err = en.Append(0xa9, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e)
+ if err != nil {
+ return
+ }
+ err = en.WriteDuration(z.OlderThan)
+ if err != nil {
+ err = msgp.WrapError(err, "OlderThan")
+ return
+ }
+ // write "CreatedAfter"
+ err = en.Append(0xac, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x66, 0x74, 0x65, 0x72)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.CreatedAfter)
+ if err != nil {
+ err = msgp.WrapError(err, "CreatedAfter")
+ return
+ }
+ // write "CreatedBefore"
+ err = en.Append(0xad, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.CreatedBefore)
+ if err != nil {
+ err = msgp.WrapError(err, "CreatedBefore")
+ return
+ }
+ // write "Tags"
+ err = en.Append(0xa4, 0x54, 0x61, 0x67, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(len(z.Tags)))
+ if err != nil {
+ err = msgp.WrapError(err, "Tags")
+ return
+ }
+ for za0001 := range z.Tags {
+ err = z.Tags[za0001].EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Tags", za0001)
+ return
+ }
+ }
+ // write "Metadata"
+ err = en.Append(0xa8, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(len(z.Metadata)))
+ if err != nil {
+ err = msgp.WrapError(err, "Metadata")
+ return
+ }
+ for za0002 := range z.Metadata {
+ err = z.Metadata[za0002].EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Metadata", za0002)
+ return
+ }
+ }
+ // write "KMSKeyID"
+ err = en.Append(0xa8, 0x4b, 0x4d, 0x53, 0x4b, 0x65, 0x79, 0x49, 0x44)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.KMSKeyID)
+ if err != nil {
+ err = msgp.WrapError(err, "KMSKeyID")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *BatchKeyRotateFilter) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 7
+ // string "NewerThan"
+ o = append(o, 0x87, 0xa9, 0x4e, 0x65, 0x77, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e)
+ o = msgp.AppendDuration(o, z.NewerThan)
+ // string "OlderThan"
+ o = append(o, 0xa9, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e)
+ o = msgp.AppendDuration(o, z.OlderThan)
+ // string "CreatedAfter"
+ o = append(o, 0xac, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x66, 0x74, 0x65, 0x72)
+ o = msgp.AppendTime(o, z.CreatedAfter)
+ // string "CreatedBefore"
+ o = append(o, 0xad, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65)
+ o = msgp.AppendTime(o, z.CreatedBefore)
+ // string "Tags"
+ o = append(o, 0xa4, 0x54, 0x61, 0x67, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.Tags)))
+ for za0001 := range z.Tags {
+ o, err = z.Tags[za0001].MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Tags", za0001)
+ return
+ }
+ }
+ // string "Metadata"
+ o = append(o, 0xa8, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.Metadata)))
+ for za0002 := range z.Metadata {
+ o, err = z.Metadata[za0002].MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Metadata", za0002)
+ return
+ }
+ }
+ // string "KMSKeyID"
+ o = append(o, 0xa8, 0x4b, 0x4d, 0x53, 0x4b, 0x65, 0x79, 0x49, 0x44)
+ o = msgp.AppendString(o, z.KMSKeyID)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BatchKeyRotateFilter) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "NewerThan":
+ z.NewerThan, bts, err = msgp.ReadDurationBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "NewerThan")
+ return
+ }
+ case "OlderThan":
+ z.OlderThan, bts, err = msgp.ReadDurationBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "OlderThan")
+ return
+ }
+ case "CreatedAfter":
+ z.CreatedAfter, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "CreatedAfter")
+ return
+ }
+ case "CreatedBefore":
+ z.CreatedBefore, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "CreatedBefore")
+ return
+ }
+ case "Tags":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Tags")
+ return
+ }
+ if cap(z.Tags) >= int(zb0002) {
+ z.Tags = (z.Tags)[:zb0002]
+ } else {
+ z.Tags = make([]BatchJobKV, zb0002)
+ }
+ for za0001 := range z.Tags {
+ bts, err = z.Tags[za0001].UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Tags", za0001)
+ return
+ }
+ }
+ case "Metadata":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Metadata")
+ return
+ }
+ if cap(z.Metadata) >= int(zb0003) {
+ z.Metadata = (z.Metadata)[:zb0003]
+ } else {
+ z.Metadata = make([]BatchJobKV, zb0003)
+ }
+ for za0002 := range z.Metadata {
+ bts, err = z.Metadata[za0002].UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Metadata", za0002)
+ return
+ }
+ }
+ case "KMSKeyID":
+ z.KMSKeyID, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "KMSKeyID")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *BatchKeyRotateFilter) Msgsize() (s int) {
+ s = 1 + 10 + msgp.DurationSize + 10 + msgp.DurationSize + 13 + msgp.TimeSize + 14 + msgp.TimeSize + 5 + msgp.ArrayHeaderSize
+ for za0001 := range z.Tags {
+ s += z.Tags[za0001].Msgsize()
+ }
+ s += 9 + msgp.ArrayHeaderSize
+ for za0002 := range z.Metadata {
+ s += z.Metadata[za0002].Msgsize()
+ }
+ s += 9 + msgp.StringPrefixSize + len(z.KMSKeyID)
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchKeyRotateNotification) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Endpoint":
+ z.Endpoint, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Endpoint")
+ return
+ }
+ case "Token":
+ z.Token, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Token")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z BatchKeyRotateNotification) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 2
+ // write "Endpoint"
+ err = en.Append(0x82, 0xa8, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Endpoint)
+ if err != nil {
+ err = msgp.WrapError(err, "Endpoint")
+ return
+ }
+ // write "Token"
+ err = en.Append(0xa5, 0x54, 0x6f, 0x6b, 0x65, 0x6e)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Token)
+ if err != nil {
+ err = msgp.WrapError(err, "Token")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z BatchKeyRotateNotification) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 2
+ // string "Endpoint"
+ o = append(o, 0x82, 0xa8, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74)
+ o = msgp.AppendString(o, z.Endpoint)
+ // string "Token"
+ o = append(o, 0xa5, 0x54, 0x6f, 0x6b, 0x65, 0x6e)
+ o = msgp.AppendString(o, z.Token)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BatchKeyRotateNotification) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Endpoint":
+ z.Endpoint, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Endpoint")
+ return
+ }
+ case "Token":
+ z.Token, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Token")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z BatchKeyRotateNotification) Msgsize() (s int) {
+ s = 1 + 9 + msgp.StringPrefixSize + len(z.Endpoint) + 6 + msgp.StringPrefixSize + len(z.Token)
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *BatchKeyRotationType) DecodeMsg(dc *msgp.Reader) (err error) {
+ {
+ var zb0001 string
+ zb0001, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ (*z) = BatchKeyRotationType(zb0001)
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z BatchKeyRotationType) 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 BatchKeyRotationType) 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 *BatchKeyRotationType) 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) = BatchKeyRotationType(zb0001)
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z BatchKeyRotationType) Msgsize() (s int) {
+ s = msgp.StringPrefixSize + len(string(z))
+ return
+}
diff --git a/cmd/batch-rotate_gen_test.go b/cmd/batch-rotate_gen_test.go
new file mode 100644
index 00000000..906b7938
--- /dev/null
+++ b/cmd/batch-rotate_gen_test.go
@@ -0,0 +1,575 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+func TestMarshalUnmarshalBatchJobKeyRotateEncryption(t *testing.T) {
+ v := BatchJobKeyRotateEncryption{}
+ 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 BenchmarkMarshalMsgBatchJobKeyRotateEncryption(b *testing.B) {
+ v := BatchJobKeyRotateEncryption{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBatchJobKeyRotateEncryption(b *testing.B) {
+ v := BatchJobKeyRotateEncryption{}
+ 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 BenchmarkUnmarshalBatchJobKeyRotateEncryption(b *testing.B) {
+ v := BatchJobKeyRotateEncryption{}
+ 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 TestEncodeDecodeBatchJobKeyRotateEncryption(t *testing.T) {
+ v := BatchJobKeyRotateEncryption{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBatchJobKeyRotateEncryption Msgsize() is inaccurate")
+ }
+
+ vn := BatchJobKeyRotateEncryption{}
+ 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 BenchmarkEncodeBatchJobKeyRotateEncryption(b *testing.B) {
+ v := BatchJobKeyRotateEncryption{}
+ 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 BenchmarkDecodeBatchJobKeyRotateEncryption(b *testing.B) {
+ v := BatchJobKeyRotateEncryption{}
+ 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 TestMarshalUnmarshalBatchJobKeyRotateFlags(t *testing.T) {
+ v := BatchJobKeyRotateFlags{}
+ 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 BenchmarkMarshalMsgBatchJobKeyRotateFlags(b *testing.B) {
+ v := BatchJobKeyRotateFlags{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBatchJobKeyRotateFlags(b *testing.B) {
+ v := BatchJobKeyRotateFlags{}
+ 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 BenchmarkUnmarshalBatchJobKeyRotateFlags(b *testing.B) {
+ v := BatchJobKeyRotateFlags{}
+ 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 TestEncodeDecodeBatchJobKeyRotateFlags(t *testing.T) {
+ v := BatchJobKeyRotateFlags{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBatchJobKeyRotateFlags Msgsize() is inaccurate")
+ }
+
+ vn := BatchJobKeyRotateFlags{}
+ 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 BenchmarkEncodeBatchJobKeyRotateFlags(b *testing.B) {
+ v := BatchJobKeyRotateFlags{}
+ 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 BenchmarkDecodeBatchJobKeyRotateFlags(b *testing.B) {
+ v := BatchJobKeyRotateFlags{}
+ 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 TestMarshalUnmarshalBatchJobKeyRotateV1(t *testing.T) {
+ v := BatchJobKeyRotateV1{}
+ 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 BenchmarkMarshalMsgBatchJobKeyRotateV1(b *testing.B) {
+ v := BatchJobKeyRotateV1{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBatchJobKeyRotateV1(b *testing.B) {
+ v := BatchJobKeyRotateV1{}
+ 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 BenchmarkUnmarshalBatchJobKeyRotateV1(b *testing.B) {
+ v := BatchJobKeyRotateV1{}
+ 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 TestEncodeDecodeBatchJobKeyRotateV1(t *testing.T) {
+ v := BatchJobKeyRotateV1{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBatchJobKeyRotateV1 Msgsize() is inaccurate")
+ }
+
+ vn := BatchJobKeyRotateV1{}
+ 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 BenchmarkEncodeBatchJobKeyRotateV1(b *testing.B) {
+ v := BatchJobKeyRotateV1{}
+ 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 BenchmarkDecodeBatchJobKeyRotateV1(b *testing.B) {
+ v := BatchJobKeyRotateV1{}
+ 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 TestMarshalUnmarshalBatchKeyRotateFilter(t *testing.T) {
+ v := BatchKeyRotateFilter{}
+ 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 BenchmarkMarshalMsgBatchKeyRotateFilter(b *testing.B) {
+ v := BatchKeyRotateFilter{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBatchKeyRotateFilter(b *testing.B) {
+ v := BatchKeyRotateFilter{}
+ 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 BenchmarkUnmarshalBatchKeyRotateFilter(b *testing.B) {
+ v := BatchKeyRotateFilter{}
+ 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 TestEncodeDecodeBatchKeyRotateFilter(t *testing.T) {
+ v := BatchKeyRotateFilter{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBatchKeyRotateFilter Msgsize() is inaccurate")
+ }
+
+ vn := BatchKeyRotateFilter{}
+ 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 BenchmarkEncodeBatchKeyRotateFilter(b *testing.B) {
+ v := BatchKeyRotateFilter{}
+ 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 BenchmarkDecodeBatchKeyRotateFilter(b *testing.B) {
+ v := BatchKeyRotateFilter{}
+ 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 TestMarshalUnmarshalBatchKeyRotateNotification(t *testing.T) {
+ v := BatchKeyRotateNotification{}
+ 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 BenchmarkMarshalMsgBatchKeyRotateNotification(b *testing.B) {
+ v := BatchKeyRotateNotification{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBatchKeyRotateNotification(b *testing.B) {
+ v := BatchKeyRotateNotification{}
+ 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 BenchmarkUnmarshalBatchKeyRotateNotification(b *testing.B) {
+ v := BatchKeyRotateNotification{}
+ 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 TestEncodeDecodeBatchKeyRotateNotification(t *testing.T) {
+ v := BatchKeyRotateNotification{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBatchKeyRotateNotification Msgsize() is inaccurate")
+ }
+
+ vn := BatchKeyRotateNotification{}
+ 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 BenchmarkEncodeBatchKeyRotateNotification(b *testing.B) {
+ v := BatchKeyRotateNotification{}
+ 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 BenchmarkDecodeBatchKeyRotateNotification(b *testing.B) {
+ v := BatchKeyRotateNotification{}
+ 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)
+ }
+ }
+}
diff --git a/cmd/batchjobmetric_string.go b/cmd/batchjobmetric_string.go
new file mode 100644
index 00000000..a1697a19
--- /dev/null
+++ b/cmd/batchjobmetric_string.go
@@ -0,0 +1,25 @@
+// Code generated by "stringer -type=batchJobMetric -trimprefix=batchJobMetric batch-handlers.go"; DO NOT EDIT.
+
+package cmd
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[batchJobMetricReplication-0]
+ _ = x[batchJobMetricKeyRotation-1]
+ _ = x[batchJobMetricExpire-2]
+}
+
+const _batchJobMetric_name = "ReplicationKeyRotationExpire"
+
+var _batchJobMetric_index = [...]uint8{0, 11, 22, 28}
+
+func (i batchJobMetric) String() string {
+ if i >= batchJobMetric(len(_batchJobMetric_index)-1) {
+ return "batchJobMetric(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _batchJobMetric_name[_batchJobMetric_index[i]:_batchJobMetric_index[i+1]]
+}
diff --git a/cmd/benchmark-utils_test.go b/cmd/benchmark-utils_test.go
new file mode 100644
index 00000000..0f7025a9
--- /dev/null
+++ b/cmd/benchmark-utils_test.go
@@ -0,0 +1,235 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "math"
+ "math/rand"
+ "strconv"
+ "testing"
+
+ "github.com/dustin/go-humanize"
+)
+
+// Benchmark utility functions for ObjectLayer.PutObject().
+// Creates Object layer setup ( MakeBucket ) and then runs the PutObject benchmark.
+func runPutObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
+ var err error
+ // obtains random bucket name.
+ bucket := getRandomBucketName()
+ // create bucket.
+ err = obj.MakeBucket(b.Context(), bucket, MakeBucketOptions{})
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ // get text data generated for number of bytes equal to object size.
+ textData := generateBytesData(objSize)
+ // generate md5sum for the generated data.
+ // md5sum of the data to written is required as input for PutObject.
+
+ md5hex := getMD5Hash(textData)
+ sha256hex := ""
+
+ // benchmark utility which helps obtain number of allocations and bytes allocated per ops.
+ b.ReportAllocs()
+ // the actual benchmark for PutObject starts here. Reset the benchmark timer.
+
+ for i := 0; b.Loop(); i++ {
+ // insert the object.
+ objInfo, err := obj.PutObject(b.Context(), bucket, "object"+strconv.Itoa(i),
+ mustGetPutObjReader(b, bytes.NewReader(textData), int64(len(textData)), md5hex, sha256hex), ObjectOptions{})
+ if err != nil {
+ b.Fatal(err)
+ }
+ if objInfo.ETag != md5hex {
+ b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, objInfo.ETag, md5hex)
+ }
+ }
+ // Benchmark ends here. Stop timer.
+ b.StopTimer()
+}
+
+// Benchmark utility functions for ObjectLayer.PutObjectPart().
+// Creates Object layer setup ( MakeBucket ) and then runs the PutObjectPart benchmark.
+func runPutObjectPartBenchmark(b *testing.B, obj ObjectLayer, partSize int) {
+ var err error
+ // obtains random bucket name.
+ bucket := getRandomBucketName()
+ object := getRandomObjectName()
+
+ // create bucket.
+ err = obj.MakeBucket(b.Context(), bucket, MakeBucketOptions{})
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ objSize := 128 * humanize.MiByte
+
+ // PutObjectPart returns etag of the object inserted.
+ // etag variable is assigned with that value.
+ var etag string
+ // get text data generated for number of bytes equal to object size.
+ textData := generateBytesData(objSize)
+ // generate md5sum for the generated data.
+ // md5sum of the data to written is required as input for NewMultipartUpload.
+ res, err := obj.NewMultipartUpload(b.Context(), bucket, object, ObjectOptions{})
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ sha256hex := ""
+
+ var textPartData []byte
+ // benchmark utility which helps obtain number of allocations and bytes allocated per ops.
+ b.ReportAllocs()
+ // the actual benchmark for PutObjectPart starts here. Reset the benchmark timer.
+
+ for i := 0; b.Loop(); i++ {
+ // insert the object.
+ totalPartsNR := int(math.Ceil(float64(objSize) / float64(partSize)))
+ for j := range totalPartsNR {
+ if j < totalPartsNR-1 {
+ textPartData = textData[j*partSize : (j+1)*partSize-1]
+ } else {
+ textPartData = textData[j*partSize:]
+ }
+ md5hex := getMD5Hash(textPartData)
+ var partInfo PartInfo
+ partInfo, err = obj.PutObjectPart(b.Context(), bucket, object, res.UploadID, j,
+ mustGetPutObjReader(b, bytes.NewReader(textPartData), int64(len(textPartData)), md5hex, sha256hex), ObjectOptions{})
+ if err != nil {
+ b.Fatal(err)
+ }
+ if partInfo.ETag != md5hex {
+ b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, etag, md5hex)
+ }
+ }
+ }
+ // Benchmark ends here. Stop timer.
+ b.StopTimer()
+}
+
+// creates Erasure/FS backend setup, obtains the object layer and calls the runPutObjectPartBenchmark function.
+func benchmarkPutObjectPart(b *testing.B, instanceType string, objSize int) {
+ // create a temp Erasure/FS backend.
+ ctx, cancel := context.WithCancel(b.Context())
+ defer cancel()
+ objLayer, disks, err := prepareTestBackend(ctx, instanceType)
+ if err != nil {
+ b.Fatalf("Failed obtaining Temp Backend: %s", err)
+ }
+ // cleaning up the backend by removing all the directories and files created on function return.
+ defer removeRoots(disks)
+
+ // uses *testing.B and the object Layer to run the benchmark.
+ runPutObjectPartBenchmark(b, objLayer, objSize)
+}
+
+// creates Erasure/FS backend setup, obtains the object layer and calls the runPutObjectBenchmark function.
+func benchmarkPutObject(b *testing.B, instanceType string, objSize int) {
+ // create a temp Erasure/FS backend.
+ ctx, cancel := context.WithCancel(b.Context())
+ defer cancel()
+ objLayer, disks, err := prepareTestBackend(ctx, instanceType)
+ if err != nil {
+ b.Fatalf("Failed obtaining Temp Backend: %s", err)
+ }
+ // cleaning up the backend by removing all the directories and files created on function return.
+ defer removeRoots(disks)
+
+ // uses *testing.B and the object Layer to run the benchmark.
+ runPutObjectBenchmark(b, objLayer, objSize)
+}
+
+// creates Erasure/FS backend setup, obtains the object layer and runs parallel benchmark for put object.
+func benchmarkPutObjectParallel(b *testing.B, instanceType string, objSize int) {
+ // create a temp Erasure/FS backend.
+ ctx, cancel := context.WithCancel(b.Context())
+ defer cancel()
+ objLayer, disks, err := prepareTestBackend(ctx, instanceType)
+ if err != nil {
+ b.Fatalf("Failed obtaining Temp Backend: %s", err)
+ }
+ // cleaning up the backend by removing all the directories and files created on function return.
+ defer removeRoots(disks)
+
+ // uses *testing.B and the object Layer to run the benchmark.
+ runPutObjectBenchmarkParallel(b, objLayer, objSize)
+}
+
+// randomly picks a character and returns its equivalent byte array.
+func getRandomByte() []byte {
+ const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ // seeding the random number generator.
+ rand.Seed(UTCNow().UnixNano())
+ // pick a character randomly.
+ return []byte{letterBytes[rand.Intn(len(letterBytes))]}
+}
+
+// picks a random byte and repeats it to size bytes.
+func generateBytesData(size int) []byte {
+ // repeat the random character chosen size
+ return bytes.Repeat(getRandomByte(), size)
+}
+
+// Parallel benchmark utility functions for ObjectLayer.PutObject().
+// Creates Object layer setup ( MakeBucket ) and then runs the PutObject benchmark.
+func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
+ // obtains random bucket name.
+ bucket := getRandomBucketName()
+ // create bucket.
+ err := obj.MakeBucket(b.Context(), bucket, MakeBucketOptions{})
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ // get text data generated for number of bytes equal to object size.
+ textData := generateBytesData(objSize)
+ // generate md5sum for the generated data.
+ // md5sum of the data to written is required as input for PutObject.
+
+ md5hex := getMD5Hash(textData)
+ sha256hex := ""
+
+ // benchmark utility which helps obtain number of allocations and bytes allocated per ops.
+ b.ReportAllocs()
+ // the actual benchmark for PutObject starts here. Reset the benchmark timer.
+ b.ResetTimer()
+
+ b.RunParallel(func(pb *testing.PB) {
+ i := 0
+ for pb.Next() {
+ // insert the object.
+ objInfo, err := obj.PutObject(b.Context(), bucket, "object"+strconv.Itoa(i),
+ mustGetPutObjReader(b, bytes.NewReader(textData), int64(len(textData)), md5hex, sha256hex), ObjectOptions{})
+ if err != nil {
+ b.Fatal(err)
+ }
+ if objInfo.ETag != md5hex {
+ b.Fatalf("Write no: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", objInfo.ETag, md5hex)
+ }
+ i++
+ }
+ })
+
+ // Benchmark ends here. Stop timer.
+ b.StopTimer()
+}
diff --git a/cmd/bitrot-streaming.go b/cmd/bitrot-streaming.go
new file mode 100644
index 00000000..7c1a313b
--- /dev/null
+++ b/cmd/bitrot-streaming.go
@@ -0,0 +1,215 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "hash"
+ "io"
+ "sync"
+
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/minio/internal/ringbuffer"
+)
+
+// Calculates bitrot in chunks and writes the hash into the stream.
+type streamingBitrotWriter struct {
+ iow io.WriteCloser
+ closeWithErr func(err error)
+ h hash.Hash
+ shardSize int64
+ canClose *sync.WaitGroup
+ byteBuf []byte
+ finished bool
+}
+
+func (b *streamingBitrotWriter) Write(p []byte) (int, error) {
+ if len(p) == 0 {
+ 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.Write(p)
+ hashBytes := b.h.Sum(nil)
+ _, err := b.iow.Write(hashBytes)
+ if err != nil {
+ b.closeWithErr(err)
+ return 0, err
+ }
+ n, err := b.iow.Write(p)
+ if err != nil {
+ b.closeWithErr(err)
+ return n, err
+ }
+ if n != len(p) {
+ err = io.ErrShortWrite
+ b.closeWithErr(err)
+ }
+ return n, err
+}
+
+func (b *streamingBitrotWriter) Close() error {
+ // Close the underlying writer.
+ // This will also flush the ring buffer if used.
+ err := b.iow.Close()
+
+ // Wait for all data to be written before returning else it causes race conditions.
+ // Race condition is because of io.PipeWriter implementation. i.e consider the following
+ // sequent of operations:
+ // 1) pipe.Write()
+ // 2) pipe.Close()
+ // Now pipe.Close() can return before the data is read on the other end of the pipe and written to the disk
+ // Hence an immediate Read() on the file can return incorrect data.
+ if b.canClose != nil {
+ b.canClose.Wait()
+ }
+
+ // Recycle the buffer.
+ if b.byteBuf != nil {
+ globalBytePoolCap.Load().Put(b.byteBuf)
+ b.byteBuf = nil
+ }
+ return err
+}
+
+// newStreamingBitrotWriterBuffer returns streaming bitrot writer implementation.
+// The output is written to the supplied writer w.
+func newStreamingBitrotWriterBuffer(w io.Writer, algo BitrotAlgorithm, shardSize int64) io.Writer {
+ return &streamingBitrotWriter{iow: ioutil.NopCloser(w), h: algo.New(), shardSize: shardSize, canClose: nil, closeWithErr: func(err error) {}}
+}
+
+// Returns streaming bitrot writer implementation.
+func newStreamingBitrotWriter(disk StorageAPI, origvolume, volume, filePath string, length int64, algo BitrotAlgorithm, shardSize int64) io.Writer {
+ h := algo.New()
+ buf := globalBytePoolCap.Load().Get()
+ rb := ringbuffer.NewBuffer(buf[:cap(buf)]).SetBlocking(true)
+
+ bw := &streamingBitrotWriter{
+ iow: ioutil.NewDeadlineWriter(rb.WriteCloser(), globalDriveConfig.GetMaxTimeout()),
+ closeWithErr: rb.CloseWithError,
+ h: h,
+ shardSize: shardSize,
+ canClose: &sync.WaitGroup{},
+ byteBuf: buf,
+ }
+ bw.canClose.Add(1)
+ go func() {
+ defer bw.canClose.Done()
+
+ totalFileSize := int64(-1) // For compressed objects length will be unknown (represented by length=-1)
+ if length != -1 {
+ bitrotSumsTotalSize := ceilFrac(length, shardSize) * int64(h.Size()) // Size used for storing bitrot checksums.
+ totalFileSize = bitrotSumsTotalSize + length
+ }
+ rb.CloseWithError(disk.CreateFile(context.TODO(), origvolume, volume, filePath, totalFileSize, rb))
+ }()
+ return bw
+}
+
+// ReadAt() implementation which verifies the bitrot hash available as part of the stream.
+type streamingBitrotReader struct {
+ disk StorageAPI
+ data []byte
+ rc io.Reader
+ volume string
+ filePath string
+ tillOffset int64
+ currOffset int64
+ h hash.Hash
+ shardSize int64
+ hashBytes []byte
+}
+
+func (b *streamingBitrotReader) Close() error {
+ if b.rc == nil {
+ return nil
+ }
+ if closer, ok := b.rc.(io.Closer); ok {
+ // drain the body for connection reuse at network layer.
+ xhttp.DrainBody(io.NopCloser(b.rc))
+ return closer.Close()
+ }
+ return nil
+}
+
+func (b *streamingBitrotReader) ReadAt(buf []byte, offset int64) (int, error) {
+ var err error
+ if offset%b.shardSize != 0 {
+ // Offset should always be aligned to b.shardSize
+ // Can never happen unless there are programmer bugs
+ return 0, errUnexpected
+ }
+ if b.rc == nil {
+ // For the first ReadAt() call we need to open the stream for reading.
+ b.currOffset = offset
+ streamOffset := (offset/b.shardSize)*int64(b.h.Size()) + offset
+ if len(b.data) == 0 && b.tillOffset != streamOffset {
+ b.rc, err = b.disk.ReadFileStream(context.TODO(), b.volume, b.filePath, streamOffset, b.tillOffset-streamOffset)
+ } else {
+ b.rc = io.NewSectionReader(bytes.NewReader(b.data), streamOffset, b.tillOffset-streamOffset)
+ }
+ if err != nil {
+ return 0, err
+ }
+ }
+ if offset != b.currOffset {
+ // Can never happen unless there are programmer bugs
+ return 0, errUnexpected
+ }
+ b.h.Reset()
+ _, err = io.ReadFull(b.rc, b.hashBytes)
+ if err != nil {
+ return 0, err
+ }
+ _, err = io.ReadFull(b.rc, buf)
+ if err != nil {
+ return 0, err
+ }
+ b.h.Write(buf)
+ if !bytes.Equal(b.h.Sum(nil), b.hashBytes) {
+ return 0, errFileCorrupt
+ }
+ b.currOffset += int64(len(buf))
+ return len(buf), nil
+}
+
+// Returns streaming bitrot reader implementation.
+func newStreamingBitrotReader(disk StorageAPI, data []byte, volume, filePath string, tillOffset int64, algo BitrotAlgorithm, shardSize int64) *streamingBitrotReader {
+ h := algo.New()
+ return &streamingBitrotReader{
+ disk: disk,
+ data: data,
+ volume: volume,
+ filePath: filePath,
+ tillOffset: ceilFrac(tillOffset, shardSize)*int64(h.Size()) + tillOffset,
+ h: h,
+ shardSize: shardSize,
+ hashBytes: make([]byte, h.Size()),
+ }
+}
diff --git a/cmd/bitrot-whole.go b/cmd/bitrot-whole.go
new file mode 100644
index 00000000..3278b1c7
--- /dev/null
+++ b/cmd/bitrot-whole.go
@@ -0,0 +1,91 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "hash"
+ "io"
+)
+
+// Implementation to calculate bitrot for the whole file.
+type wholeBitrotWriter struct {
+ disk StorageAPI
+ volume string
+ filePath string
+ shardSize int64 // This is the shard size of the erasure logic
+ hash.Hash // For bitrot hash
+}
+
+func (b *wholeBitrotWriter) Write(p []byte) (int, error) {
+ err := b.disk.AppendFile(context.TODO(), b.volume, b.filePath, p)
+ if err != nil {
+ return 0, err
+ }
+ _, err = b.Hash.Write(p)
+ if err != nil {
+ return 0, err
+ }
+ return len(p), nil
+}
+
+func (b *wholeBitrotWriter) Close() error {
+ return nil
+}
+
+// Returns whole-file bitrot writer.
+func newWholeBitrotWriter(disk StorageAPI, volume, filePath string, algo BitrotAlgorithm, shardSize int64) io.WriteCloser {
+ return &wholeBitrotWriter{disk, volume, filePath, shardSize, algo.New()}
+}
+
+// Implementation to verify bitrot for the whole file.
+type wholeBitrotReader struct {
+ disk StorageAPI
+ volume string
+ filePath string
+ verifier *BitrotVerifier // Holds the bit-rot info
+ tillOffset int64 // Affects the length of data requested in disk.ReadFile depending on Read()'s offset
+ buf []byte // Holds bit-rot verified data
+}
+
+func (b *wholeBitrotReader) ReadAt(buf []byte, offset int64) (n int, err error) {
+ if b.buf == nil {
+ b.buf = make([]byte, b.tillOffset-offset)
+ if _, err := b.disk.ReadFile(context.TODO(), b.volume, b.filePath, offset, b.buf, b.verifier); err != nil {
+ return 0, err
+ }
+ }
+ if len(b.buf) < len(buf) {
+ return 0, errLessData
+ }
+ n = copy(buf, b.buf)
+ b.buf = b.buf[n:]
+ return n, nil
+}
+
+// Returns whole-file bitrot reader.
+func newWholeBitrotReader(disk StorageAPI, volume, filePath string, algo BitrotAlgorithm, tillOffset int64, sum []byte) *wholeBitrotReader {
+ return &wholeBitrotReader{
+ disk: disk,
+ volume: volume,
+ filePath: filePath,
+ verifier: &BitrotVerifier{algo, sum},
+ tillOffset: tillOffset,
+ buf: nil,
+ }
+}
diff --git a/cmd/bitrot.go b/cmd/bitrot.go
new file mode 100644
index 00000000..f1b2a0e4
--- /dev/null
+++ b/cmd/bitrot.go
@@ -0,0 +1,255 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "hash"
+ "io"
+
+ "github.com/minio/highwayhash"
+ "github.com/minio/minio/internal/hash/sha256"
+ "golang.org/x/crypto/blake2b"
+
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/minio/internal/logger"
+)
+
+// magic HH-256 key as HH-256 hash of the first 100 decimals of π as utf-8 string with a zero key.
+var magicHighwayHash256Key = []byte("\x4b\xe7\x34\xfa\x8e\x23\x8a\xcd\x26\x3e\x83\xe6\xbb\x96\x85\x52\x04\x0f\x93\x5d\xa3\x9f\x44\x14\x97\xe0\x9d\x13\x22\xde\x36\xa0")
+
+var bitrotAlgorithms = map[BitrotAlgorithm]string{
+ SHA256: "sha256",
+ BLAKE2b512: "blake2b",
+ HighwayHash256: "highwayhash256",
+ HighwayHash256S: "highwayhash256S",
+}
+
+// New returns a new hash.Hash calculating the given bitrot algorithm.
+func (a BitrotAlgorithm) New() hash.Hash {
+ switch a {
+ case SHA256:
+ return sha256.New()
+ case BLAKE2b512:
+ b2, _ := blake2b.New512(nil) // New512 never returns an error if the key is nil
+ return b2
+ case HighwayHash256:
+ hh, _ := highwayhash.New(magicHighwayHash256Key) // New will never return error since key is 256 bit
+ return hh
+ case HighwayHash256S:
+ hh, _ := highwayhash.New(magicHighwayHash256Key) // New will never return error since key is 256 bit
+ return hh
+ default:
+ logger.CriticalIf(GlobalContext, errors.New("Unsupported bitrot algorithm"))
+ return nil
+ }
+}
+
+// Available reports whether the given algorithm is available.
+func (a BitrotAlgorithm) Available() bool {
+ _, ok := bitrotAlgorithms[a]
+ return ok
+}
+
+// String returns the string identifier for a given bitrot algorithm.
+// If the algorithm is not supported String panics.
+func (a BitrotAlgorithm) String() string {
+ name, ok := bitrotAlgorithms[a]
+ if !ok {
+ logger.CriticalIf(GlobalContext, errors.New("Unsupported bitrot algorithm"))
+ }
+ return name
+}
+
+// NewBitrotVerifier returns a new BitrotVerifier implementing the given algorithm.
+func NewBitrotVerifier(algorithm BitrotAlgorithm, checksum []byte) *BitrotVerifier {
+ return &BitrotVerifier{algorithm, checksum}
+}
+
+// BitrotVerifier can be used to verify protected data.
+type BitrotVerifier struct {
+ algorithm BitrotAlgorithm
+ sum []byte
+}
+
+// BitrotAlgorithmFromString returns a bitrot algorithm from the given string representation.
+// It returns 0 if the string representation does not match any supported algorithm.
+// The zero value of a bitrot algorithm is never supported.
+func BitrotAlgorithmFromString(s string) (a BitrotAlgorithm) {
+ for alg, name := range bitrotAlgorithms {
+ if name == s {
+ return alg
+ }
+ }
+ return a
+}
+
+func newBitrotWriter(disk StorageAPI, origvolume, volume, filePath string, length int64, algo BitrotAlgorithm, shardSize int64) io.Writer {
+ if algo == HighwayHash256S {
+ return newStreamingBitrotWriter(disk, origvolume, volume, filePath, length, algo, shardSize)
+ }
+ return newWholeBitrotWriter(disk, volume, filePath, algo, shardSize)
+}
+
+func newBitrotReader(disk StorageAPI, data []byte, bucket string, filePath string, tillOffset int64, algo BitrotAlgorithm, sum []byte, shardSize int64) io.ReaderAt {
+ if algo == HighwayHash256S {
+ return newStreamingBitrotReader(disk, data, bucket, filePath, tillOffset, algo, shardSize)
+ }
+ return newWholeBitrotReader(disk, bucket, filePath, algo, tillOffset, sum)
+}
+
+// Close all the readers.
+func closeBitrotReaders(rs []io.ReaderAt) {
+ for _, r := range rs {
+ if r != nil {
+ if br, ok := r.(io.Closer); ok {
+ br.Close()
+ }
+ }
+ }
+}
+
+// Close all the writers.
+func closeBitrotWriters(ws []io.Writer) []error {
+ errs := make([]error, len(ws))
+ for i, w := range ws {
+ if w == nil {
+ 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.
+func bitrotWriterSum(w io.Writer) []byte {
+ if bw, ok := w.(*wholeBitrotWriter); ok {
+ return bw.Sum(nil)
+ }
+ return nil
+}
+
+// Returns the size of the file with bitrot protection
+func bitrotShardFileSize(size int64, shardSize int64, algo BitrotAlgorithm) int64 {
+ if algo != HighwayHash256S {
+ return size
+ }
+ return ceilFrac(size, shardSize)*int64(algo.New().Size()) + size
+}
+
+// bitrotVerify a single stream of data.
+func bitrotVerify(r io.Reader, wantSize, partSize int64, algo BitrotAlgorithm, want []byte, shardSize int64) error {
+ if algo != HighwayHash256S {
+ h := algo.New()
+ if n, err := io.Copy(h, r); err != nil || n != wantSize {
+ // Premature failure in reading the object, file is corrupt.
+ return errFileCorrupt
+ }
+ if !bytes.Equal(h.Sum(nil), want) {
+ return errFileCorrupt
+ }
+ return nil
+ }
+
+ h := algo.New()
+ hashBuf := make([]byte, h.Size())
+ left := wantSize
+
+ // Calculate the size of the bitrot file and compare
+ // it with the actual file size.
+ if left != bitrotShardFileSize(partSize, shardSize, algo) {
+ return errFileCorrupt
+ }
+
+ bufp := xioutil.ODirectPoolSmall.Get()
+ defer xioutil.ODirectPoolSmall.Put(bufp)
+
+ for left > 0 {
+ // Read expected hash...
+ h.Reset()
+ n, err := io.ReadFull(r, hashBuf)
+ if err != nil {
+ // Read's failed for object with right size, file is corrupt.
+ return err
+ }
+ // Subtract hash length..
+ left -= int64(n)
+ if left < shardSize {
+ shardSize = left
+ }
+
+ read, err := io.CopyBuffer(h, io.LimitReader(r, shardSize), *bufp)
+ if err != nil {
+ // Read's failed for object with right size, at different offsets.
+ return errFileCorrupt
+ }
+
+ left -= read
+ if !bytes.Equal(h.Sum(nil), hashBuf[:n]) {
+ return errFileCorrupt
+ }
+ }
+ return nil
+}
+
+// bitrotSelfTest performs a self-test to ensure that bitrot
+// algorithms compute correct checksums. If any algorithm
+// produces an incorrect checksum it fails with a hard error.
+//
+// bitrotSelfTest tries to catch any issue in the bitrot implementation
+// early instead of silently corrupting data.
+func bitrotSelfTest() {
+ checksums := map[BitrotAlgorithm]string{
+ SHA256: "a7677ff19e0182e4d52e3a3db727804abc82a5818749336369552e54b838b004",
+ BLAKE2b512: "e519b7d84b1c3c917985f544773a35cf265dcab10948be3550320d156bab612124a5ae2ae5a8c73c0eea360f68b0e28136f26e858756dbfe7375a7389f26c669",
+ HighwayHash256: "39c0407ed3f01b18d22c85db4aeff11e060ca5f43131b0126731ca197cd42313",
+ HighwayHash256S: "39c0407ed3f01b18d22c85db4aeff11e060ca5f43131b0126731ca197cd42313",
+ }
+ for algorithm := range bitrotAlgorithms {
+ if !algorithm.Available() {
+ continue
+ }
+
+ checksum, err := hex.DecodeString(checksums[algorithm])
+ if err != nil {
+ logger.Fatal(errSelfTestFailure, fmt.Sprintf("bitrot: failed to decode %v checksum %s for selftest: %v", algorithm, checksums[algorithm], err))
+ }
+ var (
+ hash = algorithm.New()
+ msg = make([]byte, 0, hash.Size()*hash.BlockSize())
+ sum = make([]byte, 0, hash.Size())
+ )
+ for i := 0; i < hash.Size()*hash.BlockSize(); i += hash.Size() {
+ hash.Write(msg)
+ sum = hash.Sum(sum[:0])
+ msg = append(msg, sum...)
+ hash.Reset()
+ }
+ if !bytes.Equal(sum, checksum) {
+ logger.Fatal(errSelfTestFailure, fmt.Sprintf("bitrot: %v selftest checksum mismatch: got %x - want %x", algorithm, sum, checksum))
+ }
+ }
+}
diff --git a/cmd/bitrot_test.go b/cmd/bitrot_test.go
new file mode 100644
index 00000000..636d1873
--- /dev/null
+++ b/cmd/bitrot_test.go
@@ -0,0 +1,83 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "io"
+ "testing"
+)
+
+func testBitrotReaderWriterAlgo(t *testing.T, bitrotAlgo BitrotAlgorithm) {
+ tmpDir := t.TempDir()
+
+ volume := "testvol"
+ filePath := "testfile"
+
+ disk, err := newLocalXLStorage(tmpDir)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ disk.MakeVol(t.Context(), volume)
+
+ writer := newBitrotWriter(disk, "", volume, filePath, 35, bitrotAlgo, 10)
+
+ _, err = writer.Write([]byte("aaaaaaaaaa"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, err = writer.Write([]byte("aaaaaaaaaa"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, err = writer.Write([]byte("aaaaaaaaaa"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, err = writer.Write([]byte("aaaaa"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if bw, ok := writer.(io.Closer); ok {
+ bw.Close()
+ }
+
+ reader := newBitrotReader(disk, nil, volume, filePath, 35, bitrotAlgo, bitrotWriterSum(writer), 10)
+ b := make([]byte, 10)
+ if _, err = reader.ReadAt(b, 0); err != nil {
+ t.Fatal(err)
+ }
+ if _, err = reader.ReadAt(b, 10); err != nil {
+ t.Fatal(err)
+ }
+ if _, err = reader.ReadAt(b, 20); err != nil {
+ t.Fatal(err)
+ }
+ if _, err = reader.ReadAt(b[:5], 30); err != nil {
+ t.Fatal(err)
+ }
+ if br, ok := reader.(io.Closer); ok {
+ br.Close()
+ }
+}
+
+func TestAllBitrotAlgorithms(t *testing.T) {
+ for bitrotAlgo := range bitrotAlgorithms {
+ testBitrotReaderWriterAlgo(t, bitrotAlgo)
+ }
+}
diff --git a/cmd/bootstrap-messages.go b/cmd/bootstrap-messages.go
new file mode 100644
index 00000000..c01b4529
--- /dev/null
+++ b/cmd/bootstrap-messages.go
@@ -0,0 +1,67 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "sync"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/pubsub"
+)
+
+const bootstrapTraceLimit = 4 << 10
+
+type bootstrapTracer struct {
+ mu sync.RWMutex
+ info []madmin.TraceInfo
+}
+
+var globalBootstrapTracer = &bootstrapTracer{}
+
+func (bs *bootstrapTracer) Record(info madmin.TraceInfo) {
+ bs.mu.Lock()
+ defer bs.mu.Unlock()
+
+ if len(bs.info) > bootstrapTraceLimit {
+ return
+ }
+ bs.info = append(bs.info, info)
+}
+
+func (bs *bootstrapTracer) Events() []madmin.TraceInfo {
+ traceInfo := make([]madmin.TraceInfo, 0, bootstrapTraceLimit)
+
+ bs.mu.RLock()
+ traceInfo = append(traceInfo, bs.info...)
+ bs.mu.RUnlock()
+
+ return traceInfo
+}
+
+func (bs *bootstrapTracer) Publish(ctx context.Context, trace *pubsub.PubSub[madmin.TraceInfo, madmin.TraceType]) {
+ for _, bsEvent := range bs.Events() {
+ if bsEvent.Message != "" {
+ select {
+ case <-ctx.Done():
+ default:
+ trace.Publish(bsEvent)
+ }
+ }
+ }
+}
diff --git a/cmd/bootstrap-peer-server.go b/cmd/bootstrap-peer-server.go
new file mode 100644
index 00000000..4fb179bb
--- /dev/null
+++ b/cmd/bootstrap-peer-server.go
@@ -0,0 +1,288 @@
+// Copyright (c) 2015-2022 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "crypto/md5"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "io"
+ "math/rand"
+ "os"
+ "reflect"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/minio/minio-go/v7/pkg/set"
+ "github.com/minio/minio/internal/grid"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/env"
+)
+
+// To abstract a node over network.
+type bootstrapRESTServer struct{}
+
+//go:generate msgp -file=$GOFILE
+
+// ServerSystemConfig - captures information about server configuration.
+type ServerSystemConfig struct {
+ NEndpoints int
+ CmdLines []string
+ MinioEnv map[string]string
+ Checksum string
+}
+
+// Diff - returns error on first difference found in two configs.
+func (s1 *ServerSystemConfig) Diff(s2 *ServerSystemConfig) error {
+ if s1.Checksum != s2.Checksum {
+ return fmt.Errorf("Expected MinIO binary checksum: %s, seen: %s", s1.Checksum, s2.Checksum)
+ }
+
+ ns1 := s1.NEndpoints
+ ns2 := s2.NEndpoints
+ if ns1 != ns2 {
+ return fmt.Errorf("Expected number of endpoints %d, seen %d", ns1, ns2)
+ }
+
+ for i, cmdLine := range s1.CmdLines {
+ if cmdLine != s2.CmdLines[i] {
+ return fmt.Errorf("Expected command line argument %s, seen %s", cmdLine,
+ s2.CmdLines[i])
+ }
+ }
+
+ if reflect.DeepEqual(s1.MinioEnv, s2.MinioEnv) {
+ return nil
+ }
+
+ // Report differences in environment variables.
+ var missing []string
+ var mismatching []string
+ for k, v := range s1.MinioEnv {
+ ev, ok := s2.MinioEnv[k]
+ if !ok {
+ missing = append(missing, k)
+ } else if v != ev {
+ mismatching = append(mismatching, k)
+ }
+ }
+ var extra []string
+ for k := range s2.MinioEnv {
+ _, ok := s1.MinioEnv[k]
+ if !ok {
+ extra = append(extra, k)
+ }
+ }
+ msg := "Expected MINIO_* environment name and values across all servers to be same: "
+ if len(missing) > 0 {
+ msg += fmt.Sprintf(`Missing environment values: %v. `, missing)
+ }
+ if len(mismatching) > 0 {
+ msg += fmt.Sprintf(`Mismatching environment values: %v. `, mismatching)
+ }
+ if len(extra) > 0 {
+ msg += fmt.Sprintf(`Extra environment values: %v. `, extra)
+ }
+
+ return errors.New(strings.TrimSpace(msg))
+}
+
+var skipEnvs = map[string]struct{}{
+ "MINIO_OPTS": {},
+ "MINIO_CERT_PASSWD": {},
+ "MINIO_SERVER_DEBUG": {},
+ "MINIO_DSYNC_TRACE": {},
+ "MINIO_ROOT_USER": {},
+ "MINIO_ROOT_PASSWORD": {},
+ "MINIO_ACCESS_KEY": {},
+ "MINIO_SECRET_KEY": {},
+ "MINIO_OPERATOR_VERSION": {},
+ "MINIO_VSPHERE_PLUGIN_VERSION": {},
+ "MINIO_CI_CD": {},
+}
+
+func getServerSystemCfg() *ServerSystemConfig {
+ envs := env.List("MINIO_")
+ envValues := make(map[string]string, len(envs))
+ for _, envK := range envs {
+ // skip certain environment variables as part
+ // of the whitelist and could be configured
+ // differently on each nodes, update skipEnvs()
+ // map if there are such environment values
+ if _, ok := skipEnvs[envK]; ok {
+ continue
+ }
+ envValues[envK] = logger.HashString(env.Get(envK, ""))
+ }
+ scfg := &ServerSystemConfig{NEndpoints: globalEndpoints.NEndpoints(), MinioEnv: envValues, Checksum: binaryChecksum}
+ var cmdLines []string
+ for _, ep := range globalEndpoints {
+ cmdLines = append(cmdLines, ep.CmdLine)
+ }
+ scfg.CmdLines = cmdLines
+ return scfg
+}
+
+func (s *bootstrapRESTServer) VerifyHandler(params *grid.MSS) (*ServerSystemConfig, *grid.RemoteErr) {
+ return getServerSystemCfg(), nil
+}
+
+var serverVerifyHandler = grid.NewSingleHandler[*grid.MSS, *ServerSystemConfig](grid.HandlerServerVerify, grid.NewMSS, func() *ServerSystemConfig { return &ServerSystemConfig{} })
+
+// registerBootstrapRESTHandlers - register bootstrap rest router.
+func registerBootstrapRESTHandlers(gm *grid.Manager) {
+ server := &bootstrapRESTServer{}
+ logger.FatalIf(serverVerifyHandler.Register(gm, server.VerifyHandler), "unable to register handler")
+}
+
+// client to talk to bootstrap NEndpoints.
+type bootstrapRESTClient struct {
+ gridConn *grid.Connection
+}
+
+// Verify function verifies the server config.
+func (client *bootstrapRESTClient) Verify(ctx context.Context, srcCfg *ServerSystemConfig) (err error) {
+ if newObjectLayerFn() != nil {
+ return nil
+ }
+
+ recvCfg, err := serverVerifyHandler.Call(ctx, client.gridConn, grid.NewMSS())
+ if err != nil {
+ return err
+ }
+ // We do not need the response after returning.
+ defer serverVerifyHandler.PutResponse(recvCfg)
+
+ return srcCfg.Diff(recvCfg)
+}
+
+// Stringer provides a canonicalized representation of node.
+func (client *bootstrapRESTClient) String() string {
+ return client.gridConn.String()
+}
+
+var binaryChecksum = getBinaryChecksum()
+
+func getBinaryChecksum() string {
+ mw := md5.New()
+ binPath, err := os.Executable()
+ if err != nil {
+ logger.Error("Calculating checksum failed: %s", err)
+ return "00000000000000000000000000000000"
+ }
+ b, err := os.Open(binPath)
+ if err != nil {
+ logger.Error("Calculating checksum failed: %s", err)
+ return "00000000000000000000000000000000"
+ }
+
+ defer b.Close()
+ io.Copy(mw, b)
+ return hex.EncodeToString(mw.Sum(nil))
+}
+
+func verifyServerSystemConfig(ctx context.Context, endpointServerPools EndpointServerPools, gm *grid.Manager) error {
+ srcCfg := getServerSystemCfg()
+ clnts := newBootstrapRESTClients(endpointServerPools, gm)
+ var onlineServers int
+ var offlineEndpoints []error
+ var incorrectConfigs []error
+ var retries int
+ var mu sync.Mutex
+ for onlineServers < len(clnts)/2 {
+ var wg sync.WaitGroup
+ wg.Add(len(clnts))
+ onlineServers = 0
+ for _, clnt := range clnts {
+ go func(clnt *bootstrapRESTClient) {
+ defer wg.Done()
+
+ if clnt.gridConn.State() != grid.StateConnected {
+ mu.Lock()
+ offlineEndpoints = append(offlineEndpoints, fmt.Errorf("%s is unreachable: %w", clnt, grid.ErrDisconnected))
+ mu.Unlock()
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
+ defer cancel()
+
+ err := clnt.Verify(ctx, srcCfg)
+ mu.Lock()
+ if err != nil {
+ bootstrapTraceMsg(fmt.Sprintf("bootstrapVerify: %v, endpoint: %s", err, clnt))
+ if !isNetworkError(err) {
+ bootLogOnceIf(context.Background(), fmt.Errorf("%s has incorrect configuration: %w", clnt, err), "incorrect_"+clnt.String())
+ incorrectConfigs = append(incorrectConfigs, fmt.Errorf("%s has incorrect configuration: %w", clnt, err))
+ } else {
+ offlineEndpoints = append(offlineEndpoints, fmt.Errorf("%s is unreachable: %w", clnt, err))
+ }
+ } else {
+ onlineServers++
+ }
+ mu.Unlock()
+ }(clnt)
+ }
+ wg.Wait()
+
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ default:
+ // Sleep and stagger to avoid blocked CPU and thundering
+ // herd upon start up sequence.
+ time.Sleep(25*time.Millisecond + time.Duration(rand.Int63n(int64(100*time.Millisecond))))
+ retries++
+ // after 20 retries start logging that servers are not reachable yet
+ if retries >= 20 {
+ logger.Info(fmt.Sprintf("Waiting for at least %d remote servers with valid configuration to be online", len(clnts)/2))
+ if len(offlineEndpoints) > 0 {
+ logger.Info(fmt.Sprintf("Following servers are currently offline or unreachable %s", offlineEndpoints))
+ }
+ if len(incorrectConfigs) > 0 {
+ logger.Info(fmt.Sprintf("Following servers have mismatching configuration %s", incorrectConfigs))
+ }
+ retries = 0 // reset to log again after 20 retries.
+ }
+ offlineEndpoints = nil
+ incorrectConfigs = nil
+ }
+ }
+ return nil
+}
+
+func newBootstrapRESTClients(endpointServerPools EndpointServerPools, gm *grid.Manager) []*bootstrapRESTClient {
+ seenClient := set.NewStringSet()
+ var clnts []*bootstrapRESTClient
+ for _, ep := range endpointServerPools {
+ for _, endpoint := range ep.Endpoints {
+ if endpoint.IsLocal {
+ continue
+ }
+ if seenClient.Contains(endpoint.Host) {
+ continue
+ }
+ seenClient.Add(endpoint.Host)
+ clnts = append(clnts, &bootstrapRESTClient{gm.Connection(endpoint.GridHost())})
+ }
+ }
+ return clnts
+}
diff --git a/cmd/bootstrap-peer-server_gen.go b/cmd/bootstrap-peer-server_gen.go
new file mode 100644
index 00000000..79b35bbb
--- /dev/null
+++ b/cmd/bootstrap-peer-server_gen.go
@@ -0,0 +1,292 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "github.com/tinylib/msgp/msgp"
+)
+
+// DecodeMsg implements msgp.Decodable
+func (z *ServerSystemConfig) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "NEndpoints":
+ z.NEndpoints, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "NEndpoints")
+ return
+ }
+ case "CmdLines":
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "CmdLines")
+ return
+ }
+ if cap(z.CmdLines) >= int(zb0002) {
+ z.CmdLines = (z.CmdLines)[:zb0002]
+ } else {
+ z.CmdLines = make([]string, zb0002)
+ }
+ for za0001 := range z.CmdLines {
+ z.CmdLines[za0001], err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "CmdLines", za0001)
+ return
+ }
+ }
+ case "MinioEnv":
+ var zb0003 uint32
+ zb0003, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "MinioEnv")
+ return
+ }
+ if z.MinioEnv == nil {
+ z.MinioEnv = make(map[string]string, zb0003)
+ } else if len(z.MinioEnv) > 0 {
+ clear(z.MinioEnv)
+ }
+ for zb0003 > 0 {
+ zb0003--
+ var za0002 string
+ za0002, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "MinioEnv")
+ return
+ }
+ var za0003 string
+ za0003, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "MinioEnv", za0002)
+ return
+ }
+ z.MinioEnv[za0002] = za0003
+ }
+ case "Checksum":
+ z.Checksum, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Checksum")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *ServerSystemConfig) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 4
+ // write "NEndpoints"
+ err = en.Append(0x84, 0xaa, 0x4e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.NEndpoints)
+ if err != nil {
+ err = msgp.WrapError(err, "NEndpoints")
+ return
+ }
+ // write "CmdLines"
+ err = en.Append(0xa8, 0x43, 0x6d, 0x64, 0x4c, 0x69, 0x6e, 0x65, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(len(z.CmdLines)))
+ if err != nil {
+ err = msgp.WrapError(err, "CmdLines")
+ return
+ }
+ for za0001 := range z.CmdLines {
+ err = en.WriteString(z.CmdLines[za0001])
+ if err != nil {
+ err = msgp.WrapError(err, "CmdLines", za0001)
+ return
+ }
+ }
+ // write "MinioEnv"
+ err = en.Append(0xa8, 0x4d, 0x69, 0x6e, 0x69, 0x6f, 0x45, 0x6e, 0x76)
+ if err != nil {
+ return
+ }
+ err = en.WriteMapHeader(uint32(len(z.MinioEnv)))
+ if err != nil {
+ err = msgp.WrapError(err, "MinioEnv")
+ return
+ }
+ for za0002, za0003 := range z.MinioEnv {
+ err = en.WriteString(za0002)
+ if err != nil {
+ err = msgp.WrapError(err, "MinioEnv")
+ return
+ }
+ err = en.WriteString(za0003)
+ if err != nil {
+ err = msgp.WrapError(err, "MinioEnv", za0002)
+ return
+ }
+ }
+ // write "Checksum"
+ err = en.Append(0xa8, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Checksum)
+ if err != nil {
+ err = msgp.WrapError(err, "Checksum")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *ServerSystemConfig) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 4
+ // string "NEndpoints"
+ o = append(o, 0x84, 0xaa, 0x4e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73)
+ o = msgp.AppendInt(o, z.NEndpoints)
+ // string "CmdLines"
+ o = append(o, 0xa8, 0x43, 0x6d, 0x64, 0x4c, 0x69, 0x6e, 0x65, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.CmdLines)))
+ for za0001 := range z.CmdLines {
+ o = msgp.AppendString(o, z.CmdLines[za0001])
+ }
+ // string "MinioEnv"
+ o = append(o, 0xa8, 0x4d, 0x69, 0x6e, 0x69, 0x6f, 0x45, 0x6e, 0x76)
+ o = msgp.AppendMapHeader(o, uint32(len(z.MinioEnv)))
+ for za0002, za0003 := range z.MinioEnv {
+ o = msgp.AppendString(o, za0002)
+ o = msgp.AppendString(o, za0003)
+ }
+ // string "Checksum"
+ o = append(o, 0xa8, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d)
+ o = msgp.AppendString(o, z.Checksum)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ServerSystemConfig) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "NEndpoints":
+ z.NEndpoints, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "NEndpoints")
+ return
+ }
+ case "CmdLines":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "CmdLines")
+ return
+ }
+ if cap(z.CmdLines) >= int(zb0002) {
+ z.CmdLines = (z.CmdLines)[:zb0002]
+ } else {
+ z.CmdLines = make([]string, zb0002)
+ }
+ for za0001 := range z.CmdLines {
+ z.CmdLines[za0001], bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "CmdLines", za0001)
+ return
+ }
+ }
+ case "MinioEnv":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "MinioEnv")
+ return
+ }
+ if z.MinioEnv == nil {
+ z.MinioEnv = make(map[string]string, zb0003)
+ } else if len(z.MinioEnv) > 0 {
+ clear(z.MinioEnv)
+ }
+ for zb0003 > 0 {
+ var za0003 string
+ zb0003--
+ var za0002 string
+ za0002, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "MinioEnv")
+ return
+ }
+ za0003, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "MinioEnv", za0002)
+ return
+ }
+ z.MinioEnv[za0002] = za0003
+ }
+ case "Checksum":
+ z.Checksum, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Checksum")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *ServerSystemConfig) Msgsize() (s int) {
+ s = 1 + 11 + msgp.IntSize + 9 + msgp.ArrayHeaderSize
+ for za0001 := range z.CmdLines {
+ s += msgp.StringPrefixSize + len(z.CmdLines[za0001])
+ }
+ s += 9 + msgp.MapHeaderSize
+ if z.MinioEnv != nil {
+ for za0002, za0003 := range z.MinioEnv {
+ _ = za0003
+ s += msgp.StringPrefixSize + len(za0002) + msgp.StringPrefixSize + len(za0003)
+ }
+ }
+ s += 9 + msgp.StringPrefixSize + len(z.Checksum)
+ return
+}
diff --git a/cmd/bootstrap-peer-server_gen_test.go b/cmd/bootstrap-peer-server_gen_test.go
new file mode 100644
index 00000000..efb0ee0c
--- /dev/null
+++ b/cmd/bootstrap-peer-server_gen_test.go
@@ -0,0 +1,123 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+func TestMarshalUnmarshalServerSystemConfig(t *testing.T) {
+ v := ServerSystemConfig{}
+ 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 BenchmarkMarshalMsgServerSystemConfig(b *testing.B) {
+ v := ServerSystemConfig{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgServerSystemConfig(b *testing.B) {
+ v := ServerSystemConfig{}
+ 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 BenchmarkUnmarshalServerSystemConfig(b *testing.B) {
+ v := ServerSystemConfig{}
+ 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 TestEncodeDecodeServerSystemConfig(t *testing.T) {
+ v := ServerSystemConfig{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeServerSystemConfig Msgsize() is inaccurate")
+ }
+
+ vn := ServerSystemConfig{}
+ 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 BenchmarkEncodeServerSystemConfig(b *testing.B) {
+ v := ServerSystemConfig{}
+ 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 BenchmarkDecodeServerSystemConfig(b *testing.B) {
+ v := ServerSystemConfig{}
+ 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)
+ }
+ }
+}
diff --git a/cmd/bucket-encryption-handlers.go b/cmd/bucket-encryption-handlers.go
new file mode 100644
index 00000000..1fe7631d
--- /dev/null
+++ b/cmd/bucket-encryption-handlers.go
@@ -0,0 +1,214 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "encoding/base64"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/minio/kms-go/kes"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/kms"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/mux"
+ "github.com/minio/pkg/v3/policy"
+)
+
+const (
+ // Bucket Encryption configuration file name.
+ bucketSSEConfig = "bucket-encryption.xml"
+)
+
+// PutBucketEncryptionHandler - Stores given bucket encryption configuration
+// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketEncryption.html
+func (api objectAPIHandlers) PutBucketEncryptionHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "PutBucketEncryption")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketEncryptionAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Check if bucket exists.
+ if _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Parse bucket encryption xml
+ encConfig, err := validateBucketSSEConfig(io.LimitReader(r.Body, maxBucketSSEConfigSize))
+ if err != nil {
+ apiErr := APIError{
+ Code: "MalformedXML",
+ Description: fmt.Sprintf("%s (%s)", errorCodes[ErrMalformedXML].Description, err),
+ HTTPStatusCode: errorCodes[ErrMalformedXML].HTTPStatusCode,
+ }
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+
+ // Return error if KMS is not initialized
+ if GlobalKMS == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
+ return
+ }
+ kmsKey := encConfig.KeyID()
+ if kmsKey != "" {
+ kmsContext := kms.Context{"MinIO admin API": "ServerInfoHandler"} // Context for a test key operation
+ _, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{Name: kmsKey, AssociatedData: kmsContext})
+ if err != nil {
+ if errors.Is(err, kes.ErrKeyNotFound) {
+ writeErrorResponse(ctx, w, toAPIError(ctx, errKMSKeyNotFound), r.URL)
+ return
+ }
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ }
+
+ configData, err := xml.Marshal(encConfig)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Store the bucket encryption configuration in the object layer
+ updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketSSEConfig, configData)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Call site replication hook.
+ //
+ // We encode the xml bytes as base64 to ensure there are no encoding
+ // errors.
+ cfgStr := base64.StdEncoding.EncodeToString(configData)
+ replLogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
+ Type: madmin.SRBucketMetaTypeSSEConfig,
+ Bucket: bucket,
+ SSEConfig: &cfgStr,
+ UpdatedAt: updatedAt,
+ }))
+
+ writeSuccessResponseHeadersOnly(w)
+}
+
+// GetBucketEncryptionHandler - Returns bucket policy configuration
+// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketEncryption.html
+func (api objectAPIHandlers) GetBucketEncryptionHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetBucketEncryption")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketEncryptionAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Check if bucket exists
+ var err error
+ if _, err = objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ config, _, err := globalBucketMetadataSys.GetSSEConfig(bucket)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ configData, err := xml.Marshal(config)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Write bucket encryption configuration to client
+ writeSuccessResponseXML(w, configData)
+}
+
+// DeleteBucketEncryptionHandler - Removes bucket encryption configuration
+func (api objectAPIHandlers) DeleteBucketEncryptionHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "DeleteBucketEncryption")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketEncryptionAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Check if bucket exists
+ var err error
+ if _, err = objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Delete bucket encryption config from object layer
+ updatedAt, err := globalBucketMetadataSys.Delete(ctx, bucket, bucketSSEConfig)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Call site replication hook.
+ replLogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
+ Type: madmin.SRBucketMetaTypeSSEConfig,
+ Bucket: bucket,
+ SSEConfig: nil,
+ UpdatedAt: updatedAt,
+ }))
+
+ writeSuccessNoContent(w)
+}
diff --git a/cmd/bucket-encryption.go b/cmd/bucket-encryption.go
new file mode 100644
index 00000000..c74fbae3
--- /dev/null
+++ b/cmd/bucket-encryption.go
@@ -0,0 +1,53 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "errors"
+ "io"
+
+ sse "github.com/minio/minio/internal/bucket/encryption"
+)
+
+// BucketSSEConfigSys - in-memory cache of bucket encryption config
+type BucketSSEConfigSys struct{}
+
+// NewBucketSSEConfigSys - Creates an empty in-memory bucket encryption configuration cache
+func NewBucketSSEConfigSys() *BucketSSEConfigSys {
+ return &BucketSSEConfigSys{}
+}
+
+// Get - gets bucket encryption config for the given bucket.
+func (sys *BucketSSEConfigSys) Get(bucket string) (*sse.BucketSSEConfig, error) {
+ sseCfg, _, err := globalBucketMetadataSys.GetSSEConfig(bucket)
+ return sseCfg, err
+}
+
+// validateBucketSSEConfig parses bucket encryption configuration and validates if it is supported by MinIO.
+func validateBucketSSEConfig(r io.Reader) (*sse.BucketSSEConfig, error) {
+ encConfig, err := sse.ParseBucketSSEConfig(r)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(encConfig.Rules) == 1 {
+ return encConfig, nil
+ }
+
+ return nil, errors.New("Unsupported bucket encryption configuration")
+}
diff --git a/cmd/bucket-encryption_test.go b/cmd/bucket-encryption_test.go
new file mode 100644
index 00000000..3d5ff48d
--- /dev/null
+++ b/cmd/bucket-encryption_test.go
@@ -0,0 +1,70 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "testing"
+)
+
+func TestValidateBucketSSEConfig(t *testing.T) {
+ testCases := []struct {
+ inputXML string
+ expectedErr error
+ shouldPass bool
+ }{
+ // MinIO supported XML
+ {
+ inputXML: `
+
+
+ AES256
+
+
+ `,
+ expectedErr: nil,
+ shouldPass: true,
+ },
+ // Unsupported XML
+ {
+ inputXML: `
+
+
+ aws:kms
+ my-key
+
+
+ `,
+ expectedErr: nil,
+ shouldPass: true,
+ },
+ }
+
+ for i, tc := range testCases {
+ _, err := validateBucketSSEConfig(bytes.NewReader([]byte(tc.inputXML)))
+ if tc.shouldPass && err != nil {
+ t.Fatalf("Test case %d: Expected to succeed but got %s", i+1, err)
+ }
+
+ if !tc.shouldPass {
+ if err == nil || err != nil && err.Error() != tc.expectedErr.Error() {
+ t.Fatalf("Test case %d: Expected %s but got %s", i+1, tc.expectedErr, err)
+ }
+ }
+ }
+}
diff --git a/cmd/bucket-handlers.go b/cmd/bucket-handlers.go
new file mode 100644
index 00000000..564572f2
--- /dev/null
+++ b/cmd/bucket-handlers.go
@@ -0,0 +1,2035 @@
+// Copyright (c) 2015-2022 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "crypto/md5"
+ "encoding/base64"
+ "encoding/hex"
+ "encoding/json"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "io"
+ "mime"
+ "mime/multipart"
+ "net/http"
+ "net/textproto"
+ "net/url"
+ "path"
+ "runtime"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+
+ "github.com/google/uuid"
+ "github.com/minio/mux"
+ "github.com/valyala/bytebufferpool"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio-go/v7"
+ "github.com/minio/minio-go/v7/pkg/set"
+ "github.com/minio/minio-go/v7/pkg/tags"
+ "github.com/minio/minio/internal/auth"
+ sse "github.com/minio/minio/internal/bucket/encryption"
+ objectlock "github.com/minio/minio/internal/bucket/object/lock"
+ "github.com/minio/minio/internal/bucket/replication"
+ "github.com/minio/minio/internal/config/dns"
+ "github.com/minio/minio/internal/crypto"
+ "github.com/minio/minio/internal/etag"
+ "github.com/minio/minio/internal/event"
+ "github.com/minio/minio/internal/handlers"
+ "github.com/minio/minio/internal/hash"
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/minio/internal/kms"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/policy"
+ "github.com/minio/pkg/v3/sync/errgroup"
+)
+
+const (
+ objectLockConfig = "object-lock.xml"
+ bucketTaggingConfig = "tagging.xml"
+ bucketReplicationConfig = "replication.xml"
+
+ xMinIOErrCodeHeader = "x-minio-error-code"
+ xMinIOErrDescHeader = "x-minio-error-desc"
+
+ postPolicyBucketTagging = "tagging"
+)
+
+// Check if there are buckets on server without corresponding entry in etcd backend and
+// make entries. Here is the general flow
+// - Range over all the available buckets
+// - Check if a bucket has an entry in etcd backend
+// -- If no, make an entry
+// -- If yes, check if the entry matches local IP check if we
+//
+// need to update the entry then proceed to update
+//
+// -- If yes, check if the IP of entry matches local IP.
+//
+// This means entry is for this instance.
+//
+// -- If IP of the entry doesn't match, this means entry is
+//
+// for another instance. Log an error to console.
+func initFederatorBackend(buckets []string, objLayer ObjectLayer) {
+ if len(buckets) == 0 {
+ return
+ }
+
+ // Get buckets in the DNS
+ dnsBuckets, err := globalDNSConfig.List()
+ if err != nil && !IsErrIgnored(err, dns.ErrNoEntriesFound, dns.ErrNotImplemented, dns.ErrDomainMissing) {
+ dnsLogIf(GlobalContext, err)
+ return
+ }
+
+ bucketsSet := set.NewStringSet()
+ bucketsToBeUpdated := set.NewStringSet()
+ bucketsInConflict := set.NewStringSet()
+
+ // This means that domain is updated, we should update
+ // all bucket entries with new domain name.
+ domainMissing := err == dns.ErrDomainMissing
+ if dnsBuckets != nil {
+ for _, bucket := range buckets {
+ bucketsSet.Add(bucket)
+ r, ok := dnsBuckets[bucket]
+ if !ok {
+ bucketsToBeUpdated.Add(bucket)
+ continue
+ }
+ if !globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(r)...)).IsEmpty() {
+ if globalDomainIPs.Difference(set.CreateStringSet(getHostsSlice(r)...)).IsEmpty() && !domainMissing {
+ // No difference in terms of domainIPs and nothing
+ // has changed so we don't change anything on the etcd.
+ //
+ // Additionally also check if domain is updated/missing with more
+ // entries, if that is the case we should update the
+ // new domain entries as well.
+ continue
+ }
+
+ // if domain IPs intersect then it won't be an empty set.
+ // such an intersection means that bucket exists on etcd.
+ // but if we do see a difference with local domain IPs with
+ // hostSlice from etcd then we should update with newer
+ // domainIPs, we proceed to do that here.
+ bucketsToBeUpdated.Add(bucket)
+ continue
+ }
+
+ // No IPs seem to intersect, this means that bucket exists but has
+ // different IP addresses perhaps from a different deployment.
+ // bucket names are globally unique in federation at a given
+ // path prefix, name collision is not allowed. We simply log
+ // an error and continue.
+ bucketsInConflict.Add(bucket)
+ }
+ }
+
+ // Add/update buckets that are not registered with the DNS
+ bucketsToBeUpdatedSlice := bucketsToBeUpdated.ToSlice()
+ g := errgroup.WithNErrs(len(bucketsToBeUpdatedSlice)).WithConcurrency(50)
+
+ for index := range bucketsToBeUpdatedSlice {
+ g.Go(func() error {
+ return globalDNSConfig.Put(bucketsToBeUpdatedSlice[index])
+ }, index)
+ }
+
+ ctx := GlobalContext
+ for _, err := range g.Wait() {
+ if err != nil {
+ dnsLogIf(ctx, err)
+ return
+ }
+ }
+
+ for _, bucket := range bucketsInConflict.ToSlice() {
+ dnsLogIf(ctx, fmt.Errorf("Unable to add bucket DNS entry for bucket %s, an entry exists for the same bucket by a different tenant. This local bucket will be ignored. Bucket names are globally unique in federated deployments. Use path style requests on following addresses '%v' to access this bucket", bucket, globalDomainIPs.ToSlice()))
+ }
+
+ var wg sync.WaitGroup
+ // Remove buckets that are in DNS for this server, but aren't local
+ for bucket, records := range dnsBuckets {
+ if bucketsSet.Contains(bucket) {
+ continue
+ }
+
+ if globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(records)...)).IsEmpty() {
+ // This is not for our server, so we can continue
+ continue
+ }
+
+ wg.Add(1)
+ go func(bucket string) {
+ defer wg.Done()
+ // We go to here, so we know the bucket no longer exists,
+ // but is registered in DNS to this server
+ if err := globalDNSConfig.Delete(bucket); err != nil {
+ dnsLogIf(GlobalContext, fmt.Errorf("Failed to remove DNS entry for %s due to %w",
+ bucket, err))
+ }
+ }(bucket)
+ }
+ wg.Wait()
+}
+
+// GetBucketLocationHandler - GET Bucket location.
+// -------------------------
+// This operation returns bucket location.
+func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetBucketLocation")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketLocationAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ getBucketInfo := objectAPI.GetBucketInfo
+
+ if _, err := getBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Generate response.
+ encodedSuccessResponse := encodeResponse(LocationResponse{})
+ // Get current region.
+ region := globalSite.Region()
+ if region != globalMinioDefaultRegion {
+ encodedSuccessResponse = encodeResponse(LocationResponse{
+ Location: region,
+ })
+ }
+
+ // Write success response.
+ writeSuccessResponseXML(w, encodedSuccessResponse)
+}
+
+// ListMultipartUploadsHandler - GET Bucket (List Multipart uploads)
+// -------------------------
+// This operation lists in-progress multipart uploads. An in-progress
+// multipart upload is a multipart upload that has been initiated,
+// using the Initiate Multipart Upload request, but has not yet been
+// completed or aborted. This operation returns at most 1,000 multipart
+// uploads in the response.
+func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "ListMultipartUploads")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.ListBucketMultipartUploadsAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ prefix, keyMarker, uploadIDMarker, delimiter, maxUploads, encodingType, errCode := getBucketMultipartResources(r.Form)
+ if errCode != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(errCode), r.URL)
+ return
+ }
+
+ if maxUploads < 0 {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidMaxUploads), r.URL)
+ return
+ }
+
+ if keyMarker != "" {
+ // Marker not common with prefix is not implemented.
+ if !HasPrefix(keyMarker, prefix) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+ return
+ }
+ }
+
+ listMultipartsInfo, err := objectAPI.ListMultipartUploads(ctx, bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ // generate response
+ response := generateListMultipartUploadsResponse(bucket, listMultipartsInfo, encodingType)
+ encodedSuccessResponse := encodeResponse(response)
+
+ // write success response.
+ writeSuccessResponseXML(w, encodedSuccessResponse)
+}
+
+// ListBucketsHandler - GET Service.
+// -----------
+// This implementation of the GET operation returns a list of all buckets
+// owned by the authenticated sender of the request.
+func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "ListBuckets")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ listBuckets := objectAPI.ListBuckets
+
+ cred, owner, s3Error := checkRequestAuthTypeCredential(ctx, r, policy.ListAllMyBucketsAction)
+ if s3Error != ErrNone && s3Error != ErrAccessDenied {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Anonymous users, should be rejected.
+ if cred.AccessKey == "" {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+
+ // If etcd, dns federation configured list buckets from etcd.
+ var bucketsInfo []BucketInfo
+ if globalDNSConfig != nil && globalBucketFederation {
+ dnsBuckets, err := globalDNSConfig.List()
+ if err != nil && !IsErrIgnored(err,
+ dns.ErrNoEntriesFound,
+ dns.ErrDomainMissing) {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ for _, dnsRecords := range dnsBuckets {
+ bucketsInfo = append(bucketsInfo, BucketInfo{
+ Name: dnsRecords[0].Key,
+ Created: dnsRecords[0].CreationDate,
+ })
+ }
+ sort.Slice(bucketsInfo, func(i, j int) bool {
+ return bucketsInfo[i].Name < bucketsInfo[j].Name
+ })
+ } else {
+ // Invoke the list buckets.
+ var err error
+ bucketsInfo, err = listBuckets(ctx, BucketOptions{})
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ }
+
+ if s3Error == ErrAccessDenied {
+ // Set prefix value for "s3:prefix" policy conditionals.
+ r.Header.Set("prefix", "")
+
+ // Set delimiter value for "s3:delimiter" policy conditionals.
+ r.Header.Set("delimiter", SlashSeparator)
+
+ n := 0
+ // Use the following trick to filter in place
+ // https://github.com/golang/go/wiki/SliceTricks#filter-in-place
+ for _, bucketInfo := range bucketsInfo {
+ if globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.ListBucketAction,
+ BucketName: bucketInfo.Name,
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ ObjectName: "",
+ Claims: cred.Claims,
+ }) {
+ bucketsInfo[n] = bucketInfo
+ n++
+ } else if globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.GetBucketLocationAction,
+ BucketName: bucketInfo.Name,
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ ObjectName: "",
+ Claims: cred.Claims,
+ }) {
+ bucketsInfo[n] = bucketInfo
+ n++
+ }
+ }
+ bucketsInfo = bucketsInfo[:n]
+ // No buckets can be filtered return access denied error.
+ if len(bucketsInfo) == 0 {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+ }
+
+ // Generate response.
+ response := generateListBucketsResponse(bucketsInfo)
+ encodedSuccessResponse := encodeResponse(response)
+
+ // Write response.
+ writeSuccessResponseXML(w, encodedSuccessResponse)
+}
+
+// DeleteMultipleObjectsHandler - deletes multiple objects.
+func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "DeleteMultipleObjects")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ // Content-Md5 is required should be set
+ // http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
+ if !validateLengthAndChecksum(r) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL)
+ return
+ }
+
+ // Content-Length is required and should be non-zero
+ // http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
+ if r.ContentLength <= 0 {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL)
+ return
+ }
+
+ // The max. XML contains 100000 object names (each at most 1024 bytes long) + XML overhead
+ const maxBodySize = 2 * 100000 * 1024
+
+ if r.ContentLength > maxBodySize {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrEntityTooLarge), r.URL)
+ return
+ }
+
+ // Unmarshal list of keys to be deleted.
+ deleteObjectsReq := &DeleteObjectsRequest{}
+ if err := xmlDecoder(r.Body, deleteObjectsReq, maxBodySize); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ objects := make([]ObjectV, len(deleteObjectsReq.Objects))
+ // Convert object name delete objects if it has `/` in the beginning.
+ for i := range deleteObjectsReq.Objects {
+ deleteObjectsReq.Objects[i].ObjectName = trimLeadingSlash(deleteObjectsReq.Objects[i].ObjectName)
+ objects[i] = deleteObjectsReq.Objects[i].ObjectV
+ }
+
+ // Make sure to update context to print ObjectNames for multi objects.
+ ctx = updateReqContext(ctx, objects...)
+
+ // Call checkRequestAuthType to populate ReqInfo.AccessKey before GetBucketInfo()
+ // Ignore errors here to preserve the S3 error behavior of GetBucketInfo()
+ checkRequestAuthType(ctx, r, policy.DeleteObjectAction, bucket, "")
+
+ deleteObjectsFn := objectAPI.DeleteObjects
+
+ // Return Malformed XML as S3 spec if the number of objects is empty
+ if len(deleteObjectsReq.Objects) == 0 || len(deleteObjectsReq.Objects) > maxDeleteList {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedXML), r.URL)
+ return
+ }
+
+ objectsToDelete := map[ObjectToDelete]int{}
+ getObjectInfoFn := objectAPI.GetObjectInfo
+
+ var (
+ hasLockEnabled bool
+ dsc ReplicateDecision
+ goi ObjectInfo
+ gerr error
+ )
+ replicateDeletes := hasReplicationRules(ctx, bucket, deleteObjectsReq.Objects)
+ if rcfg, _ := globalBucketObjectLockSys.Get(bucket); rcfg.LockEnabled {
+ hasLockEnabled = true
+ }
+
+ type deleteResult struct {
+ delInfo DeletedObject
+ errInfo DeleteError
+ }
+
+ deleteResults := make([]deleteResult, len(deleteObjectsReq.Objects))
+
+ vc, _ := globalBucketVersioningSys.Get(bucket)
+ oss := make([]*objSweeper, len(deleteObjectsReq.Objects))
+ for index, object := range deleteObjectsReq.Objects {
+ if apiErrCode := checkRequestAuthTypeWithVID(ctx, r, policy.DeleteObjectAction, bucket, object.ObjectName, object.VersionID); apiErrCode != ErrNone {
+ if apiErrCode == ErrSignatureDoesNotMatch || apiErrCode == ErrInvalidAccessKeyID {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(apiErrCode), r.URL)
+ return
+ }
+ apiErr := errorCodes.ToAPIErr(apiErrCode)
+ deleteResults[index].errInfo = DeleteError{
+ Code: apiErr.Code,
+ Message: apiErr.Description,
+ Key: object.ObjectName,
+ VersionID: object.VersionID,
+ }
+ continue
+ }
+ if object.VersionID != "" && object.VersionID != nullVersionID {
+ if _, err := uuid.Parse(object.VersionID); err != nil {
+ apiErr := errorCodes.ToAPIErr(ErrNoSuchVersion)
+ deleteResults[index].errInfo = DeleteError{
+ Code: apiErr.Code,
+ Message: fmt.Sprintf("%s (%s)", apiErr.Description, err),
+ Key: object.ObjectName,
+ VersionID: object.VersionID,
+ }
+ continue
+ }
+ }
+
+ opts := ObjectOptions{
+ VersionID: object.VersionID,
+ Versioned: vc.PrefixEnabled(object.ObjectName),
+ VersionSuspended: vc.Suspended(),
+ }
+
+ if replicateDeletes || object.VersionID != "" && hasLockEnabled || !globalTierConfigMgr.Empty() {
+ if !globalTierConfigMgr.Empty() && object.VersionID == "" && opts.VersionSuspended {
+ opts.VersionID = nullVersionID
+ }
+ goi, gerr = getObjectInfoFn(ctx, bucket, object.ObjectName, opts)
+ }
+
+ if !globalTierConfigMgr.Empty() {
+ oss[index] = newObjSweeper(bucket, object.ObjectName).WithVersion(opts.VersionID).WithVersioning(opts.Versioned, opts.VersionSuspended)
+ oss[index].SetTransitionState(goi.TransitionedObject)
+ }
+
+ // All deletes on directory objects needs to be for `nullVersionID`
+ if isDirObject(object.ObjectName) && object.VersionID == "" {
+ object.VersionID = nullVersionID
+ }
+
+ if replicateDeletes {
+ dsc = checkReplicateDelete(ctx, bucket, ObjectToDelete{
+ ObjectV: ObjectV{
+ ObjectName: object.ObjectName,
+ VersionID: object.VersionID,
+ },
+ }, goi, opts, gerr)
+ if dsc.ReplicateAny() {
+ if object.VersionID != "" {
+ object.VersionPurgeStatus = replication.VersionPurgePending
+ object.VersionPurgeStatuses = dsc.PendingStatus()
+ } else {
+ object.DeleteMarkerReplicationStatus = dsc.PendingStatus()
+ }
+ object.ReplicateDecisionStr = dsc.String()
+ }
+ }
+ if object.VersionID != "" && hasLockEnabled {
+ if err := enforceRetentionBypassForDelete(ctx, r, bucket, object, goi, gerr); err != nil {
+ apiErr := toAPIError(ctx, err)
+ deleteResults[index].errInfo = DeleteError{
+ Code: apiErr.Code,
+ Message: apiErr.Description,
+ Key: object.ObjectName,
+ VersionID: object.VersionID,
+ }
+ continue
+ }
+ }
+
+ // Avoid duplicate objects, we use map to filter them out.
+ if _, ok := objectsToDelete[object]; !ok {
+ objectsToDelete[object] = index
+ }
+ }
+
+ toNames := func(input map[ObjectToDelete]int) (output []ObjectToDelete) {
+ output = make([]ObjectToDelete, len(input))
+ idx := 0
+ for obj := range input {
+ output[idx] = obj
+ idx++
+ }
+ return output
+ }
+
+ // Disable timeouts and cancellation
+ ctx = bgContext(ctx)
+
+ deleteList := toNames(objectsToDelete)
+ dObjects, errs := deleteObjectsFn(ctx, bucket, deleteList, ObjectOptions{
+ PrefixEnabledFn: vc.PrefixEnabled,
+ VersionSuspended: vc.Suspended(),
+ })
+
+ // Are all objects saying bucket not found?
+ if isAllBucketsNotFound(errs) {
+ writeErrorResponse(ctx, w, toAPIError(ctx, errs[0]), r.URL)
+ return
+ }
+
+ for i := range errs {
+ // DeleteMarkerVersionID is not used specifically to avoid
+ // lookup errors, since DeleteMarkerVersionID is only
+ // created during DeleteMarker creation when client didn't
+ // specify a versionID.
+ objToDel := ObjectToDelete{
+ ObjectV: ObjectV{
+ ObjectName: decodeDirObject(dObjects[i].ObjectName),
+ VersionID: dObjects[i].VersionID,
+ },
+ VersionPurgeStatus: dObjects[i].VersionPurgeStatus(),
+ VersionPurgeStatuses: dObjects[i].ReplicationState.VersionPurgeStatusInternal,
+ DeleteMarkerReplicationStatus: dObjects[i].ReplicationState.ReplicationStatusInternal,
+ ReplicateDecisionStr: dObjects[i].ReplicationState.ReplicateDecisionStr,
+ }
+ dindex := objectsToDelete[objToDel]
+ if errs[i] == nil || isErrObjectNotFound(errs[i]) || isErrVersionNotFound(errs[i]) {
+ if replicateDeletes {
+ dObjects[i].ReplicationState = deleteList[i].ReplicationState()
+ }
+ deleteResults[dindex].delInfo = dObjects[i]
+ continue
+ }
+ apiErr := toAPIError(ctx, errs[i])
+ deleteResults[dindex].errInfo = DeleteError{
+ Code: apiErr.Code,
+ Message: apiErr.Description,
+ Key: deleteList[i].ObjectName,
+ VersionID: deleteList[i].VersionID,
+ }
+ }
+
+ // Generate response
+ deleteErrors := make([]DeleteError, 0, len(deleteObjectsReq.Objects))
+ deletedObjects := make([]DeletedObject, 0, len(deleteObjectsReq.Objects))
+ for _, deleteResult := range deleteResults {
+ if deleteResult.errInfo.Code != "" {
+ deleteErrors = append(deleteErrors, deleteResult.errInfo)
+ } else {
+ // All deletes on directory objects was with `nullVersionID`.
+ // Remove it from response.
+ if isDirObject(deleteResult.delInfo.ObjectName) && deleteResult.delInfo.VersionID == nullVersionID {
+ deleteResult.delInfo.VersionID = ""
+ }
+ deletedObjects = append(deletedObjects, deleteResult.delInfo)
+ }
+ }
+
+ response := generateMultiDeleteResponse(deleteObjectsReq.Quiet, deletedObjects, deleteErrors)
+ encodedSuccessResponse := encodeResponse(response)
+
+ // Write success response.
+ writeSuccessResponseXML(w, encodedSuccessResponse)
+ for _, dobj := range deletedObjects {
+ if dobj.ObjectName == "" {
+ continue
+ }
+
+ if replicateDeletes && (dobj.DeleteMarkerReplicationStatus() == replication.Pending || dobj.VersionPurgeStatus() == replication.VersionPurgePending) {
+ // copy so we can re-add null ID.
+ dobj := dobj
+ if isDirObject(dobj.ObjectName) && dobj.VersionID == "" {
+ dobj.VersionID = nullVersionID
+ }
+ dv := DeletedObjectReplicationInfo{
+ DeletedObject: dobj,
+ Bucket: bucket,
+ EventType: ReplicateIncomingDelete,
+ }
+ scheduleReplicationDelete(ctx, dv, objectAPI)
+ }
+
+ eventName := event.ObjectRemovedDelete
+ objInfo := ObjectInfo{
+ Name: dobj.ObjectName,
+ VersionID: dobj.VersionID,
+ DeleteMarker: dobj.DeleteMarker,
+ }
+
+ if objInfo.DeleteMarker {
+ objInfo.VersionID = dobj.DeleteMarkerVersionID
+ eventName = event.ObjectRemovedDeleteMarkerCreated
+ }
+
+ sendEvent(eventArgs{
+ EventName: eventName,
+ BucketName: bucket,
+ Object: objInfo,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+ }
+
+ // Clean up transitioned objects from remote tier
+ for _, os := range oss {
+ if os == nil { // skip objects that weren't deleted due to invalid versionID etc.
+ continue
+ }
+ os.Sweep()
+ }
+}
+
+// PutBucketHandler - PUT Bucket
+// ----------
+// This implementation of the PUT operation creates a new bucket for authenticated request
+func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "PutBucket")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectLockEnabled := false
+ if vs := r.Header.Get(xhttp.AmzObjectLockEnabled); len(vs) > 0 {
+ v := strings.ToLower(vs)
+ switch v {
+ case "true", "false":
+ objectLockEnabled = v == "true"
+ default:
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
+ return
+ }
+ }
+
+ forceCreate := false
+ if vs := r.Header.Get(xhttp.MinIOForceCreate); len(vs) > 0 {
+ v := strings.ToLower(vs)
+ switch v {
+ case "true", "false":
+ forceCreate = v == "true"
+ default:
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
+ return
+ }
+ }
+
+ cred, owner, s3Error := checkRequestAuthTypeCredential(ctx, r, policy.CreateBucketAction)
+ if s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ if objectLockEnabled {
+ // Creating a bucket with locking requires the user having more permissions
+ for _, action := range []policy.Action{policy.PutBucketObjectLockConfigurationAction, policy.PutBucketVersioningAction} {
+ if !globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: action,
+ ConditionValues: getConditionValues(r, "", cred),
+ BucketName: bucket,
+ IsOwner: owner,
+ Claims: cred.Claims,
+ }) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+ }
+ }
+
+ // Parse incoming location constraint.
+ _, s3Error = parseLocationConstraint(r)
+ if s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // check if client is attempting to create more buckets, complain about it.
+ if currBuckets := globalBucketMetadataSys.Count(); currBuckets+1 > maxBuckets {
+ internalLogIf(ctx, fmt.Errorf("Please avoid creating more buckets %d beyond recommended %d", currBuckets+1, maxBuckets), logger.WarningKind)
+ }
+
+ opts := MakeBucketOptions{
+ LockEnabled: objectLockEnabled,
+ ForceCreate: forceCreate,
+ }
+
+ if globalDNSConfig != nil {
+ sr, err := globalDNSConfig.Get(bucket)
+ if err != nil {
+ // ErrNotImplemented indicates a DNS backend that doesn't need to check if bucket already
+ // exists elsewhere
+ if err == dns.ErrNoEntriesFound || err == dns.ErrNotImplemented {
+ // Proceed to creating a bucket.
+ if err = objectAPI.MakeBucket(ctx, bucket, opts); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if err = globalDNSConfig.Put(bucket); err != nil {
+ objectAPI.DeleteBucket(context.Background(), bucket, DeleteBucketOptions{
+ Force: true,
+ SRDeleteOp: getSRBucketDeleteOp(globalSiteReplicationSys.isEnabled()),
+ })
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Load updated bucket metadata into memory.
+ globalNotificationSys.LoadBucketMetadata(GlobalContext, bucket)
+
+ // Make sure to add Location information here only for bucket
+ w.Header().Set(xhttp.Location, pathJoin(SlashSeparator, bucket))
+
+ writeSuccessResponseHeadersOnly(w)
+
+ sendEvent(eventArgs{
+ EventName: event.BucketCreated,
+ BucketName: bucket,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+
+ return
+ }
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ apiErr := ErrBucketAlreadyExists
+ if !globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(sr)...)).IsEmpty() {
+ apiErr = ErrBucketAlreadyOwnedByYou
+ }
+ // No IPs seem to intersect, this means that bucket exists but has
+ // different IP addresses perhaps from a different deployment.
+ // bucket names are globally unique in federation at a given
+ // path prefix, name collision is not allowed. Return appropriate error.
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(apiErr), r.URL)
+ return
+ }
+
+ // Proceed to creating a bucket.
+ if err := objectAPI.MakeBucket(ctx, bucket, opts); err != nil {
+ if _, ok := err.(BucketExists); ok {
+ // Though bucket exists locally, we send the site-replication
+ // hook to ensure all sites have this bucket. If the hook
+ // succeeds, the client will still receive a bucket exists
+ // message.
+ globalSiteReplicationSys.MakeBucketHook(ctx, bucket, opts)
+ }
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Load updated bucket metadata into memory.
+ globalNotificationSys.LoadBucketMetadata(GlobalContext, bucket)
+
+ // Call site replication hook
+ replLogIf(ctx, globalSiteReplicationSys.MakeBucketHook(ctx, bucket, opts))
+
+ // Make sure to add Location information here only for bucket
+ w.Header().Set(xhttp.Location, pathJoin(SlashSeparator, bucket))
+
+ writeSuccessResponseHeadersOnly(w)
+
+ sendEvent(eventArgs{
+ EventName: event.BucketCreated,
+ BucketName: bucket,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+}
+
+// multipartReader is just like https://pkg.go.dev/net/http#Request.MultipartReader but
+// rejects multipart/mixed as its not supported in S3 API.
+func multipartReader(r *http.Request) (*multipart.Reader, error) {
+ v := r.Header.Get("Content-Type")
+ if v == "" {
+ return nil, http.ErrNotMultipart
+ }
+ if r.Body == nil {
+ return nil, errors.New("missing form body")
+ }
+ d, params, err := mime.ParseMediaType(v)
+ if err != nil {
+ return nil, http.ErrNotMultipart
+ }
+ if d != "multipart/form-data" {
+ return nil, http.ErrNotMultipart
+ }
+ boundary, ok := params["boundary"]
+ if !ok {
+ return nil, http.ErrMissingBoundary
+ }
+ return multipart.NewReader(r.Body, boundary), nil
+}
+
+// PostPolicyBucketHandler - POST policy
+// ----------
+// This implementation of the POST operation handles object creation with a specified
+// signature policy in multipart/form-data
+func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "PostPolicyBucket")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ if crypto.S3KMS.IsRequested(r.Header) { // SSE-KMS is not supported
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+ return
+ }
+
+ bucket := mux.Vars(r)["bucket"]
+ resource, err := getResource(r.URL.Path, r.Host, globalDomainNames)
+ if err != nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
+ return
+ }
+
+ // Make sure that the URL does not contain object name.
+ if bucket != path.Clean(resource[1:]) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
+ return
+ }
+
+ if r.ContentLength <= 0 {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrEmptyRequestBody), r.URL)
+ return
+ }
+
+ // Here the parameter is the size of the form data that should
+ // be loaded in memory, the remaining being put in temporary files.
+ mp, err := multipartReader(r)
+ if err != nil {
+ apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
+ apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, err)
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+
+ const mapEntryOverhead = 200
+
+ var (
+ reader io.Reader
+ actualSize int64 = -1
+ fileName string
+ fanOutEntries = make([]minio.PutObjectFanOutEntry, 0, 100)
+ )
+
+ maxParts := 1000
+ // Canonicalize the form values into http.Header.
+ formValues := make(http.Header)
+ var headerLen int64
+ for {
+ part, err := mp.NextRawPart()
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ if err != nil {
+ apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
+ apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, err)
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+ if maxParts <= 0 {
+ apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
+ apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, multipart.ErrMessageTooLarge)
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+ maxParts--
+
+ name := part.FormName()
+ if name == "" {
+ continue
+ }
+
+ fileName = part.FileName()
+
+ // Multiple values for the same key (one map entry, longer slice) are cheaper
+ // than the same number of values for different keys (many map entries), but
+ // using a consistent per-value cost for overhead is simpler.
+ maxMemoryBytes := 2 * int64(10<<20)
+ maxMemoryBytes -= int64(len(name))
+ maxMemoryBytes -= mapEntryOverhead
+ if maxMemoryBytes < 0 {
+ // We can't actually take this path, since nextPart would already have
+ // rejected the MIME headers for being too large. Check anyway.
+ apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
+ apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, multipart.ErrMessageTooLarge)
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+
+ headerLen += int64(len(name)) + int64(len(fileName))
+ if name != "file" {
+ if http.CanonicalHeaderKey(name) == http.CanonicalHeaderKey("x-minio-fanout-list") {
+ dec := json.NewDecoder(part)
+
+ // while the array contains values
+ for dec.More() {
+ var m minio.PutObjectFanOutEntry
+ if err := dec.Decode(&m); err != nil {
+ part.Close()
+ apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
+ apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, err)
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+ fanOutEntries = append(fanOutEntries, m)
+ }
+ part.Close()
+ continue
+ }
+
+ buf := bytebufferpool.Get()
+ // value, store as string in memory
+ n, err := io.CopyN(buf, part, maxMemoryBytes+1)
+ value := buf.String()
+ buf.Reset()
+ bytebufferpool.Put(buf)
+ part.Close()
+
+ if err != nil && err != io.EOF {
+ apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
+ apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, err)
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+ maxMemoryBytes -= n
+ if maxMemoryBytes < 0 {
+ apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
+ apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, multipart.ErrMessageTooLarge)
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+ if n > maxFormFieldSize {
+ apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
+ apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, multipart.ErrMessageTooLarge)
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+ headerLen += n
+ formValues[http.CanonicalHeaderKey(name)] = append(formValues[http.CanonicalHeaderKey(name)], value)
+ continue
+ }
+
+ // In accordance with https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPOST.html
+ // The file or text content.
+ // The file or text content must be the last field in the form.
+ // You cannot upload more than one file at a time.
+ reader = part
+
+ possibleShardSize := (r.ContentLength - headerLen)
+ if globalStorageClass.ShouldInline(possibleShardSize, false) { // keep versioned false for this check
+ var b bytes.Buffer
+ n, err := io.Copy(&b, reader)
+ if err != nil {
+ apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
+ apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, err)
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+ reader = &b
+ actualSize = n
+ }
+
+ // we have found the File part of the request we are done processing multipart-form
+ break
+ }
+
+ // check if have a file
+ if reader == nil {
+ apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
+ apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, errors.New("The file or text content is missing"))
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+
+ if keyName, ok := formValues["Key"]; !ok {
+ apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
+ apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, errors.New("The name of the uploaded key is missing"))
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ } else if fileName == "" && len(keyName) >= 1 {
+ // if we can't get fileName. We use keyName[0] to fileName
+ fileName = keyName[0]
+ }
+
+ if fileName == "" {
+ apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
+ apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, errors.New("The file or text content is missing"))
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+ checksum, err := hash.GetContentChecksum(formValues)
+ if err != nil {
+ apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
+ apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, fmt.Errorf("Invalid checksum: %w", err))
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+ if checksum != nil && checksum.Type.Trailing() {
+ // Not officially supported in POST requests.
+ apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
+ apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, errors.New("Trailing checksums not available for POST operations"))
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+
+ formValues.Set("Bucket", bucket)
+ if fileName != "" && strings.Contains(formValues.Get("Key"), "${filename}") {
+ // S3 feature to replace ${filename} found in Key form field
+ // by the filename attribute passed in multipart
+ formValues.Set("Key", strings.ReplaceAll(formValues.Get("Key"), "${filename}", fileName))
+ }
+ object := trimLeadingSlash(formValues.Get("Key"))
+
+ successRedirect := formValues.Get("success_action_redirect")
+ successStatus := formValues.Get("success_action_status")
+ var redirectURL *url.URL
+ if successRedirect != "" {
+ redirectURL, err = url.Parse(successRedirect)
+ if err != nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedPOSTRequest), r.URL)
+ return
+ }
+ }
+
+ // Verify policy signature.
+ cred, errCode := doesPolicySignatureMatch(formValues)
+ if errCode != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(errCode), r.URL)
+ return
+ }
+
+ if len(fanOutEntries) > 0 {
+ // Once signature is validated, check if the user has
+ // explicit permissions for the user.
+ if !globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.PutObjectFanOutAction,
+ ConditionValues: getConditionValues(r, "", cred),
+ BucketName: bucket,
+ ObjectName: object,
+ IsOwner: globalActiveCred.AccessKey == cred.AccessKey,
+ Claims: cred.Claims,
+ }) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+ } else {
+ // Once signature is validated, check if the user has
+ // explicit permissions for the user.
+ if !globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.PutObjectAction,
+ ConditionValues: getConditionValues(r, "", cred),
+ BucketName: bucket,
+ ObjectName: object,
+ IsOwner: globalActiveCred.AccessKey == cred.AccessKey,
+ Claims: cred.Claims,
+ }) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+ }
+
+ policyBytes, err := base64.StdEncoding.DecodeString(formValues.Get("Policy"))
+ if err != nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedPOSTRequest), r.URL)
+ return
+ }
+
+ clientETag, err := etag.FromContentMD5(formValues)
+ if err != nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidDigest), r.URL)
+ return
+ }
+
+ var forceMD5 []byte
+ // Optimization: If SSE-KMS and SSE-C did not request Content-Md5. Use uuid as etag. Optionally enable this also
+ // for server that is started with `--no-compat`.
+ kind, _ := crypto.IsRequested(formValues)
+ if !etag.ContentMD5Requested(formValues) && (kind == crypto.SSEC || kind == crypto.S3KMS || !globalServerCtxt.StrictS3Compat) {
+ forceMD5 = mustGetUUIDBytes()
+ }
+
+ hashReader, err := hash.NewReaderWithOpts(ctx, reader, hash.Options{
+ Size: actualSize,
+ MD5Hex: clientETag.String(),
+ SHA256Hex: "",
+ ActualSize: actualSize,
+ DisableMD5: false,
+ ForceMD5: forceMD5,
+ })
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if checksum != nil && checksum.Valid() {
+ if err = hashReader.AddChecksumNoTrailer(formValues, false); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ }
+
+ // Handle policy if it is set.
+ if len(policyBytes) > 0 {
+ postPolicyForm, err := parsePostPolicyForm(bytes.NewReader(policyBytes))
+ if err != nil {
+ errAPI := errorCodes.ToAPIErr(ErrPostPolicyConditionInvalidFormat)
+ errAPI.Description = fmt.Sprintf("%s '(%s)'", errAPI.Description, err)
+ writeErrorResponse(ctx, w, errAPI, r.URL)
+ return
+ }
+
+ // Make sure formValues adhere to policy restrictions.
+ if err = checkPostPolicy(formValues, postPolicyForm); err != nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrAccessDenied, err), r.URL)
+ return
+ }
+
+ // Ensure that the object size is within expected range, also the file size
+ // should not exceed the maximum single Put size (5 GiB)
+ lengthRange := postPolicyForm.Conditions.ContentLengthRange
+ if lengthRange.Valid {
+ hashReader.SetExpectedMin(lengthRange.Min)
+ hashReader.SetExpectedMax(lengthRange.Max)
+ }
+ }
+
+ // Extract metadata to be saved from received Form.
+ metadata := make(map[string]string)
+ err = extractMetadataFromMime(ctx, textproto.MIMEHeader(formValues), metadata)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ rawReader := hashReader
+ pReader := NewPutObjReader(rawReader)
+ var objectEncryptionKey crypto.ObjectKey
+
+ // Check if bucket encryption is enabled
+ sseConfig, _ := globalBucketSSEConfigSys.Get(bucket)
+ sseConfig.Apply(formValues, sse.ApplyOptions{
+ AutoEncrypt: globalAutoEncryption,
+ })
+
+ var opts ObjectOptions
+ opts, err = putOptsFromReq(ctx, r, bucket, object, metadata)
+ if err != nil {
+ writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
+ return
+ }
+ opts.WantChecksum = checksum
+
+ fanOutOpts := fanOutOptions{Checksum: checksum}
+ if crypto.Requested(formValues) {
+ if crypto.SSECopy.IsRequested(r.Header) {
+ writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidEncryptionParameters), r.URL)
+ return
+ }
+
+ if crypto.SSEC.IsRequested(r.Header) && crypto.S3.IsRequested(r.Header) {
+ writeErrorResponse(ctx, w, toAPIError(ctx, crypto.ErrIncompatibleEncryptionMethod), r.URL)
+ return
+ }
+
+ if crypto.SSEC.IsRequested(r.Header) && crypto.S3KMS.IsRequested(r.Header) {
+ writeErrorResponse(ctx, w, toAPIError(ctx, crypto.ErrIncompatibleEncryptionMethod), r.URL)
+ return
+ }
+
+ var (
+ reader io.Reader
+ keyID string
+ key []byte
+ kmsCtx kms.Context
+ )
+ kind, _ := crypto.IsRequested(formValues)
+ switch kind {
+ case crypto.SSEC:
+ key, err = ParseSSECustomerHeader(formValues)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ case crypto.S3KMS:
+ keyID, kmsCtx, err = crypto.S3KMS.ParseHTTP(formValues)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ }
+
+ if len(fanOutEntries) == 0 {
+ reader, objectEncryptionKey, err = newEncryptReader(ctx, hashReader, kind, keyID, key, bucket, object, metadata, kmsCtx)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ wantSize := int64(-1)
+ if actualSize >= 0 {
+ info := ObjectInfo{Size: actualSize}
+ wantSize = info.EncryptedSize()
+ }
+
+ // do not try to verify encrypted content/
+ hashReader, err = hash.NewReader(ctx, reader, wantSize, "", "", actualSize)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ if checksum != nil && checksum.Valid() {
+ if err = hashReader.AddChecksumNoTrailer(formValues, true); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ }
+ opts.EncryptFn = metadataEncrypter(objectEncryptionKey)
+ pReader, err = pReader.WithEncryption(hashReader, &objectEncryptionKey)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ } else {
+ fanOutOpts = fanOutOptions{
+ Key: key,
+ Kind: kind,
+ KeyID: keyID,
+ KmsCtx: kmsCtx,
+ Checksum: checksum,
+ }
+ }
+ }
+
+ if len(fanOutEntries) > 0 {
+ // Fan-out requires no copying, and must be carried from original source
+ // https://en.wikipedia.org/wiki/Copy_protection so the incoming stream
+ // is always going to be in-memory as we cannot re-read from what we
+ // wrote to disk - since that amounts to "copying" from a "copy"
+ // instead of "copying" from source, we need the stream to be seekable
+ // to ensure that we can make fan-out calls concurrently.
+ buf := bytebufferpool.Get()
+ defer func() {
+ buf.Reset()
+ bytebufferpool.Put(buf)
+ }()
+
+ md5w := md5.New()
+
+ // Maximum allowed fan-out object size.
+ const maxFanOutSize = 16 << 20
+
+ n, err := io.Copy(io.MultiWriter(buf, md5w), ioutil.HardLimitReader(pReader, maxFanOutSize))
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Set the correct hex md5sum for the fan-out stream.
+ fanOutOpts.MD5Hex = hex.EncodeToString(md5w.Sum(nil))
+
+ concurrentSize := min(runtime.GOMAXPROCS(0), 100)
+
+ fanOutResp := make([]minio.PutObjectFanOutResponse, 0, len(fanOutEntries))
+ eventArgsList := make([]eventArgs, 0, len(fanOutEntries))
+ for {
+ var objInfos []ObjectInfo
+ var errs []error
+
+ var done bool
+ if len(fanOutEntries) < concurrentSize {
+ objInfos, errs = fanOutPutObject(ctx, bucket, objectAPI, fanOutEntries, buf.Bytes()[:n], fanOutOpts)
+ done = true
+ } else {
+ objInfos, errs = fanOutPutObject(ctx, bucket, objectAPI, fanOutEntries[:concurrentSize], buf.Bytes()[:n], fanOutOpts)
+ fanOutEntries = fanOutEntries[concurrentSize:]
+ }
+
+ for i, objInfo := range objInfos {
+ if errs[i] != nil {
+ fanOutResp = append(fanOutResp, minio.PutObjectFanOutResponse{
+ Key: objInfo.Name,
+ Error: errs[i].Error(),
+ })
+ eventArgsList = append(eventArgsList, eventArgs{
+ EventName: event.ObjectCreatedPost,
+ BucketName: objInfo.Bucket,
+ Object: ObjectInfo{Name: objInfo.Name},
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: fmt.Sprintf("%s MinIO-Fan-Out (failed: %v)", r.UserAgent(), errs[i]),
+ Host: handlers.GetSourceIP(r),
+ })
+ continue
+ }
+
+ fanOutResp = append(fanOutResp, minio.PutObjectFanOutResponse{
+ Key: objInfo.Name,
+ ETag: getDecryptedETag(formValues, objInfo, false),
+ VersionID: objInfo.VersionID,
+ LastModified: &objInfo.ModTime,
+ })
+
+ eventArgsList = append(eventArgsList, eventArgs{
+ EventName: event.ObjectCreatedPost,
+ BucketName: objInfo.Bucket,
+ Object: objInfo,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent() + " " + "MinIO-Fan-Out",
+ Host: handlers.GetSourceIP(r),
+ })
+ }
+
+ if done {
+ break
+ }
+ }
+
+ enc := json.NewEncoder(w)
+ for i, fanOutResp := range fanOutResp {
+ if err = enc.Encode(&fanOutResp); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Notify object created events.
+ sendEvent(eventArgsList[i])
+
+ if eventArgsList[i].Object.NumVersions > int(scannerExcessObjectVersions.Load()) {
+ // Send events for excessive versions.
+ sendEvent(eventArgs{
+ EventName: event.ObjectManyVersions,
+ BucketName: eventArgsList[i].Object.Bucket,
+ Object: eventArgsList[i].Object,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent() + " " + "MinIO-Fan-Out",
+ Host: handlers.GetSourceIP(r),
+ })
+
+ auditLogInternal(context.Background(), AuditLogOptions{
+ Event: "scanner:manyversions",
+ APIName: "PostPolicyBucket",
+ Bucket: eventArgsList[i].Object.Bucket,
+ Object: eventArgsList[i].Object.Name,
+ VersionID: eventArgsList[i].Object.VersionID,
+ Status: http.StatusText(http.StatusOK),
+ })
+ }
+ }
+
+ return
+ }
+
+ if formValues.Get(postPolicyBucketTagging) != "" {
+ tags, err := tags.ParseObjectXML(strings.NewReader(formValues.Get(postPolicyBucketTagging)))
+ if err != nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedPOSTRequest), r.URL)
+ return
+ }
+ tagsStr := tags.String()
+ opts.UserDefined[xhttp.AmzObjectTagging] = tagsStr
+ } else {
+ // avoid user set an invalid tag using `X-Amz-Tagging`
+ delete(opts.UserDefined, xhttp.AmzObjectTagging)
+ }
+
+ objInfo, err := objectAPI.PutObject(ctx, bucket, object, pReader, opts)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ etag := getDecryptedETag(formValues, objInfo, false)
+
+ // We must not use the http.Header().Set method here because some (broken)
+ // clients expect the ETag header key to be literally "ETag" - not "Etag" (case-sensitive).
+ // Therefore, we have to set the ETag directly as map entry.
+ w.Header()[xhttp.ETag] = []string{`"` + etag + `"`}
+
+ // Set the relevant version ID as part of the response header.
+ if objInfo.VersionID != "" && objInfo.VersionID != nullVersionID {
+ w.Header()[xhttp.AmzVersionID] = []string{objInfo.VersionID}
+ }
+
+ if obj := getObjectLocation(r, globalDomainNames, bucket, object); obj != "" {
+ w.Header().Set(xhttp.Location, obj)
+ }
+
+ // Notify object created event.
+ defer sendEvent(eventArgs{
+ EventName: event.ObjectCreatedPost,
+ BucketName: objInfo.Bucket,
+ Object: objInfo,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+
+ if objInfo.NumVersions > int(scannerExcessObjectVersions.Load()) {
+ defer sendEvent(eventArgs{
+ EventName: event.ObjectManyVersions,
+ BucketName: objInfo.Bucket,
+ Object: objInfo,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+
+ auditLogInternal(context.Background(), AuditLogOptions{
+ Event: "scanner:manyversions",
+ APIName: "PostPolicyBucket",
+ Bucket: objInfo.Bucket,
+ Object: objInfo.Name,
+ VersionID: objInfo.VersionID,
+ Status: http.StatusText(http.StatusOK),
+ })
+ }
+
+ if redirectURL != nil { // success_action_redirect is valid and set.
+ v := redirectURL.Query()
+ v.Add("bucket", objInfo.Bucket)
+ v.Add("key", objInfo.Name)
+ v.Add("etag", "\""+objInfo.ETag+"\"")
+ redirectURL.RawQuery = v.Encode()
+ writeRedirectSeeOther(w, redirectURL.String())
+ return
+ }
+
+ // Add checksum header.
+ if checksum != nil && checksum.Valid() {
+ hash.AddChecksumHeader(w, checksum.AsMap())
+ }
+
+ // Decide what http response to send depending on success_action_status parameter
+ switch successStatus {
+ case "201":
+ resp := encodeResponse(PostResponse{
+ Bucket: objInfo.Bucket,
+ Key: objInfo.Name,
+ ETag: `"` + objInfo.ETag + `"`,
+ Location: w.Header().Get(xhttp.Location),
+ })
+ writeResponse(w, http.StatusCreated, resp, mimeXML)
+ case "200":
+ writeSuccessResponseHeadersOnly(w)
+ default:
+ writeSuccessNoContent(w)
+ }
+}
+
+// GetBucketPolicyStatusHandler - Retrieves the policy status
+// for an MinIO bucket, indicating whether the bucket is public.
+func (api objectAPIHandlers) GetBucketPolicyStatusHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetBucketPolicyStatus")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(ErrServerNotInitialized))
+ return
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketPolicyStatusAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(s3Error))
+ return
+ }
+
+ // Check if bucket exists.
+ if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Check if anonymous (non-owner) has access to list objects.
+ readable := globalPolicySys.IsAllowed(policy.BucketPolicyArgs{
+ Action: policy.ListBucketAction,
+ BucketName: bucket,
+ ConditionValues: getConditionValues(r, "", auth.AnonymousCredentials),
+ IsOwner: false,
+ })
+
+ // Check if anonymous (non-owner) has access to upload objects.
+ writable := globalPolicySys.IsAllowed(policy.BucketPolicyArgs{
+ Action: policy.PutObjectAction,
+ BucketName: bucket,
+ ConditionValues: getConditionValues(r, "", auth.AnonymousCredentials),
+ IsOwner: false,
+ })
+
+ encodedSuccessResponse := encodeResponse(PolicyStatus{
+ IsPublic: func() string {
+ // Silly to have special 'boolean' values yes
+ // but complying with silly implementation
+ // https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketPolicyStatus.html
+ if readable && writable {
+ return "TRUE"
+ }
+ return "FALSE"
+ }(),
+ })
+
+ writeSuccessResponseXML(w, encodedSuccessResponse)
+}
+
+// HeadBucketHandler - HEAD Bucket
+// ----------
+// This operation is useful to determine if a bucket exists.
+// The operation returns a 200 OK if the bucket exists and you
+// have permission to access it. Otherwise, the operation might
+// return responses such as 404 Not Found and 403 Forbidden.
+func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "HeadBucket")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(ErrServerNotInitialized))
+ return
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.HeadBucketAction, bucket, ""); s3Error != ErrNone {
+ if s3Error := checkRequestAuthType(ctx, r, policy.ListBucketAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(s3Error))
+ return
+ }
+ }
+
+ getBucketInfo := objectAPI.GetBucketInfo
+
+ if _, err := getBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
+ return
+ }
+
+ writeResponse(w, http.StatusOK, nil, mimeXML)
+}
+
+// DeleteBucketHandler - Delete bucket
+func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "DeleteBucket")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ // Verify if the caller has sufficient permissions.
+ if s3Error := checkRequestAuthType(ctx, r, policy.DeleteBucketAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ forceDelete := false
+ if value := r.Header.Get(xhttp.MinIOForceDelete); value != "" {
+ var err error
+ forceDelete, err = strconv.ParseBool(value)
+ if err != nil {
+ apiErr := errorCodes.ToAPIErr(ErrInvalidRequest)
+ apiErr.Description = err.Error()
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+
+ // if force delete header is set, we need to evaluate the policy anyways
+ // regardless of it being true or not.
+ if s3Error := checkRequestAuthType(ctx, r, policy.ForceDeleteBucketAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ if forceDelete {
+ if rcfg, _ := globalBucketObjectLockSys.Get(bucket); rcfg.LockEnabled {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
+ return
+ }
+ rcfg, err := getReplicationConfig(ctx, bucket)
+ switch {
+ case err != nil:
+ if _, ok := err.(BucketReplicationConfigNotFound); !ok {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
+ return
+ }
+ case rcfg != nil && rcfg.HasActiveRules("", true):
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
+ return
+ }
+ }
+ }
+
+ // Return an error if the bucket does not exist
+ if !forceDelete {
+ if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ }
+
+ // Attempt to delete bucket.
+ if err := objectAPI.DeleteBucket(ctx, bucket, DeleteBucketOptions{
+ Force: forceDelete,
+ SRDeleteOp: getSRBucketDeleteOp(globalSiteReplicationSys.isEnabled()),
+ }); err != nil {
+ apiErr := toAPIError(ctx, err)
+ if _, ok := err.(BucketNotEmpty); ok {
+ if globalBucketVersioningSys.Enabled(bucket) || globalBucketVersioningSys.Suspended(bucket) {
+ apiErr.Description = "The bucket you tried to delete is not empty. You must delete all versions in the bucket."
+ }
+ }
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+
+ if globalDNSConfig != nil {
+ if err := globalDNSConfig.Delete(bucket); err != nil {
+ dnsLogIf(ctx, fmt.Errorf("Unable to delete bucket DNS entry %w, please delete it manually, bucket on MinIO no longer exists", err))
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ }
+
+ globalNotificationSys.DeleteBucketMetadata(ctx, bucket)
+ globalReplicationPool.Get().deleteResyncMetadata(ctx, bucket)
+
+ // Call site replication hook.
+ replLogIf(ctx, globalSiteReplicationSys.DeleteBucketHook(ctx, bucket, forceDelete))
+
+ // Write success response.
+ writeSuccessNoContent(w)
+
+ sendEvent(eventArgs{
+ EventName: event.BucketRemoved,
+ BucketName: bucket,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+}
+
+// PutBucketObjectLockConfigHandler - PUT Bucket object lock configuration.
+// ----------
+// Places an Object Lock configuration on the specified bucket. The rule
+// specified in the Object Lock configuration will be applied by default
+// to every new object placed in the specified bucket.
+func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "PutBucketObjectLockConfig")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+ if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketObjectLockConfigurationAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ config, err := objectlock.ParseObjectLockConfig(r.Body)
+ if err != nil {
+ apiErr := errorCodes.ToAPIErr(ErrInvalidArgument)
+ apiErr.Description = err.Error()
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+
+ // Audit log tags.
+ reqInfo := logger.GetReqInfo(ctx)
+ reqInfo.SetTags("retention", config.String())
+
+ configData, err := xml.Marshal(config)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Deny object locking configuration settings on existing buckets without object lock enabled.
+ if _, _, err = globalBucketMetadataSys.GetObjectLockConfig(bucket); err != nil {
+ if _, ok := err.(BucketObjectLockConfigNotFound); ok {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrObjectLockConfigurationNotAllowed), r.URL)
+ } else {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ }
+ return
+ }
+
+ updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, objectLockConfig, configData)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Call site replication hook.
+ //
+ // We encode the xml bytes as base64 to ensure there are no encoding
+ // errors.
+ cfgStr := base64.StdEncoding.EncodeToString(configData)
+ replLogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
+ Type: madmin.SRBucketMetaTypeObjectLockConfig,
+ Bucket: bucket,
+ ObjectLockConfig: &cfgStr,
+ UpdatedAt: updatedAt,
+ }))
+
+ // Write success response.
+ writeSuccessResponseHeadersOnly(w)
+}
+
+// GetBucketObjectLockConfigHandler - GET Bucket object lock configuration.
+// ----------
+// Gets the Object Lock configuration for a bucket. The rule specified in
+// the Object Lock configuration will be applied by default to every new
+// object placed in the specified bucket.
+func (api objectAPIHandlers) GetBucketObjectLockConfigHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetBucketObjectLockConfig")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ // check if user has permissions to perform this operation
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketObjectLockConfigurationAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ config, _, err := globalBucketMetadataSys.GetObjectLockConfig(bucket)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ configData, err := xml.Marshal(config)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Write success response.
+ writeSuccessResponseXML(w, configData)
+}
+
+// PutBucketTaggingHandler - PUT Bucket tagging.
+// ----------
+func (api objectAPIHandlers) PutBucketTaggingHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "PutBucketTagging")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ // Check if bucket exists.
+ if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketTaggingAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ tags, err := tags.ParseBucketXML(io.LimitReader(r.Body, r.ContentLength))
+ if err != nil {
+ apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
+ apiErr.Description = err.Error()
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+
+ configData, err := xml.Marshal(tags)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketTaggingConfig, configData)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Call site replication hook.
+ //
+ // We encode the xml bytes as base64 to ensure there are no encoding
+ // errors.
+ cfgStr := base64.StdEncoding.EncodeToString(configData)
+ replLogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
+ Type: madmin.SRBucketMetaTypeTags,
+ Bucket: bucket,
+ Tags: &cfgStr,
+ UpdatedAt: updatedAt,
+ }))
+
+ // Write success response.
+ writeSuccessResponseHeadersOnly(w)
+}
+
+// GetBucketTaggingHandler - GET Bucket tagging.
+// ----------
+func (api objectAPIHandlers) GetBucketTaggingHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetBucketTagging")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ // check if user has permissions to perform this operation
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketTaggingAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ config, _, err := globalBucketMetadataSys.GetTaggingConfig(bucket)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ configData, err := xml.Marshal(config)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Write success response.
+ writeSuccessResponseXML(w, configData)
+}
+
+// DeleteBucketTaggingHandler - DELETE Bucket tagging.
+// ----------
+func (api objectAPIHandlers) DeleteBucketTaggingHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "DeleteBucketTagging")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketTaggingAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ updatedAt, err := globalBucketMetadataSys.Delete(ctx, bucket, bucketTaggingConfig)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ replLogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
+ Type: madmin.SRBucketMetaTypeTags,
+ Bucket: bucket,
+ UpdatedAt: updatedAt,
+ }))
+
+ // Write success response.
+ writeSuccessNoContent(w)
+}
diff --git a/cmd/bucket-handlers_test.go b/cmd/bucket-handlers_test.go
new file mode 100644
index 00000000..0adb9b8f
--- /dev/null
+++ b/cmd/bucket-handlers_test.go
@@ -0,0 +1,946 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "encoding/xml"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "strconv"
+ "testing"
+
+ "github.com/minio/minio/internal/auth"
+)
+
+// Wrapper for calling RemoveBucket HTTP handler tests for both Erasure multiple disks and single node setup.
+func TestRemoveBucketHandler(t *testing.T) {
+ ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testRemoveBucketHandler, endpoints: []string{"RemoveBucket"}})
+}
+
+func testRemoveBucketHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ credentials auth.Credentials, t *testing.T,
+) {
+ _, err := obj.PutObject(GlobalContext, bucketName, "test-object", mustGetPutObjReader(t, bytes.NewReader([]byte{}), int64(0), "", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), ObjectOptions{})
+ // if object upload fails stop the test.
+ if err != nil {
+ t.Fatalf("Error uploading object: %v", err)
+ }
+
+ // initialize httptest Recorder, this records any mutations to response writer inside the handler.
+ rec := httptest.NewRecorder()
+ // construct HTTP request for DELETE bucket.
+ req, err := newTestSignedRequestV4(http.MethodDelete, getBucketLocationURL("", bucketName), 0, nil, credentials.AccessKey, credentials.SecretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %s: Failed to create HTTP request for RemoveBucketHandler: %v", instanceType, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler.
+ apiRouter.ServeHTTP(rec, req)
+ switch rec.Code {
+ case http.StatusOK, http.StatusCreated, http.StatusAccepted, http.StatusNoContent:
+ t.Fatalf("Test %v: expected failure, but succeeded with %v", instanceType, rec.Code)
+ }
+
+ // Verify response of the V2 signed HTTP request.
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ recV2 := httptest.NewRecorder()
+ // construct HTTP request for DELETE bucket.
+ reqV2, err := newTestSignedRequestV2(http.MethodDelete, getBucketLocationURL("", bucketName), 0, nil, credentials.AccessKey, credentials.SecretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %s: Failed to create HTTP request for RemoveBucketHandler: %v", instanceType, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler.
+ apiRouter.ServeHTTP(recV2, reqV2)
+ switch recV2.Code {
+ case http.StatusOK, http.StatusCreated, http.StatusAccepted, http.StatusNoContent:
+ t.Fatalf("Test %v: expected failure, but succeeded with %v", instanceType, recV2.Code)
+ }
+}
+
+// Wrapper for calling GetBucketPolicy HTTP handler tests for both Erasure multiple disks and single node setup.
+func TestGetBucketLocationHandler(t *testing.T) {
+ ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testGetBucketLocationHandler, endpoints: []string{"GetBucketLocation"}})
+}
+
+func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ credentials auth.Credentials, t *testing.T,
+) {
+ // test cases with sample input and expected output.
+ testCases := []struct {
+ bucketName string
+ accessKey string
+ secretKey string
+ // expected Response.
+ expectedRespStatus int
+ locationResponse []byte
+ errorResponse APIErrorResponse
+ shouldPass bool
+ }{
+ // Test case - 1.
+ // Tests for authenticated request and proper response.
+ {
+ bucketName: bucketName,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusOK,
+ locationResponse: []byte(`
+`),
+ errorResponse: APIErrorResponse{},
+ shouldPass: true,
+ },
+ // Test case - 2.
+ // Tests for signature mismatch error.
+ {
+ bucketName: bucketName,
+ accessKey: "abcd",
+ secretKey: "abcd",
+ expectedRespStatus: http.StatusForbidden,
+ locationResponse: []byte(""),
+ errorResponse: APIErrorResponse{
+ Resource: SlashSeparator + bucketName + SlashSeparator,
+ Code: "InvalidAccessKeyId",
+ Message: "The Access Key Id you provided does not exist in our records.",
+ },
+ shouldPass: false,
+ },
+ }
+
+ for i, testCase := range testCases {
+ if i != 1 {
+ continue
+ }
+ // initialize httptest Recorder, this records any mutations to response writer inside the handler.
+ rec := httptest.NewRecorder()
+ // construct HTTP request for Get bucket location.
+ req, err := newTestSignedRequestV4(http.MethodGet, getBucketLocationURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed to create HTTP request for GetBucketLocationHandler: %v", i+1, instanceType, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler.
+ apiRouter.ServeHTTP(rec, req)
+ if rec.Code != testCase.expectedRespStatus {
+ t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, rec.Code)
+ }
+ if !bytes.Equal(testCase.locationResponse, rec.Body.Bytes()) && testCase.shouldPass {
+ t.Errorf("Test %d: %s: Expected the response to be `%s`, but instead found `%s`", i+1, instanceType, string(testCase.locationResponse), rec.Body.String())
+ }
+ errorResponse := APIErrorResponse{}
+ err = xml.Unmarshal(rec.Body.Bytes(), &errorResponse)
+ if err != nil && !testCase.shouldPass {
+ t.Fatalf("Test %d: %s: Unable to marshal response body %s", i+1, instanceType, rec.Body.String())
+ }
+ if errorResponse.Resource != testCase.errorResponse.Resource {
+ t.Errorf("Test %d: %s: Expected the error resource to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Resource, errorResponse.Resource)
+ }
+ if errorResponse.Message != testCase.errorResponse.Message {
+ t.Errorf("Test %d: %s: Expected the error message to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Message, errorResponse.Message)
+ }
+ if errorResponse.Code != testCase.errorResponse.Code {
+ t.Errorf("Test %d: %s: Expected the error code to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Code, errorResponse.Code)
+ }
+
+ // Verify response of the V2 signed HTTP request.
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ recV2 := httptest.NewRecorder()
+ // construct HTTP request for PUT bucket policy endpoint.
+ reqV2, err := newTestSignedRequestV2(http.MethodGet, getBucketLocationURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: %v", i+1, instanceType, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler.
+ apiRouter.ServeHTTP(recV2, reqV2)
+ 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)
+ }
+
+ errorResponse = APIErrorResponse{}
+ err = xml.Unmarshal(recV2.Body.Bytes(), &errorResponse)
+ if err != nil && !testCase.shouldPass {
+ t.Fatalf("Test %d: %s: Unable to marshal response body %s", i+1, instanceType, recV2.Body.String())
+ }
+ if errorResponse.Resource != testCase.errorResponse.Resource {
+ t.Errorf("Test %d: %s: Expected the error resource to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Resource, errorResponse.Resource)
+ }
+ if errorResponse.Message != testCase.errorResponse.Message {
+ t.Errorf("Test %d: %s: Expected the error message to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Message, errorResponse.Message)
+ }
+ if errorResponse.Code != testCase.errorResponse.Code {
+ t.Errorf("Test %d: %s: Expected the error code to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Code, errorResponse.Code)
+ }
+ }
+
+ // Test for Anonymous/unsigned http request.
+ // ListBucketsHandler doesn't support bucket policies, setting the policies shouldn't make any difference.
+ anonReq, err := newTestRequest(http.MethodGet, getBucketLocationURL("", bucketName), 0, nil)
+ if err != nil {
+ t.Fatalf("MinIO %s: Failed to create an anonymous request.", instanceType)
+ }
+
+ // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
+ // sets the bucket policy using the policy statement generated from `getReadOnlyBucketStatement` so that the
+ // unsigned request goes through and its validated again.
+ ExecObjectLayerAPIAnonTest(t, obj, "TestGetBucketLocationHandler", bucketName, "", instanceType, apiRouter, anonReq, getAnonReadOnlyBucketPolicy(bucketName))
+
+ // HTTP request for testing when `objectLayer` is set to `nil`.
+ // There is no need to use an existing bucket and valid input for creating the request
+ // since the `objectLayer==nil` check is performed before any other checks inside the handlers.
+ // The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
+
+ nilBucket := "dummy-bucket"
+ nilReq, err := newTestRequest(http.MethodGet, getBucketLocationURL("", nilBucket), 0, nil)
+ if err != nil {
+ t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
+ }
+ // Executes the object layer set to `nil` test.
+ // `ExecObjectLayerAPINilTest` manages the operation.
+ ExecObjectLayerAPINilTest(t, nilBucket, "", instanceType, apiRouter, nilReq)
+}
+
+// Wrapper for calling HeadBucket HTTP handler tests for both Erasure multiple disks and single node setup.
+func TestHeadBucketHandler(t *testing.T) {
+ ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testHeadBucketHandler, endpoints: []string{"HeadBucket"}})
+}
+
+func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ credentials auth.Credentials, t *testing.T,
+) {
+ // test cases with sample input and expected output.
+ testCases := []struct {
+ bucketName string
+ accessKey string
+ secretKey string
+ // expected Response.
+ expectedRespStatus int
+ }{
+ // Test case - 1.
+ // Bucket exists.
+ {
+ bucketName: bucketName,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusOK,
+ },
+ // Test case - 2.
+ // Non-existent bucket name.
+ {
+ bucketName: "2333",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusNotFound,
+ },
+ // Test case - 3.
+ // Testing for signature mismatch error.
+ // setting invalid access and secret key.
+ {
+ bucketName: bucketName,
+ accessKey: "abcd",
+ secretKey: "abcd",
+ expectedRespStatus: http.StatusForbidden,
+ },
+ }
+
+ for i, testCase := range testCases {
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ rec := httptest.NewRecorder()
+ // construct HTTP request for HEAD bucket.
+ req, err := newTestSignedRequestV4(http.MethodHead, getHEADBucketURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed to create HTTP request for HeadBucketHandler: %v", i+1, instanceType, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler.
+ apiRouter.ServeHTTP(rec, req)
+ if rec.Code != testCase.expectedRespStatus {
+ t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, rec.Code)
+ }
+
+ // Verify response the V2 signed HTTP request.
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ recV2 := httptest.NewRecorder()
+ // construct HTTP request for PUT bucket policy endpoint.
+ reqV2, err := newTestSignedRequestV2(http.MethodHead, getHEADBucketURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: %v", i+1, instanceType, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler.
+ apiRouter.ServeHTTP(recV2, reqV2)
+ 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)
+ }
+ }
+
+ // Test for Anonymous/unsigned http request.
+ anonReq, err := newTestRequest(http.MethodHead, getHEADBucketURL("", bucketName), 0, nil)
+ if err != nil {
+ t.Fatalf("MinIO %s: Failed to create an anonymous request for bucket \"%s\": %v",
+ instanceType, bucketName, err)
+ }
+
+ // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
+ // sets the bucket policy using the policy statement generated from `getReadOnlyBucketStatement` so that the
+ // unsigned request goes through and its validated again.
+ ExecObjectLayerAPIAnonTest(t, obj, "TestHeadBucketHandler", bucketName, "", instanceType, apiRouter, anonReq, getAnonReadOnlyBucketPolicy(bucketName))
+
+ // HTTP request for testing when `objectLayer` is set to `nil`.
+ // There is no need to use an existing bucket and valid input for creating the request
+ // since the `objectLayer==nil` check is performed before any other checks inside the handlers.
+ // The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
+
+ nilBucket := "dummy-bucket"
+ nilReq, err := newTestRequest(http.MethodHead, getHEADBucketURL("", nilBucket), 0, nil)
+ if err != nil {
+ t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
+ }
+ // execute the object layer set to `nil` test.
+ // `ExecObjectLayerAPINilTest` manages the operation.
+ ExecObjectLayerAPINilTest(t, nilBucket, "", instanceType, apiRouter, nilReq)
+}
+
+// Wrapper for calling TestListMultipartUploadsHandler tests for both Erasure multiple disks and single node setup.
+func TestListMultipartUploadsHandler(t *testing.T) {
+ ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testListMultipartUploadsHandler, endpoints: []string{"ListMultipartUploads"}})
+}
+
+// testListMultipartUploadsHandler - Tests validate listing of multipart uploads.
+func testListMultipartUploadsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ credentials auth.Credentials, t *testing.T,
+) {
+ // Collection of non-exhaustive ListMultipartUploads test cases, valid errors
+ // and success responses.
+ testCases := []struct {
+ // Inputs to ListMultipartUploads.
+ bucket string
+ prefix string
+ keyMarker string
+ uploadIDMarker string
+ delimiter string
+ maxUploads string
+ accessKey string
+ secretKey string
+ expectedRespStatus int
+ shouldPass bool
+ }{
+ // Test case - 1.
+ // Setting invalid bucket name.
+ {
+ bucket: ".test",
+ prefix: "",
+ keyMarker: "",
+ uploadIDMarker: "",
+ delimiter: "",
+ maxUploads: "0",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusBadRequest,
+ shouldPass: false,
+ },
+ // Test case - 2.
+ // Setting a non-existent bucket.
+ {
+ bucket: "volatile-bucket-1",
+ prefix: "",
+ keyMarker: "",
+ uploadIDMarker: "",
+ delimiter: "",
+ maxUploads: "0",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusNotFound,
+ shouldPass: false,
+ },
+ // Test case -3.
+ // Delimiter unsupported, but response is empty.
+ {
+ bucket: bucketName,
+ prefix: "",
+ keyMarker: "",
+ uploadIDMarker: "",
+ delimiter: "-",
+ maxUploads: "0",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusOK,
+ shouldPass: true,
+ },
+ // Test case - 4.
+ // Setting Invalid prefix and marker combination.
+ {
+ bucket: bucketName,
+ prefix: "asia",
+ keyMarker: "europe-object",
+ uploadIDMarker: "",
+ delimiter: "",
+ maxUploads: "0",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusNotImplemented,
+ shouldPass: false,
+ },
+ // Test case - 5.
+ // Invalid upload id and marker combination.
+ {
+ bucket: bucketName,
+ prefix: "asia",
+ keyMarker: "asia/europe/",
+ uploadIDMarker: "abc",
+ delimiter: "",
+ maxUploads: "0",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusNotImplemented,
+ shouldPass: false,
+ },
+ // Test case - 6.
+ // Setting a negative value to max-uploads parameter, should result in http.StatusBadRequest.
+ {
+ bucket: bucketName,
+ prefix: "",
+ keyMarker: "",
+ uploadIDMarker: "",
+ delimiter: "",
+ maxUploads: "-1",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusBadRequest,
+ shouldPass: false,
+ },
+ // Test case - 7.
+ // Case with right set of parameters,
+ // should result in success 200OK.
+ {
+ bucket: bucketName,
+ prefix: "",
+ keyMarker: "",
+ uploadIDMarker: "",
+ delimiter: SlashSeparator,
+ maxUploads: "100",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusOK,
+ shouldPass: true,
+ },
+ // Test case - 8.
+ // Good case without delimiter.
+ {
+ bucket: bucketName,
+ prefix: "",
+ keyMarker: "",
+ uploadIDMarker: "",
+ delimiter: "",
+ maxUploads: "100",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusOK,
+ shouldPass: true,
+ },
+ // Test case - 9.
+ // Setting Invalid AccessKey and SecretKey to induce and verify Signature Mismatch error.
+ {
+ bucket: bucketName,
+ prefix: "",
+ keyMarker: "",
+ uploadIDMarker: "",
+ delimiter: "",
+ maxUploads: "100",
+ accessKey: "abcd",
+ secretKey: "abcd",
+ expectedRespStatus: http.StatusForbidden,
+ shouldPass: true,
+ },
+ }
+
+ for i, testCase := range testCases {
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ rec := httptest.NewRecorder()
+
+ // construct HTTP request for List multipart uploads endpoint.
+ u := getListMultipartUploadsURLWithParams("", testCase.bucket, testCase.prefix, testCase.keyMarker, testCase.uploadIDMarker, testCase.delimiter, testCase.maxUploads)
+ req, gerr := newTestSignedRequestV4(http.MethodGet, u, 0, nil, testCase.accessKey, testCase.secretKey, nil)
+ if gerr != nil {
+ t.Fatalf("Test %d: %s: Failed to create HTTP request for ListMultipartUploadsHandler: %v", i+1, instanceType, gerr)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler.
+ apiRouter.ServeHTTP(rec, req)
+ if rec.Code != testCase.expectedRespStatus {
+ t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, rec.Code)
+ }
+
+ // Verify response the V2 signed HTTP request.
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ recV2 := httptest.NewRecorder()
+ // construct HTTP request for PUT bucket policy endpoint.
+
+ // verify response for V2 signed HTTP request.
+ reqV2, err := newTestSignedRequestV2(http.MethodGet, u, 0, nil, testCase.accessKey, testCase.secretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: %v", i+1, instanceType, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler.
+ apiRouter.ServeHTTP(recV2, reqV2)
+ 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)
+ }
+ }
+
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ rec := httptest.NewRecorder()
+
+ // construct HTTP request for List multipart uploads endpoint.
+ u := getListMultipartUploadsURLWithParams("", bucketName, "", "", "", "", "")
+ req, err := newTestSignedRequestV4(http.MethodGet, u, 0, nil, "", "", nil) // Generate an anonymous request.
+ if err != nil {
+ t.Fatalf("Test %s: Failed to create HTTP request for ListMultipartUploadsHandler: %v", instanceType, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler.
+ apiRouter.ServeHTTP(rec, req)
+ if rec.Code != http.StatusForbidden {
+ t.Errorf("Test %s: Expected the response status to be `http.StatusForbidden`, but instead found `%d`", instanceType, rec.Code)
+ }
+
+ url := getListMultipartUploadsURLWithParams("", testCases[6].bucket, testCases[6].prefix, testCases[6].keyMarker,
+ testCases[6].uploadIDMarker, testCases[6].delimiter, testCases[6].maxUploads)
+ // Test for Anonymous/unsigned http request.
+ anonReq, err := newTestRequest(http.MethodGet, url, 0, nil)
+ if err != nil {
+ t.Fatalf("MinIO %s: Failed to create an anonymous request for bucket \"%s\": %v",
+ instanceType, bucketName, err)
+ }
+
+ // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
+ // sets the bucket policy using the policy statement generated from `getWriteOnlyBucketStatement` so that the
+ // unsigned request goes through and its validated again.
+ ExecObjectLayerAPIAnonTest(t, obj, "TestListMultipartUploadsHandler", bucketName, "", instanceType, apiRouter, anonReq, getAnonWriteOnlyBucketPolicy(bucketName))
+
+ // HTTP request for testing when `objectLayer` is set to `nil`.
+ // There is no need to use an existing bucket and valid input for creating the request
+ // since the `objectLayer==nil` check is performed before any other checks inside the handlers.
+ // The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
+
+ nilBucket := "dummy-bucket"
+ url = getListMultipartUploadsURLWithParams("", nilBucket, "dummy-prefix", testCases[6].keyMarker,
+ testCases[6].uploadIDMarker, testCases[6].delimiter, testCases[6].maxUploads)
+
+ nilReq, err := newTestRequest(http.MethodGet, url, 0, nil)
+ if err != nil {
+ t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
+ }
+ // execute the object layer set to `nil` test.
+ // `ExecObjectLayerAPINilTest` manages the operation.
+ ExecObjectLayerAPINilTest(t, nilBucket, "", instanceType, apiRouter, nilReq)
+}
+
+// Wrapper for calling TestListBucketsHandler tests for both Erasure multiple disks and single node setup.
+func TestListBucketsHandler(t *testing.T) {
+ ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testListBucketsHandler, endpoints: []string{"ListBuckets"}})
+}
+
+// testListBucketsHandler - Tests validate listing of buckets.
+func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ credentials auth.Credentials, t *testing.T,
+) {
+ testCases := []struct {
+ bucketName string
+ accessKey string
+ secretKey string
+ expectedRespStatus int
+ }{
+ // Test case - 1.
+ // Validate a good case request succeeds.
+ {
+ bucketName: bucketName,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusOK,
+ },
+ // Test case - 2.
+ // Test case with invalid accessKey to produce and validate Signature Mismatch error.
+ {
+ bucketName: bucketName,
+ accessKey: "abcd",
+ secretKey: "abcd",
+ expectedRespStatus: http.StatusForbidden,
+ },
+ }
+
+ for i, testCase := range testCases {
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ rec := httptest.NewRecorder()
+ req, lerr := newTestSignedRequestV4(http.MethodGet, getListBucketURL(""), 0, nil, testCase.accessKey, testCase.secretKey, nil)
+ if lerr != nil {
+ t.Fatalf("Test %d: %s: Failed to create HTTP request for ListBucketsHandler: %v", i+1, instanceType, lerr)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler.
+ apiRouter.ServeHTTP(rec, req)
+ if rec.Code != testCase.expectedRespStatus {
+ t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, rec.Code)
+ }
+
+ // Verify response of the V2 signed HTTP request.
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ recV2 := httptest.NewRecorder()
+ // construct HTTP request for PUT bucket policy endpoint.
+
+ // verify response for V2 signed HTTP request.
+ reqV2, err := newTestSignedRequestV2(http.MethodGet, getListBucketURL(""), 0, nil, testCase.accessKey, testCase.secretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: %v", i+1, instanceType, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler.
+ apiRouter.ServeHTTP(recV2, reqV2)
+ 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)
+ }
+ }
+
+ // Test for Anonymous/unsigned http request.
+ // ListBucketsHandler doesn't support bucket policies, setting the policies shouldn't make a difference.
+ anonReq, err := newTestRequest(http.MethodGet, getListBucketURL(""), 0, nil)
+ if err != nil {
+ t.Fatalf("MinIO %s: Failed to create an anonymous request.", instanceType)
+ }
+
+ // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
+ // sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
+ // unsigned request goes through and its validated again.
+ ExecObjectLayerAPIAnonTest(t, obj, "ListBucketsHandler", "", "", instanceType, apiRouter, anonReq, getAnonWriteOnlyBucketPolicy("*"))
+
+ // HTTP request for testing when `objectLayer` is set to `nil`.
+ // There is no need to use an existing bucket and valid input for creating the request
+ // since the `objectLayer==nil` check is performed before any other checks inside the handlers.
+ // The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
+
+ nilReq, err := newTestRequest(http.MethodGet, getListBucketURL(""), 0, nil)
+ if err != nil {
+ t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
+ }
+ // execute the object layer set to `nil` test.
+ // `ExecObjectLayerAPINilTest` manages the operation.
+ ExecObjectLayerAPINilTest(t, "", "", instanceType, apiRouter, nilReq)
+}
+
+// Wrapper for calling DeleteMultipleObjects HTTP handler tests for both Erasure multiple disks and single node setup.
+func TestAPIDeleteMultipleObjectsHandler(t *testing.T) {
+ ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testAPIDeleteMultipleObjectsHandler, endpoints: []string{"DeleteMultipleObjects", "PutBucketPolicy"}})
+}
+
+func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ credentials auth.Credentials, t *testing.T,
+) {
+ var err error
+
+ sha256sum := ""
+ var objectNames []string
+ for i := range 10 {
+ contentBytes := []byte("hello")
+ objectName := "test-object-" + strconv.Itoa(i)
+ if i == 0 {
+ objectName += "/"
+ contentBytes = []byte{}
+ }
+ // uploading the object.
+ _, err = obj.PutObject(GlobalContext, bucketName, objectName, mustGetPutObjReader(t, bytes.NewReader(contentBytes), int64(len(contentBytes)), "", sha256sum), ObjectOptions{})
+ // if object upload fails stop the test.
+ if err != nil {
+ t.Fatalf("Put Object %d: Error uploading object: %v", i, err)
+ }
+
+ // object used for the test.
+ objectNames = append(objectNames, objectName)
+ }
+
+ contentBytes := []byte("hello")
+ for _, name := range []string{"private/object", "public/object"} {
+ // Uploading the object with retention enabled
+ _, err = obj.PutObject(GlobalContext, bucketName, name, mustGetPutObjReader(t, bytes.NewReader(contentBytes), int64(len(contentBytes)), "", sha256sum), ObjectOptions{})
+ // if object upload fails stop the test.
+ if err != nil {
+ t.Fatalf("Put Object %s: Error uploading object: %v", name, err)
+ }
+ }
+
+ // The following block will create a bucket policy with delete object to 'public/*'. This is
+ // to test a mixed response of a successful & failure while deleting objects in a single request
+ policyBytes := fmt.Appendf(nil, `{"Id": "Policy1637752602639", "Version": "2012-10-17", "Statement": [{"Sid": "Stmt1637752600730", "Action": "s3:DeleteObject", "Effect": "Allow", "Resource": "arn:aws:s3:::%s/public/*", "Principal": "*"}]}`, bucketName)
+ rec := httptest.NewRecorder()
+ req, err := newTestSignedRequestV4(http.MethodPut, getPutPolicyURL("", bucketName), int64(len(policyBytes)), bytes.NewReader(policyBytes),
+ credentials.AccessKey, credentials.SecretKey, nil)
+ if err != nil {
+ t.Fatalf("Failed to create HTTP request for PutBucketPolicyHandler: %v", err)
+ }
+ apiRouter.ServeHTTP(rec, req)
+ if rec.Code != http.StatusNoContent {
+ t.Errorf("Expected the response status to be `%d`, but instead found `%d`", 200, rec.Code)
+ }
+
+ getObjectToDeleteList := func(objectNames []string) (objectList []ObjectToDelete) {
+ for _, objectName := range objectNames {
+ objectList = append(objectList, ObjectToDelete{
+ ObjectV: ObjectV{
+ ObjectName: objectName,
+ },
+ })
+ }
+
+ return objectList
+ }
+
+ getDeleteErrorList := func(objects []ObjectToDelete) (deleteErrorList []DeleteError) {
+ for _, obj := range objects {
+ deleteErrorList = append(deleteErrorList, DeleteError{
+ Code: errorCodes[ErrAccessDenied].Code,
+ Message: errorCodes[ErrAccessDenied].Description,
+ Key: obj.ObjectName,
+ })
+ }
+
+ return deleteErrorList
+ }
+
+ objects := []ObjectToDelete{}
+ objects = append(objects, ObjectToDelete{
+ ObjectV: ObjectV{
+ ObjectName: "private/object",
+ },
+ })
+ objects = append(objects, ObjectToDelete{
+ ObjectV: ObjectV{
+ ObjectName: "public/object",
+ },
+ })
+ requestList := []DeleteObjectsRequest{
+ {Quiet: false, Objects: getObjectToDeleteList(objectNames[:5])},
+ {Quiet: true, Objects: getObjectToDeleteList(objectNames[5:])},
+ {Quiet: false, Objects: objects},
+ }
+
+ // generate multi objects delete response.
+ successRequest0 := encodeResponse(requestList[0])
+
+ deletedObjects := make([]DeletedObject, len(requestList[0].Objects))
+ for i := range requestList[0].Objects {
+ var vid string
+ if isDirObject(requestList[0].Objects[i].ObjectName) {
+ vid = ""
+ }
+ deletedObjects[i] = DeletedObject{
+ ObjectName: requestList[0].Objects[i].ObjectName,
+ VersionID: vid,
+ }
+ }
+
+ successResponse0 := generateMultiDeleteResponse(requestList[0].Quiet, deletedObjects, nil)
+ encodedSuccessResponse0 := encodeResponse(successResponse0)
+
+ successRequest1 := encodeResponse(requestList[1])
+
+ deletedObjects = make([]DeletedObject, len(requestList[1].Objects))
+ for i := range requestList[1].Objects {
+ var vid string
+ if isDirObject(requestList[0].Objects[i].ObjectName) {
+ vid = ""
+ }
+ deletedObjects[i] = DeletedObject{
+ ObjectName: requestList[1].Objects[i].ObjectName,
+ VersionID: vid,
+ }
+ }
+
+ successResponse1 := generateMultiDeleteResponse(requestList[1].Quiet, deletedObjects, nil)
+ encodedSuccessResponse1 := encodeResponse(successResponse1)
+
+ // generate multi objects delete response for errors.
+ // errorRequest := encodeResponse(requestList[1])
+ errorResponse := generateMultiDeleteResponse(requestList[1].Quiet, deletedObjects, nil)
+ encodedErrorResponse := encodeResponse(errorResponse)
+
+ anonRequest := encodeResponse(requestList[0])
+ anonResponse := generateMultiDeleteResponse(requestList[0].Quiet, nil, getDeleteErrorList(requestList[0].Objects))
+ encodedAnonResponse := encodeResponse(anonResponse)
+
+ anonRequestWithPartialPublicAccess := encodeResponse(requestList[2])
+ anonResponseWithPartialPublicAccess := generateMultiDeleteResponse(requestList[2].Quiet,
+ []DeletedObject{
+ {ObjectName: "public/object"},
+ },
+ []DeleteError{
+ {
+ Code: errorCodes[ErrAccessDenied].Code,
+ Message: errorCodes[ErrAccessDenied].Description,
+ Key: "private/object",
+ },
+ })
+ encodedAnonResponseWithPartialPublicAccess := encodeResponse(anonResponseWithPartialPublicAccess)
+
+ testCases := []struct {
+ bucket string
+ objects []byte
+ accessKey string
+ secretKey string
+ expectedContent []byte
+ expectedRespStatus int
+ }{
+ // Test case - 0.
+ // Delete objects with invalid access key.
+ 0: {
+ bucket: bucketName,
+ objects: successRequest0,
+ accessKey: "Invalid-AccessID",
+ secretKey: credentials.SecretKey,
+ expectedContent: nil,
+ expectedRespStatus: http.StatusForbidden,
+ },
+ // Test case - 1.
+ // Delete valid objects with quiet flag off.
+ 1: {
+ bucket: bucketName,
+ objects: successRequest0,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedContent: encodedSuccessResponse0,
+ expectedRespStatus: http.StatusOK,
+ },
+ // Test case - 2.
+ // Delete deleted objects with quiet flag off.
+ 2: {
+ bucket: bucketName,
+ objects: successRequest0,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedContent: encodedSuccessResponse0,
+ expectedRespStatus: http.StatusOK,
+ },
+ // Test case - 3.
+ // Delete valid objects with quiet flag on.
+ 3: {
+ bucket: bucketName,
+ objects: successRequest1,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedContent: encodedSuccessResponse1,
+ expectedRespStatus: http.StatusOK,
+ },
+ // Test case - 4.
+ // Delete previously deleted objects.
+ 4: {
+ bucket: bucketName,
+ objects: successRequest1,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedContent: encodedErrorResponse,
+ expectedRespStatus: http.StatusOK,
+ },
+ // Test case - 5.
+ // Anonymous user access denied response
+ // Currently anonymous users cannot delete multiple objects in MinIO server
+ 5: {
+ bucket: bucketName,
+ objects: anonRequest,
+ accessKey: "",
+ secretKey: "",
+ expectedContent: encodedAnonResponse,
+ expectedRespStatus: http.StatusOK,
+ },
+ // Test case - 6.
+ // Anonymous user has access to some public folder, issue removing with
+ // another private object as well
+ 6: {
+ bucket: bucketName,
+ objects: anonRequestWithPartialPublicAccess,
+ accessKey: "",
+ secretKey: "",
+ expectedContent: encodedAnonResponseWithPartialPublicAccess,
+ expectedRespStatus: http.StatusOK,
+ },
+ // Test case - 7.
+ // Bucket does not exist.
+ 7: {
+ bucket: "unknown-bucket-name",
+ objects: successRequest0,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusNotFound,
+ },
+ }
+
+ for i, testCase := range testCases {
+ var req *http.Request
+ var actualContent []byte
+
+ // Generate a signed or anonymous request based on the testCase
+ if testCase.accessKey != "" {
+ req, err = newTestSignedRequestV4(http.MethodPost, getDeleteMultipleObjectsURL("", testCase.bucket),
+ int64(len(testCase.objects)), bytes.NewReader(testCase.objects), testCase.accessKey, testCase.secretKey, nil)
+ } else {
+ req, err = newTestRequest(http.MethodPost, getDeleteMultipleObjectsURL("", testCase.bucket),
+ int64(len(testCase.objects)), bytes.NewReader(testCase.objects))
+ }
+ if err != nil {
+ t.Fatalf("Failed to create HTTP request for DeleteMultipleObjects: %v", err)
+ }
+
+ rec := httptest.NewRecorder()
+
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to executes the registered handler.
+ apiRouter.ServeHTTP(rec, req)
+ // Assert the response code with the expected status.
+ if rec.Code != testCase.expectedRespStatus {
+ t.Errorf("Test %d: MinIO %s: Expected the response status to be `%d`, but instead found `%d`", i, instanceType, testCase.expectedRespStatus, rec.Code)
+ }
+
+ // read the response body.
+ actualContent, err = io.ReadAll(rec.Body)
+ if err != nil {
+ t.Fatalf("Test %d : MinIO %s: Failed parsing response body: %v", i, instanceType, err)
+ }
+
+ // Verify whether the bucket obtained object is same as the one created.
+ if testCase.expectedContent != nil && !bytes.Equal(testCase.expectedContent, actualContent) {
+ t.Log(string(testCase.expectedContent), string(actualContent))
+ t.Errorf("Test %d : MinIO %s: Object content differs from expected value.", i, instanceType)
+ }
+ }
+
+ // HTTP request to test the case of `objectLayer` being set to `nil`.
+ // There is no need to use an existing bucket or valid input for creating the request,
+ // since the `objectLayer==nil` check is performed before any other checks inside the handlers.
+ // The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
+ // Indicating that all parts are uploaded and initiating completeMultipartUpload.
+ nilBucket := "dummy-bucket"
+ nilObject := ""
+
+ nilReq, err := newTestSignedRequestV4(http.MethodPost, getDeleteMultipleObjectsURL("", nilBucket), 0, nil, "", "", nil)
+ if err != nil {
+ t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
+ }
+ // execute the object layer set to `nil` test.
+ // `ExecObjectLayerAPINilTest` manages the operation.
+ ExecObjectLayerAPINilTest(t, nilBucket, nilObject, instanceType, apiRouter, nilReq)
+}
diff --git a/cmd/bucket-lifecycle-audit.go b/cmd/bucket-lifecycle-audit.go
new file mode 100644
index 00000000..1fa76e09
--- /dev/null
+++ b/cmd/bucket-lifecycle-audit.go
@@ -0,0 +1,93 @@
+// Copyright (c) 2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "strconv"
+
+ "github.com/minio/minio/internal/bucket/lifecycle"
+)
+
+//go:generate stringer -type lcEventSrc -trimprefix lcEventSrc_ $GOFILE
+type lcEventSrc uint8
+
+//nolint:staticcheck,revive // Underscores are used here to indicate where common prefix ends and the enumeration name begins
+const (
+ lcEventSrc_None lcEventSrc = iota
+ lcEventSrc_Heal
+ lcEventSrc_Scanner
+ lcEventSrc_Decom
+ lcEventSrc_Rebal
+ lcEventSrc_s3HeadObject
+ lcEventSrc_s3GetObject
+ lcEventSrc_s3ListObjects
+ lcEventSrc_s3PutObject
+ lcEventSrc_s3CopyObject
+ lcEventSrc_s3CompleteMultipartUpload
+)
+
+//revive:enable:var-naming
+type lcAuditEvent struct {
+ lifecycle.Event
+ source lcEventSrc
+}
+
+func (lae lcAuditEvent) Tags() map[string]string {
+ event := lae.Event
+ src := lae.source
+ const (
+ ilmSrc = "ilm-src"
+ ilmAction = "ilm-action"
+ ilmDue = "ilm-due"
+ ilmRuleID = "ilm-rule-id"
+ ilmTier = "ilm-tier"
+ ilmNewerNoncurrentVersions = "ilm-newer-noncurrent-versions"
+ ilmNoncurrentDays = "ilm-noncurrent-days"
+ )
+ tags := make(map[string]string, 5)
+ if src > lcEventSrc_None {
+ tags[ilmSrc] = src.String()
+ }
+ tags[ilmAction] = event.Action.String()
+ tags[ilmRuleID] = event.RuleID
+
+ if !event.Due.IsZero() {
+ tags[ilmDue] = event.Due.Format(iso8601Format)
+ }
+
+ // rule with Transition/NoncurrentVersionTransition in effect
+ if event.StorageClass != "" {
+ tags[ilmTier] = event.StorageClass
+ }
+
+ // rule with NewernoncurrentVersions in effect
+ if event.NewerNoncurrentVersions > 0 {
+ tags[ilmNewerNoncurrentVersions] = strconv.Itoa(event.NewerNoncurrentVersions)
+ }
+ if event.NoncurrentDays > 0 {
+ tags[ilmNoncurrentDays] = strconv.Itoa(event.NoncurrentDays)
+ }
+ return tags
+}
+
+func newLifecycleAuditEvent(src lcEventSrc, event lifecycle.Event) lcAuditEvent {
+ return lcAuditEvent{
+ Event: event,
+ source: src,
+ }
+}
diff --git a/cmd/bucket-lifecycle-handlers.go b/cmd/bucket-lifecycle-handlers.go
new file mode 100644
index 00000000..e917c9ad
--- /dev/null
+++ b/cmd/bucket-lifecycle-handlers.go
@@ -0,0 +1,230 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "encoding/xml"
+ "net/http"
+ "strconv"
+ "time"
+
+ "github.com/minio/minio/internal/bucket/lifecycle"
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/mux"
+ "github.com/minio/pkg/v3/policy"
+)
+
+const (
+ // Lifecycle configuration file.
+ bucketLifecycleConfig = "lifecycle.xml"
+)
+
+// PutBucketLifecycleHandler - This HTTP handler stores given bucket lifecycle configuration as per
+// https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lifecycle-mgmt.html
+func (api objectAPIHandlers) PutBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "PutBucketLifecycle")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ // PutBucketLifecycle always needs a Content-Md5
+ if !validateLengthAndChecksum(r) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL)
+ return
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketLifecycleAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Check if bucket exists.
+ rcfg, err := globalBucketObjectLockSys.Get(bucket)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ bucketLifecycle, err := lifecycle.ParseLifecycleConfigWithID(r.Body)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Validate the received bucket policy document
+ if err = bucketLifecycle.Validate(rcfg); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Validate the transition storage ARNs
+ if err = validateTransitionTier(bucketLifecycle); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Create a map of updated set of rules in request
+ updatedRules := make(map[string]lifecycle.Rule, len(bucketLifecycle.Rules))
+ for _, rule := range bucketLifecycle.Rules {
+ updatedRules[rule.ID] = rule
+ }
+
+ // Get list of rules for the bucket from disk
+ meta, err := globalBucketMetadataSys.GetConfigFromDisk(ctx, bucket)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ expiryRuleRemoved := false
+ if len(meta.LifecycleConfigXML) > 0 {
+ var lcCfg lifecycle.Lifecycle
+ if err := xml.Unmarshal(meta.LifecycleConfigXML, &lcCfg); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ for _, rl := range lcCfg.Rules {
+ updRule, ok := updatedRules[rl.ID]
+ // original rule had expiry that is no longer in the new config,
+ // or rule is present but missing expiration flags
+ if (!rl.Expiration.IsNull() || !rl.NoncurrentVersionExpiration.IsNull()) &&
+ (!ok || (updRule.Expiration.IsNull() && updRule.NoncurrentVersionExpiration.IsNull())) {
+ expiryRuleRemoved = true
+ }
+ }
+ }
+
+ if bucketLifecycle.HasExpiry() || expiryRuleRemoved {
+ currtime := time.Now()
+ bucketLifecycle.ExpiryUpdatedAt = &currtime
+ }
+
+ configData, err := xml.Marshal(bucketLifecycle)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketLifecycleConfig, configData); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Success.
+ writeSuccessResponseHeadersOnly(w)
+}
+
+// GetBucketLifecycleHandler - This HTTP handler returns bucket policy configuration.
+func (api objectAPIHandlers) GetBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetBucketLifecycle")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ var withUpdatedAt bool
+ if updatedAtStr := r.Form.Get("withUpdatedAt"); updatedAtStr != "" {
+ var err error
+ withUpdatedAt, err = strconv.ParseBool(updatedAtStr)
+ if err != nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidLifecycleQueryParameter), r.URL)
+ return
+ }
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketLifecycleAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Check if bucket exists.
+ if _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ config, updatedAt, err := globalBucketMetadataSys.GetLifecycleConfig(bucket)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ // explicitly set ExpiryUpdatedAt nil as its meant for internal consumption only
+ config.ExpiryUpdatedAt = nil
+
+ configData, err := xml.Marshal(config)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if withUpdatedAt {
+ w.Header().Set(xhttp.MinIOLifecycleCfgUpdatedAt, updatedAt.Format(iso8601Format))
+ }
+ // Write lifecycle configuration to client.
+ writeSuccessResponseXML(w, configData)
+}
+
+// DeleteBucketLifecycleHandler - This HTTP handler removes bucket lifecycle configuration.
+func (api objectAPIHandlers) DeleteBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "DeleteBucketLifecycle")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketLifecycleAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Check if bucket exists.
+ if _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if _, err := globalBucketMetadataSys.Delete(ctx, bucket, bucketLifecycleConfig); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Success.
+ writeSuccessNoContent(w)
+}
diff --git a/cmd/bucket-lifecycle-handlers_test.go b/cmd/bucket-lifecycle-handlers_test.go
new file mode 100644
index 00000000..7dc2cf02
--- /dev/null
+++ b/cmd/bucket-lifecycle-handlers_test.go
@@ -0,0 +1,305 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "encoding/xml"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/minio/minio/internal/auth"
+)
+
+// Test S3 Bucket lifecycle APIs with wrong credentials
+func TestBucketLifecycleWrongCredentials(t *testing.T) {
+ ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testBucketLifecycleHandlersWrongCredentials, endpoints: []string{"GetBucketLifecycle", "PutBucketLifecycle", "DeleteBucketLifecycle"}})
+}
+
+// Test for authentication
+func testBucketLifecycleHandlersWrongCredentials(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ credentials auth.Credentials, t *testing.T,
+) {
+ // test cases with sample input and expected output.
+ testCases := []struct {
+ method string
+ bucketName string
+ accessKey string
+ secretKey string
+ // Sent body
+ body []byte
+ // Expected response
+ expectedRespStatus int
+ lifecycleResponse []byte
+ errorResponse APIErrorResponse
+ shouldPass bool
+ }{
+ // GET empty credentials
+ {
+ method: http.MethodGet, bucketName: bucketName,
+ accessKey: "",
+ secretKey: "",
+ expectedRespStatus: http.StatusForbidden,
+ lifecycleResponse: []byte(""),
+ errorResponse: APIErrorResponse{
+ Resource: SlashSeparator + bucketName + SlashSeparator,
+ Code: "AccessDenied",
+ Message: "Access Denied.",
+ },
+ shouldPass: false,
+ },
+ // GET wrong credentials
+ {
+ method: http.MethodGet, bucketName: bucketName,
+ accessKey: "abcd",
+ secretKey: "abcd",
+ expectedRespStatus: http.StatusForbidden,
+ lifecycleResponse: []byte(""),
+ errorResponse: APIErrorResponse{
+ Resource: SlashSeparator + bucketName + SlashSeparator,
+ Code: "InvalidAccessKeyId",
+ Message: "The Access Key Id you provided does not exist in our records.",
+ },
+ shouldPass: false,
+ },
+ // PUT empty credentials
+ {
+ method: http.MethodPut,
+ bucketName: bucketName,
+ accessKey: "",
+ secretKey: "",
+ expectedRespStatus: http.StatusForbidden,
+ lifecycleResponse: []byte(""),
+ errorResponse: APIErrorResponse{
+ Resource: SlashSeparator + bucketName + SlashSeparator,
+ Code: "AccessDenied",
+ Message: "Access Denied.",
+ },
+ shouldPass: false,
+ },
+ // PUT wrong credentials
+ {
+ method: http.MethodPut,
+ bucketName: bucketName,
+ accessKey: "abcd",
+ secretKey: "abcd",
+ expectedRespStatus: http.StatusForbidden,
+ lifecycleResponse: []byte(""),
+ errorResponse: APIErrorResponse{
+ Resource: SlashSeparator + bucketName + SlashSeparator,
+ Code: "InvalidAccessKeyId",
+ Message: "The Access Key Id you provided does not exist in our records.",
+ },
+ shouldPass: false,
+ },
+ // DELETE empty credentials
+ {
+ method: http.MethodDelete,
+ bucketName: bucketName,
+ accessKey: "",
+ secretKey: "",
+ expectedRespStatus: http.StatusForbidden,
+ lifecycleResponse: []byte(""),
+ errorResponse: APIErrorResponse{
+ Resource: SlashSeparator + bucketName + SlashSeparator,
+ Code: "AccessDenied",
+ Message: "Access Denied.",
+ },
+ shouldPass: false,
+ },
+ // DELETE wrong credentials
+ {
+ method: http.MethodDelete,
+ bucketName: bucketName,
+ accessKey: "abcd",
+ secretKey: "abcd",
+ expectedRespStatus: http.StatusForbidden,
+ lifecycleResponse: []byte(""),
+ errorResponse: APIErrorResponse{
+ Resource: SlashSeparator + bucketName + SlashSeparator,
+ Code: "InvalidAccessKeyId",
+ Message: "The Access Key Id you provided does not exist in our records.",
+ },
+ shouldPass: false,
+ },
+ }
+
+ testBucketLifecycle(obj, instanceType, bucketName, apiRouter, t, testCases)
+}
+
+// Test S3 Bucket lifecycle APIs
+func TestBucketLifecycle(t *testing.T) {
+ ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testBucketLifecycleHandlers, endpoints: []string{"GetBucketLifecycle", "PutBucketLifecycle", "DeleteBucketLifecycle"}})
+}
+
+// Simple tests of bucket lifecycle: PUT, GET, DELETE.
+// Tests are related and the order is important.
+func testBucketLifecycleHandlers(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ creds auth.Credentials, t *testing.T,
+) {
+ // test cases with sample input and expected output.
+ testCases := []struct {
+ method string
+ bucketName string
+ accessKey string
+ secretKey string
+ // Sent body
+ body []byte
+ // Expected response
+ expectedRespStatus int
+ lifecycleResponse []byte
+ errorResponse APIErrorResponse
+ shouldPass bool
+ }{
+ // Test case - 1.
+ // Filter contains more than (Prefix,Tag,And) rule
+ {
+ method: http.MethodPut,
+ bucketName: bucketName,
+ accessKey: creds.AccessKey,
+ secretKey: creds.SecretKey,
+ body: []byte(`idlogs/Key1Value1Enabled365`),
+ expectedRespStatus: http.StatusBadRequest,
+ lifecycleResponse: []byte(``),
+ errorResponse: APIErrorResponse{
+ Resource: SlashSeparator + bucketName + SlashSeparator,
+ Code: "InvalidArgument",
+ Message: "Filter must have exactly one of Prefix, Tag, or And specified",
+ },
+
+ shouldPass: false,
+ },
+ // Date contains wrong format
+ {
+ method: http.MethodPut,
+ bucketName: bucketName,
+ accessKey: creds.AccessKey,
+ secretKey: creds.SecretKey,
+ body: []byte(`idlogs/Key1Value1Enabled365`),
+ expectedRespStatus: http.StatusBadRequest,
+ lifecycleResponse: []byte(``),
+ errorResponse: APIErrorResponse{
+ Resource: SlashSeparator + bucketName + SlashSeparator,
+ Code: "InvalidArgument",
+ Message: "Date must be provided in ISO 8601 format",
+ },
+
+ shouldPass: false,
+ },
+ {
+ method: http.MethodPut,
+ bucketName: bucketName,
+ accessKey: creds.AccessKey,
+ secretKey: creds.SecretKey,
+ body: []byte(`idlogs/Enabled365`),
+ expectedRespStatus: http.StatusOK,
+ lifecycleResponse: []byte(``),
+ errorResponse: APIErrorResponse{},
+ shouldPass: true,
+ },
+ {
+ method: http.MethodGet,
+ accessKey: creds.AccessKey,
+ secretKey: creds.SecretKey,
+ bucketName: bucketName,
+ body: []byte(``),
+ expectedRespStatus: http.StatusOK,
+ lifecycleResponse: []byte(`idEnabledlogs/365`),
+ errorResponse: APIErrorResponse{},
+ shouldPass: true,
+ },
+ {
+ method: http.MethodDelete,
+ accessKey: creds.AccessKey,
+ secretKey: creds.SecretKey,
+ bucketName: bucketName,
+ body: []byte(``),
+ expectedRespStatus: http.StatusNoContent,
+ lifecycleResponse: []byte(``),
+ errorResponse: APIErrorResponse{},
+ shouldPass: true,
+ },
+ {
+ method: http.MethodGet,
+ accessKey: creds.AccessKey,
+ secretKey: creds.SecretKey,
+ bucketName: bucketName,
+ body: []byte(``),
+ expectedRespStatus: http.StatusNotFound,
+ lifecycleResponse: []byte(``),
+ errorResponse: APIErrorResponse{
+ Resource: SlashSeparator + bucketName + SlashSeparator,
+ Code: "NoSuchLifecycleConfiguration",
+ Message: "The lifecycle configuration does not exist",
+ },
+ shouldPass: false,
+ },
+ }
+
+ testBucketLifecycle(obj, instanceType, bucketName, apiRouter, t, testCases)
+}
+
+// testBucketLifecycle is a generic testing of lifecycle requests
+func testBucketLifecycle(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ t *testing.T, testCases []struct {
+ method string
+ bucketName string
+ accessKey string
+ secretKey string
+ body []byte
+ expectedRespStatus int
+ lifecycleResponse []byte
+ errorResponse APIErrorResponse
+ shouldPass bool
+ },
+) {
+ for i, testCase := range testCases {
+ // initialize httptest Recorder, this records any mutations to response writer inside the handler.
+ rec := httptest.NewRecorder()
+ // construct HTTP request
+ req, err := newTestSignedRequestV4(testCase.method, getBucketLifecycleURL("", testCase.bucketName),
+ int64(len(testCase.body)), bytes.NewReader(testCase.body), testCase.accessKey, testCase.secretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed to create HTTP request for GetBucketLocationHandler: %v", i+1, instanceType, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler.
+ apiRouter.ServeHTTP(rec, req)
+ if rec.Code != testCase.expectedRespStatus {
+ t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, rec.Code)
+ }
+ if testCase.shouldPass && !bytes.Equal(testCase.lifecycleResponse, rec.Body.Bytes()) {
+ t.Errorf("Test %d: %s: Expected the response to be `%s`, but instead found `%s`", i+1, instanceType, string(testCase.lifecycleResponse), rec.Body.String())
+ }
+ errorResponse := APIErrorResponse{}
+ err = xml.Unmarshal(rec.Body.Bytes(), &errorResponse)
+ if err != nil && !testCase.shouldPass {
+ t.Fatalf("Test %d: %s: Unable to marshal response body %s", i+1, instanceType, rec.Body.String())
+ }
+ if errorResponse.Resource != testCase.errorResponse.Resource {
+ t.Errorf("Test %d: %s: Expected the error resource to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Resource, errorResponse.Resource)
+ }
+ if errorResponse.Message != testCase.errorResponse.Message {
+ t.Errorf("Test %d: %s: Expected the error message to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Message, errorResponse.Message)
+ }
+ if errorResponse.Code != testCase.errorResponse.Code {
+ t.Errorf("Test %d: %s: Expected the error code to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Code, errorResponse.Code)
+ }
+ }
+}
diff --git a/cmd/bucket-lifecycle.go b/cmd/bucket-lifecycle.go
new file mode 100644
index 00000000..24fdc67d
--- /dev/null
+++ b/cmd/bucket-lifecycle.go
@@ -0,0 +1,1126 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "io"
+ "maps"
+ "net/http"
+ "strconv"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio-go/v7/pkg/tags"
+ "github.com/minio/minio/internal/amztime"
+ sse "github.com/minio/minio/internal/bucket/encryption"
+ "github.com/minio/minio/internal/bucket/lifecycle"
+ "github.com/minio/minio/internal/event"
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/minio/internal/s3select"
+ xnet "github.com/minio/pkg/v3/net"
+ "github.com/zeebo/xxh3"
+)
+
+const (
+ // Disabled means the lifecycle rule is inactive
+ Disabled = "Disabled"
+ // TransitionStatus status of transition
+ TransitionStatus = "transition-status"
+ // TransitionedObjectName name of transitioned object
+ TransitionedObjectName = "transitioned-object"
+ // TransitionedVersionID is version of remote object
+ TransitionedVersionID = "transitioned-versionID"
+ // TransitionTier name of transition storage class
+ TransitionTier = "transition-tier"
+)
+
+// LifecycleSys - Bucket lifecycle subsystem.
+type LifecycleSys struct{}
+
+// Get - gets lifecycle config associated to a given bucket name.
+func (sys *LifecycleSys) Get(bucketName string) (lc *lifecycle.Lifecycle, err error) {
+ lc, _, err = globalBucketMetadataSys.GetLifecycleConfig(bucketName)
+ return lc, err
+}
+
+// NewLifecycleSys - creates new lifecycle system.
+func NewLifecycleSys() *LifecycleSys {
+ return &LifecycleSys{}
+}
+
+func ilmTrace(startTime time.Time, duration time.Duration, oi ObjectInfo, event string, metadata map[string]string, err string) madmin.TraceInfo {
+ sz, _ := oi.GetActualSize()
+ if metadata == nil {
+ metadata = make(map[string]string)
+ }
+ metadata["version-id"] = oi.VersionID
+ return madmin.TraceInfo{
+ TraceType: madmin.TraceILM,
+ Time: startTime,
+ NodeName: globalLocalNodeName,
+ FuncName: event,
+ Duration: duration,
+ Path: pathJoin(oi.Bucket, oi.Name),
+ Bytes: sz,
+ Error: err,
+ Message: getSource(4),
+ Custom: metadata,
+ }
+}
+
+func (sys *LifecycleSys) trace(oi ObjectInfo) func(event string, metadata map[string]string, err error) {
+ startTime := time.Now()
+ return func(event string, metadata map[string]string, err error) {
+ duration := time.Since(startTime)
+ if globalTrace.NumSubscribers(madmin.TraceILM) > 0 {
+ e := ""
+ if err != nil {
+ e = err.Error()
+ }
+ globalTrace.Publish(ilmTrace(startTime, duration, oi, event, metadata, e))
+ }
+ }
+}
+
+type expiryTask struct {
+ objInfo ObjectInfo
+ event lifecycle.Event
+ src lcEventSrc
+}
+
+// expiryStats records metrics related to ILM expiry activities
+type expiryStats struct {
+ missedExpiryTasks atomic.Int64
+ missedFreeVersTasks atomic.Int64
+ missedTierJournalTasks atomic.Int64
+ workers atomic.Int32
+}
+
+// MissedTasks returns the number of ILM expiry tasks that were missed since
+// there were no available workers.
+func (e *expiryStats) MissedTasks() int64 {
+ return e.missedExpiryTasks.Load()
+}
+
+// MissedFreeVersTasks returns the number of free version collection tasks that
+// were missed since there were no available workers.
+func (e *expiryStats) MissedFreeVersTasks() int64 {
+ return e.missedFreeVersTasks.Load()
+}
+
+// MissedTierJournalTasks returns the number of tasks to remove tiered objects
+// that were missed since there were no available workers.
+func (e *expiryStats) MissedTierJournalTasks() int64 {
+ return e.missedTierJournalTasks.Load()
+}
+
+// NumWorkers returns the number of active workers executing one of ILM expiry
+// tasks or free version collection tasks.
+func (e *expiryStats) NumWorkers() int32 {
+ return e.workers.Load()
+}
+
+type expiryOp interface {
+ OpHash() uint64
+}
+
+type freeVersionTask struct {
+ ObjectInfo
+}
+
+func (f freeVersionTask) OpHash() uint64 {
+ return xxh3.HashString(f.TransitionedObject.Tier + f.TransitionedObject.Name)
+}
+
+func (n noncurrentVersionsTask) OpHash() uint64 {
+ return xxh3.HashString(n.bucket + n.versions[0].ObjectName)
+}
+
+func (j jentry) OpHash() uint64 {
+ return xxh3.HashString(j.TierName + j.ObjName)
+}
+
+func (e expiryTask) OpHash() uint64 {
+ return xxh3.HashString(e.objInfo.Bucket + e.objInfo.Name)
+}
+
+// expiryState manages all ILM related expiration activities.
+type expiryState struct {
+ mu sync.RWMutex
+ workers atomic.Pointer[[]chan expiryOp]
+
+ ctx context.Context
+ objAPI ObjectLayer
+
+ stats expiryStats
+}
+
+// PendingTasks returns the number of pending ILM expiry tasks.
+func (es *expiryState) PendingTasks() int {
+ w := es.workers.Load()
+ if w == nil || len(*w) == 0 {
+ return 0
+ }
+ var tasks int
+ for _, wrkr := range *w {
+ tasks += len(wrkr)
+ }
+ return tasks
+}
+
+// enqueueTierJournalEntry enqueues a tier journal entry referring to a remote
+// object corresponding to a 'replaced' object versions. This applies only to
+// non-versioned or version suspended buckets.
+func (es *expiryState) enqueueTierJournalEntry(je jentry) {
+ wrkr := es.getWorkerCh(je.OpHash())
+ if wrkr == nil {
+ es.stats.missedTierJournalTasks.Add(1)
+ return
+ }
+ select {
+ case <-GlobalContext.Done():
+ case wrkr <- je:
+ default:
+ es.stats.missedTierJournalTasks.Add(1)
+ }
+}
+
+// enqueueFreeVersion enqueues a free version to be deleted
+func (es *expiryState) enqueueFreeVersion(oi ObjectInfo) {
+ task := freeVersionTask{ObjectInfo: oi}
+ wrkr := es.getWorkerCh(task.OpHash())
+ if wrkr == nil {
+ es.stats.missedFreeVersTasks.Add(1)
+ return
+ }
+ select {
+ case <-GlobalContext.Done():
+ case wrkr <- task:
+ default:
+ es.stats.missedFreeVersTasks.Add(1)
+ }
+}
+
+// enqueueByDays enqueues object versions expired by days for expiry.
+func (es *expiryState) enqueueByDays(oi ObjectInfo, event lifecycle.Event, src lcEventSrc) {
+ task := expiryTask{objInfo: oi, event: event, src: src}
+ wrkr := es.getWorkerCh(task.OpHash())
+ if wrkr == nil {
+ es.stats.missedExpiryTasks.Add(1)
+ return
+ }
+ select {
+ case <-GlobalContext.Done():
+ case wrkr <- task:
+ default:
+ es.stats.missedExpiryTasks.Add(1)
+ }
+}
+
+func (es *expiryState) enqueueNoncurrentVersions(bucket string, versions []ObjectToDelete, events []lifecycle.Event) {
+ if len(versions) == 0 {
+ return
+ }
+
+ task := noncurrentVersionsTask{
+ bucket: bucket,
+ versions: versions,
+ events: events,
+ }
+ wrkr := es.getWorkerCh(task.OpHash())
+ if wrkr == nil {
+ es.stats.missedExpiryTasks.Add(1)
+ return
+ }
+ select {
+ case <-GlobalContext.Done():
+ case wrkr <- task:
+ default:
+ es.stats.missedExpiryTasks.Add(1)
+ }
+}
+
+// globalExpiryState is the per-node instance which manages all ILM expiry tasks.
+var globalExpiryState *expiryState
+
+// newExpiryState creates an expiryState with buffered channels allocated for
+// each ILM expiry task type.
+func newExpiryState(ctx context.Context, objAPI ObjectLayer, n int) *expiryState {
+ es := &expiryState{
+ ctx: ctx,
+ objAPI: objAPI,
+ }
+ workers := make([]chan expiryOp, 0, n)
+ es.workers.Store(&workers)
+ es.ResizeWorkers(n)
+ return es
+}
+
+func (es *expiryState) getWorkerCh(h uint64) chan<- expiryOp {
+ w := es.workers.Load()
+ if w == nil || len(*w) == 0 {
+ return nil
+ }
+ workers := *w
+ return workers[h%uint64(len(workers))]
+}
+
+func (es *expiryState) ResizeWorkers(n int) {
+ if n == 0 {
+ n = 100
+ }
+
+ // Lock to avoid multiple resizes to happen at the same time.
+ es.mu.Lock()
+ defer es.mu.Unlock()
+ var workers []chan expiryOp
+ if v := es.workers.Load(); v != nil {
+ // Copy to new array.
+ workers = append(workers, *v...)
+ }
+
+ if n == len(workers) || n < 1 {
+ return
+ }
+
+ for len(workers) < n {
+ input := make(chan expiryOp, 10000)
+ workers = append(workers, input)
+ go es.Worker(input)
+ es.stats.workers.Add(1)
+ }
+
+ for len(workers) > n {
+ worker := workers[len(workers)-1]
+ workers = workers[:len(workers)-1]
+ worker <- expiryOp(nil)
+ es.stats.workers.Add(-1)
+ }
+ // Atomically replace workers.
+ es.workers.Store(&workers)
+}
+
+// Worker handles 4 types of expiration tasks.
+// 1. Expiry of objects, includes regular and transitioned objects
+// 2. Expiry of noncurrent versions due to NewerNoncurrentVersions
+// 3. Expiry of free-versions, for remote objects of transitioned object which have been expired since.
+// 4. Expiry of remote objects corresponding to objects in a
+// non-versioned/version suspended buckets
+func (es *expiryState) Worker(input <-chan expiryOp) {
+ for {
+ select {
+ case <-es.ctx.Done():
+ return
+ case v, ok := <-input:
+ if !ok {
+ return
+ }
+ if v == nil {
+ // ResizeWorkers signaling worker to quit
+ return
+ }
+ switch v := v.(type) {
+ case expiryTask:
+ if v.objInfo.TransitionedObject.Status != "" {
+ applyExpiryOnTransitionedObject(es.ctx, es.objAPI, v.objInfo, v.event, v.src)
+ } else {
+ applyExpiryOnNonTransitionedObjects(es.ctx, es.objAPI, v.objInfo, v.event, v.src)
+ }
+ case noncurrentVersionsTask:
+ deleteObjectVersions(es.ctx, es.objAPI, v.bucket, v.versions, v.events)
+ case jentry:
+ transitionLogIf(es.ctx, deleteObjectFromRemoteTier(es.ctx, v.ObjName, v.VersionID, v.TierName))
+ case freeVersionTask:
+ oi := v.ObjectInfo
+ traceFn := globalLifecycleSys.trace(oi)
+ if !oi.TransitionedObject.FreeVersion {
+ // nothing to be done
+ continue
+ }
+
+ ignoreNotFoundErr := func(err error) error {
+ switch {
+ case isErrVersionNotFound(err), isErrObjectNotFound(err):
+ return nil
+ }
+ return err
+ }
+ // Remove the remote object
+ err := deleteObjectFromRemoteTier(es.ctx, oi.TransitionedObject.Name, oi.TransitionedObject.VersionID, oi.TransitionedObject.Tier)
+ if ignoreNotFoundErr(err) != nil {
+ transitionLogIf(es.ctx, err)
+ traceFn(ILMFreeVersionDelete, nil, err)
+ continue
+ }
+
+ // Remove this free version
+ _, err = es.objAPI.DeleteObject(es.ctx, oi.Bucket, oi.Name, ObjectOptions{
+ VersionID: oi.VersionID,
+ InclFreeVersions: true,
+ })
+ if err == nil {
+ auditLogLifecycle(es.ctx, oi, ILMFreeVersionDelete, nil, traceFn)
+ }
+ if ignoreNotFoundErr(err) != nil {
+ transitionLogIf(es.ctx, err)
+ }
+ default:
+ bugLogIf(es.ctx, fmt.Errorf("Invalid work type - %v", v))
+ }
+ }
+ }
+}
+
+func initBackgroundExpiry(ctx context.Context, objectAPI ObjectLayer) {
+ globalExpiryState = newExpiryState(ctx, objectAPI, globalILMConfig.getExpirationWorkers())
+}
+
+type noncurrentVersionsTask struct {
+ bucket string
+ versions []ObjectToDelete
+ events []lifecycle.Event
+}
+
+type transitionTask struct {
+ objInfo ObjectInfo
+ src lcEventSrc
+ event lifecycle.Event
+}
+
+type transitionState struct {
+ transitionCh chan transitionTask
+
+ ctx context.Context
+ objAPI ObjectLayer
+ mu sync.Mutex
+ numWorkers int
+ killCh chan struct{}
+
+ activeTasks atomic.Int64
+ missedImmediateTasks atomic.Int64
+
+ lastDayMu sync.RWMutex
+ lastDayStats map[string]*lastDayTierStats
+}
+
+func (t *transitionState) queueTransitionTask(oi ObjectInfo, event lifecycle.Event, src lcEventSrc) {
+ task := transitionTask{objInfo: oi, event: event, src: src}
+ select {
+ case <-t.ctx.Done():
+ case t.transitionCh <- task:
+ default:
+ switch src {
+ case lcEventSrc_s3PutObject, lcEventSrc_s3CopyObject, lcEventSrc_s3CompleteMultipartUpload:
+ // Update missed immediate tasks only for incoming requests.
+ t.missedImmediateTasks.Add(1)
+ }
+ }
+}
+
+var globalTransitionState *transitionState
+
+// newTransitionState returns a transitionState object ready to be initialized
+// via its Init method.
+func newTransitionState(ctx context.Context) *transitionState {
+ return &transitionState{
+ transitionCh: make(chan transitionTask, 100000),
+ ctx: ctx,
+ killCh: make(chan struct{}),
+ lastDayStats: make(map[string]*lastDayTierStats),
+ }
+}
+
+// Init initializes t with given objAPI and instantiates the configured number
+// of transition workers.
+func (t *transitionState) Init(objAPI ObjectLayer) {
+ n := globalAPIConfig.getTransitionWorkers()
+ // Prefer ilm.transition_workers over now deprecated api.transition_workers
+ if tw := globalILMConfig.getTransitionWorkers(); tw > 0 {
+ n = tw
+ }
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ t.objAPI = objAPI
+ t.updateWorkers(n)
+}
+
+// PendingTasks returns the number of ILM transition tasks waiting for a worker
+// goroutine.
+func (t *transitionState) PendingTasks() int {
+ return len(t.transitionCh)
+}
+
+// ActiveTasks returns the number of active (ongoing) ILM transition tasks.
+func (t *transitionState) ActiveTasks() int64 {
+ return t.activeTasks.Load()
+}
+
+// MissedImmediateTasks returns the number of tasks - deferred to scanner due
+// to tasks channel being backlogged.
+func (t *transitionState) MissedImmediateTasks() int64 {
+ return t.missedImmediateTasks.Load()
+}
+
+// worker waits for transition tasks
+func (t *transitionState) worker(objectAPI ObjectLayer) {
+ for {
+ select {
+ case <-t.killCh:
+ return
+ case <-t.ctx.Done():
+ return
+ case task, ok := <-t.transitionCh:
+ if !ok {
+ return
+ }
+ t.activeTasks.Add(1)
+ if err := transitionObject(t.ctx, objectAPI, task.objInfo, newLifecycleAuditEvent(task.src, task.event)); err != nil {
+ if !isErrVersionNotFound(err) && !isErrObjectNotFound(err) && !xnet.IsNetworkOrHostDown(err, false) {
+ if !strings.Contains(err.Error(), "use of closed network connection") {
+ transitionLogIf(t.ctx, fmt.Errorf("Transition to %s failed for %s/%s version:%s with %w",
+ task.event.StorageClass, task.objInfo.Bucket, task.objInfo.Name, task.objInfo.VersionID, err))
+ }
+ }
+ } else {
+ ts := tierStats{
+ TotalSize: uint64(task.objInfo.Size),
+ NumVersions: 1,
+ }
+ if task.objInfo.IsLatest {
+ ts.NumObjects = 1
+ }
+ t.addLastDayStats(task.event.StorageClass, ts)
+ }
+ t.activeTasks.Add(-1)
+ }
+ }
+}
+
+func (t *transitionState) addLastDayStats(tier string, ts tierStats) {
+ t.lastDayMu.Lock()
+ defer t.lastDayMu.Unlock()
+
+ if _, ok := t.lastDayStats[tier]; !ok {
+ t.lastDayStats[tier] = &lastDayTierStats{}
+ }
+ t.lastDayStats[tier].addStats(ts)
+}
+
+func (t *transitionState) getDailyAllTierStats() DailyAllTierStats {
+ t.lastDayMu.RLock()
+ defer t.lastDayMu.RUnlock()
+
+ res := make(DailyAllTierStats, len(t.lastDayStats))
+ for tier, st := range t.lastDayStats {
+ res[tier] = st.clone()
+ }
+ return res
+}
+
+// UpdateWorkers at the end of this function leaves n goroutines waiting for
+// transition tasks
+func (t *transitionState) UpdateWorkers(n int) {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+ if t.objAPI == nil { // Init hasn't been called yet.
+ return
+ }
+ t.updateWorkers(n)
+}
+
+func (t *transitionState) updateWorkers(n int) {
+ if n == 0 {
+ n = 100
+ }
+
+ for t.numWorkers < n {
+ go t.worker(t.objAPI)
+ t.numWorkers++
+ }
+
+ for t.numWorkers > n {
+ go func() { t.killCh <- struct{}{} }()
+ t.numWorkers--
+ }
+}
+
+var errInvalidStorageClass = errors.New("invalid storage class")
+
+func validateTransitionTier(lc *lifecycle.Lifecycle) error {
+ for _, rule := range lc.Rules {
+ if rule.Transition.StorageClass != "" {
+ if valid := globalTierConfigMgr.IsTierValid(rule.Transition.StorageClass); !valid {
+ return errInvalidStorageClass
+ }
+ }
+ if rule.NoncurrentVersionTransition.StorageClass != "" {
+ if valid := globalTierConfigMgr.IsTierValid(rule.NoncurrentVersionTransition.StorageClass); !valid {
+ return errInvalidStorageClass
+ }
+ }
+ }
+ return nil
+}
+
+// enqueueTransitionImmediate enqueues obj for transition if eligible.
+// This is to be called after a successful upload of an object (version).
+func enqueueTransitionImmediate(obj ObjectInfo, src lcEventSrc) {
+ if lc, err := globalLifecycleSys.Get(obj.Bucket); err == nil {
+ switch event := lc.Eval(obj.ToLifecycleOpts()); event.Action {
+ case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
+ if obj.DeleteMarker || obj.IsDir {
+ // nothing to transition
+ return
+ }
+ globalTransitionState.queueTransitionTask(obj, event, src)
+ }
+ }
+}
+
+// expireTransitionedObject handles expiry of transitioned/restored objects
+// (versions) in one of the following situations:
+//
+// 1. when a restored (via PostRestoreObject API) object expires.
+// 2. when a transitioned object expires (based on an ILM rule).
+func expireTransitionedObject(ctx context.Context, objectAPI ObjectLayer, oi *ObjectInfo, lcEvent lifecycle.Event, src lcEventSrc) error {
+ traceFn := globalLifecycleSys.trace(*oi)
+ opts := ObjectOptions{
+ Versioned: globalBucketVersioningSys.PrefixEnabled(oi.Bucket, oi.Name),
+ Expiration: ExpirationOptions{Expire: true},
+ }
+ if lcEvent.Action.DeleteVersioned() {
+ opts.VersionID = oi.VersionID
+ }
+ tags := newLifecycleAuditEvent(src, lcEvent).Tags()
+ if lcEvent.Action.DeleteRestored() {
+ // delete locally restored copy of object or object version
+ // from the source, while leaving metadata behind. The data on
+ // transitioned tier lies untouched and still accessible
+ opts.Transition.ExpireRestored = true
+ _, err := objectAPI.DeleteObject(ctx, oi.Bucket, oi.Name, opts)
+ if err == nil {
+ // TODO consider including expiry of restored object to events we
+ // notify.
+ auditLogLifecycle(ctx, *oi, ILMExpiry, tags, traceFn)
+ }
+ return err
+ }
+
+ // Delete remote object from warm-tier
+ err := deleteObjectFromRemoteTier(ctx, oi.TransitionedObject.Name, oi.TransitionedObject.VersionID, oi.TransitionedObject.Tier)
+ if err == nil {
+ // Skip adding free version since we successfully deleted the
+ // remote object
+ opts.SkipFreeVersion = true
+ } else {
+ transitionLogIf(ctx, err)
+ }
+
+ // Now, delete object from hot-tier namespace
+ if _, err := objectAPI.DeleteObject(ctx, oi.Bucket, oi.Name, opts); err != nil {
+ return err
+ }
+
+ // Send audit for the lifecycle delete operation
+ defer auditLogLifecycle(ctx, *oi, ILMExpiry, tags, traceFn)
+
+ eventName := event.ObjectRemovedDelete
+ if oi.DeleteMarker {
+ eventName = event.ObjectRemovedDeleteMarkerCreated
+ }
+ objInfo := ObjectInfo{
+ Name: oi.Name,
+ VersionID: oi.VersionID,
+ DeleteMarker: oi.DeleteMarker,
+ }
+ // Notify object deleted event.
+ sendEvent(eventArgs{
+ EventName: eventName,
+ BucketName: oi.Bucket,
+ Object: objInfo,
+ UserAgent: "Internal: [ILM-Expiry]",
+ Host: globalLocalNodeName,
+ })
+
+ return nil
+}
+
+// generate an object name for transitioned object
+func genTransitionObjName(bucket string) (string, error) {
+ u, err := uuid.NewRandom()
+ if err != nil {
+ return "", err
+ }
+ us := u.String()
+ hash := xxh3.HashString(pathJoin(globalDeploymentID(), bucket))
+ obj := fmt.Sprintf("%s/%s/%s/%s", strconv.FormatUint(hash, 16), us[0:2], us[2:4], us)
+ return obj, nil
+}
+
+// transition object to target specified by the transition ARN. When an object is transitioned to another
+// storage specified by the transition ARN, the metadata is left behind on source cluster and original content
+// is moved to the transition tier. Note that in the case of encrypted objects, entire encrypted stream is moved
+// to the transition tier without decrypting or re-encrypting.
+func transitionObject(ctx context.Context, objectAPI ObjectLayer, oi ObjectInfo, lae lcAuditEvent) (err error) {
+ timeILM := globalScannerMetrics.timeILM(lae.Action)
+ defer func() {
+ if err != nil {
+ return
+ }
+ timeILM(1)
+ }()
+
+ opts := ObjectOptions{
+ Transition: TransitionOptions{
+ Status: lifecycle.TransitionPending,
+ Tier: lae.StorageClass,
+ ETag: oi.ETag,
+ },
+ LifecycleAuditEvent: lae,
+ VersionID: oi.VersionID,
+ Versioned: globalBucketVersioningSys.PrefixEnabled(oi.Bucket, oi.Name),
+ VersionSuspended: globalBucketVersioningSys.PrefixSuspended(oi.Bucket, oi.Name),
+ MTime: oi.ModTime,
+ }
+ return objectAPI.TransitionObject(ctx, oi.Bucket, oi.Name, opts)
+}
+
+type auditTierOp struct {
+ Tier string `json:"tier"`
+ TimeToResponseNS int64 `json:"timeToResponseNS"`
+ OutputBytes int64 `json:"tx,omitempty"`
+ Error string `json:"error,omitempty"`
+}
+
+func (op auditTierOp) String() string {
+ // flattening the auditTierOp{} for audit
+ return fmt.Sprintf("tier:%s,respNS:%d,tx:%d,err:%s", op.Tier, op.TimeToResponseNS, op.OutputBytes, op.Error)
+}
+
+func auditTierActions(ctx context.Context, tier string, bytes int64) func(err error) {
+ startTime := time.Now()
+ return func(err error) {
+ // Record only when audit targets configured.
+ if len(logger.AuditTargets()) == 0 {
+ return
+ }
+
+ op := auditTierOp{
+ Tier: tier,
+ OutputBytes: bytes,
+ }
+
+ if err == nil {
+ since := time.Since(startTime)
+ op.TimeToResponseNS = since.Nanoseconds()
+ globalTierMetrics.Observe(tier, since)
+ globalTierMetrics.logSuccess(tier)
+ } else {
+ op.Error = err.Error()
+ globalTierMetrics.logFailure(tier)
+ }
+
+ logger.GetReqInfo(ctx).AppendTags("tierStats", op.String())
+ }
+}
+
+// getTransitionedObjectReader returns a reader from the transitioned tier.
+func getTransitionedObjectReader(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, oi ObjectInfo, opts ObjectOptions) (gr *GetObjectReader, err error) {
+ tgtClient, err := globalTierConfigMgr.getDriver(ctx, oi.TransitionedObject.Tier)
+ if err != nil {
+ return nil, fmt.Errorf("transition storage class not configured: %w", err)
+ }
+
+ fn, off, length, err := NewGetObjectReader(rs, oi, opts, h)
+ if err != nil {
+ return nil, ErrorRespToObjectError(err, bucket, object)
+ }
+ gopts := WarmBackendGetOpts{}
+
+ // get correct offsets for object
+ if off >= 0 && length >= 0 {
+ gopts.startOffset = off
+ gopts.length = length
+ }
+
+ timeTierAction := auditTierActions(ctx, oi.TransitionedObject.Tier, length)
+ reader, err := tgtClient.Get(ctx, oi.TransitionedObject.Name, remoteVersionID(oi.TransitionedObject.VersionID), gopts)
+ if err != nil {
+ return nil, err
+ }
+ closer := func() {
+ timeTierAction(reader.Close())
+ }
+ return fn(reader, h, closer)
+}
+
+// RestoreRequestType represents type of restore.
+type RestoreRequestType string
+
+const (
+ // SelectRestoreRequest specifies select request. This is the only valid value
+ SelectRestoreRequest RestoreRequestType = "SELECT"
+)
+
+// Encryption specifies encryption setting on restored bucket
+type Encryption struct {
+ EncryptionType sse.Algorithm `xml:"EncryptionType"`
+ KMSContext string `xml:"KMSContext,omitempty"`
+ KMSKeyID string `xml:"KMSKeyId,omitempty"`
+}
+
+// MetadataEntry denotes name and value.
+type MetadataEntry struct {
+ Name string `xml:"Name"`
+ Value string `xml:"Value"`
+}
+
+// S3Location specifies s3 location that receives result of a restore object request
+type S3Location struct {
+ BucketName string `xml:"BucketName,omitempty"`
+ Encryption Encryption `xml:"Encryption,omitempty"`
+ Prefix string `xml:"Prefix,omitempty"`
+ StorageClass string `xml:"StorageClass,omitempty"`
+ Tagging *tags.Tags `xml:"Tagging,omitempty"`
+ UserMetadata []MetadataEntry `xml:"UserMetadata"`
+}
+
+// OutputLocation specifies bucket where object needs to be restored
+type OutputLocation struct {
+ S3 S3Location `xml:"S3,omitempty"`
+}
+
+// IsEmpty returns true if output location not specified.
+func (o *OutputLocation) IsEmpty() bool {
+ return o.S3.BucketName == ""
+}
+
+// SelectParameters specifies sql select parameters
+type SelectParameters struct {
+ s3select.S3Select
+}
+
+// IsEmpty returns true if no select parameters set
+func (sp *SelectParameters) IsEmpty() bool {
+ return sp == nil
+}
+
+var selectParamsXMLName = "SelectParameters"
+
+// UnmarshalXML - decodes XML data.
+func (sp *SelectParameters) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+ // Essentially the same as S3Select barring the xml name.
+ if start.Name.Local == selectParamsXMLName {
+ start.Name = xml.Name{Space: "", Local: "SelectRequest"}
+ }
+ return sp.S3Select.UnmarshalXML(d, start)
+}
+
+// RestoreObjectRequest - xml to restore a transitioned object
+type RestoreObjectRequest struct {
+ XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ RestoreRequest" json:"-"`
+ Days int `xml:"Days,omitempty"`
+ Type RestoreRequestType `xml:"Type,omitempty"`
+ Tier string `xml:"Tier"`
+ Description string `xml:"Description,omitempty"`
+ SelectParameters *SelectParameters `xml:"SelectParameters,omitempty"`
+ OutputLocation OutputLocation `xml:"OutputLocation,omitempty"`
+}
+
+// Maximum 2MiB size per restore object request.
+const maxRestoreObjectRequestSize = 2 << 20
+
+// parseRestoreRequest parses RestoreObjectRequest from xml
+func parseRestoreRequest(reader io.Reader) (*RestoreObjectRequest, error) {
+ req := RestoreObjectRequest{}
+ if err := xml.NewDecoder(io.LimitReader(reader, maxRestoreObjectRequestSize)).Decode(&req); err != nil {
+ return nil, err
+ }
+ return &req, nil
+}
+
+// validate a RestoreObjectRequest as per AWS S3 spec https://docs.aws.amazon.com/AmazonS3/latest/API/API_RestoreObject.html
+func (r *RestoreObjectRequest) validate(ctx context.Context, objAPI ObjectLayer) error {
+ if r.Type != SelectRestoreRequest && !r.SelectParameters.IsEmpty() {
+ return fmt.Errorf("Select parameters can only be specified with SELECT request type")
+ }
+ if r.Type == SelectRestoreRequest && r.SelectParameters.IsEmpty() {
+ return fmt.Errorf("SELECT restore request requires select parameters to be specified")
+ }
+
+ if r.Type != SelectRestoreRequest && !r.OutputLocation.IsEmpty() {
+ return fmt.Errorf("OutputLocation required only for SELECT request type")
+ }
+ if r.Type == SelectRestoreRequest && r.OutputLocation.IsEmpty() {
+ return fmt.Errorf("OutputLocation required for SELECT requests")
+ }
+
+ if r.Days != 0 && r.Type == SelectRestoreRequest {
+ return fmt.Errorf("Days cannot be specified with SELECT restore request")
+ }
+ if r.Days == 0 && r.Type != SelectRestoreRequest {
+ return fmt.Errorf("restoration days should be at least 1")
+ }
+ // Check if bucket exists.
+ if !r.OutputLocation.IsEmpty() {
+ if _, err := objAPI.GetBucketInfo(ctx, r.OutputLocation.S3.BucketName, BucketOptions{}); err != nil {
+ return err
+ }
+ if r.OutputLocation.S3.Prefix == "" {
+ return fmt.Errorf("Prefix is a required parameter in OutputLocation")
+ }
+ if r.OutputLocation.S3.Encryption.EncryptionType != xhttp.AmzEncryptionAES {
+ return NotImplemented{}
+ }
+ }
+ return nil
+}
+
+// postRestoreOpts returns ObjectOptions with version-id from the POST restore object request for a given bucket and object.
+func postRestoreOpts(ctx context.Context, r *http.Request, bucket, object string) (opts ObjectOptions, err error) {
+ versioned := globalBucketVersioningSys.PrefixEnabled(bucket, object)
+ versionSuspended := globalBucketVersioningSys.PrefixSuspended(bucket, object)
+ vid := strings.TrimSpace(r.Form.Get(xhttp.VersionID))
+ if vid != "" && vid != nullVersionID {
+ _, err := uuid.Parse(vid)
+ if err != nil {
+ s3LogIf(ctx, err)
+ return opts, InvalidVersionID{
+ Bucket: bucket,
+ Object: object,
+ VersionID: vid,
+ }
+ }
+ if !versioned && !versionSuspended {
+ return opts, InvalidArgument{
+ Bucket: bucket,
+ Object: object,
+ Err: fmt.Errorf("version-id specified %s but versioning is not enabled on %s", opts.VersionID, bucket),
+ }
+ }
+ }
+ return ObjectOptions{
+ Versioned: versioned,
+ VersionSuspended: versionSuspended,
+ VersionID: vid,
+ }, nil
+}
+
+// set ObjectOptions for PUT call to restore temporary copy of transitioned data
+func putRestoreOpts(bucket, object string, rreq *RestoreObjectRequest, objInfo ObjectInfo) (putOpts ObjectOptions) {
+ meta := make(map[string]string)
+ sc := rreq.OutputLocation.S3.StorageClass
+ if sc == "" {
+ sc = objInfo.StorageClass
+ }
+ meta[strings.ToLower(xhttp.AmzStorageClass)] = sc
+
+ if rreq.Type == SelectRestoreRequest {
+ for _, v := range rreq.OutputLocation.S3.UserMetadata {
+ if !stringsHasPrefixFold(v.Name, "x-amz-meta") {
+ meta["x-amz-meta-"+v.Name] = v.Value
+ continue
+ }
+ meta[v.Name] = v.Value
+ }
+ if tags := rreq.OutputLocation.S3.Tagging.String(); tags != "" {
+ meta[xhttp.AmzObjectTagging] = tags
+ }
+ if rreq.OutputLocation.S3.Encryption.EncryptionType != "" {
+ meta[xhttp.AmzServerSideEncryption] = xhttp.AmzEncryptionAES
+ }
+ return ObjectOptions{
+ Versioned: globalBucketVersioningSys.PrefixEnabled(bucket, object),
+ VersionSuspended: globalBucketVersioningSys.PrefixSuspended(bucket, object),
+ UserDefined: meta,
+ }
+ }
+ maps.Copy(meta, objInfo.UserDefined)
+ if len(objInfo.UserTags) != 0 {
+ meta[xhttp.AmzObjectTagging] = objInfo.UserTags
+ }
+ // Set restore object status
+ restoreExpiry := lifecycle.ExpectedExpiryTime(time.Now().UTC(), rreq.Days)
+ meta[xhttp.AmzRestore] = completedRestoreObj(restoreExpiry).String()
+ return ObjectOptions{
+ Versioned: globalBucketVersioningSys.PrefixEnabled(bucket, object),
+ VersionSuspended: globalBucketVersioningSys.PrefixSuspended(bucket, object),
+ UserDefined: meta,
+ VersionID: objInfo.VersionID,
+ MTime: objInfo.ModTime,
+ Expires: objInfo.Expires,
+ }
+}
+
+var errRestoreHDRMalformed = fmt.Errorf("x-amz-restore header malformed")
+
+// IsRemote returns true if this object version's contents are in its remote
+// tier.
+func (fi FileInfo) IsRemote() bool {
+ if fi.TransitionStatus != lifecycle.TransitionComplete {
+ return false
+ }
+ return !isRestoredObjectOnDisk(fi.Metadata)
+}
+
+// IsRemote returns true if this object version's contents are in its remote
+// tier.
+func (oi ObjectInfo) IsRemote() bool {
+ if oi.TransitionedObject.Status != lifecycle.TransitionComplete {
+ return false
+ }
+ return !isRestoredObjectOnDisk(oi.UserDefined)
+}
+
+// restoreObjStatus represents a restore-object's status. It can be either
+// ongoing or completed.
+type restoreObjStatus struct {
+ ongoing bool
+ expiry time.Time
+}
+
+// ongoingRestoreObj constructs restoreObjStatus for an ongoing restore-object.
+func ongoingRestoreObj() restoreObjStatus {
+ return restoreObjStatus{
+ ongoing: true,
+ }
+}
+
+// completedRestoreObj constructs restoreObjStatus for a completed restore-object with given expiry.
+func completedRestoreObj(expiry time.Time) restoreObjStatus {
+ return restoreObjStatus{
+ ongoing: false,
+ expiry: expiry.UTC(),
+ }
+}
+
+// String returns x-amz-restore compatible representation of r.
+func (r restoreObjStatus) String() string {
+ if r.Ongoing() {
+ return `ongoing-request="true"`
+ }
+ return fmt.Sprintf(`ongoing-request="false", expiry-date="%s"`, r.expiry.Format(http.TimeFormat))
+}
+
+// Expiry returns expiry of restored object and true if restore-object has completed.
+// Otherwise returns zero value of time.Time and false.
+func (r restoreObjStatus) Expiry() (time.Time, bool) {
+ if r.Ongoing() {
+ return time.Time{}, false
+ }
+ return r.expiry, true
+}
+
+// Ongoing returns true if restore-object is ongoing.
+func (r restoreObjStatus) Ongoing() bool {
+ return r.ongoing
+}
+
+// OnDisk returns true if restored object contents exist in MinIO. Otherwise returns false.
+// The restore operation could be in one of the following states,
+// - in progress (no content on MinIO's disks yet)
+// - completed
+// - completed but expired (again, no content on MinIO's disks)
+func (r restoreObjStatus) OnDisk() bool {
+ if expiry, ok := r.Expiry(); ok && time.Now().UTC().Before(expiry) {
+ // completed
+ return true
+ }
+ return false // in progress or completed but expired
+}
+
+// parseRestoreObjStatus parses restoreHdr from AmzRestore header. If the value is valid it returns a
+// restoreObjStatus value with the status and expiry (if any). Otherwise returns
+// the empty value and an error indicating the parse failure.
+func parseRestoreObjStatus(restoreHdr string) (restoreObjStatus, error) {
+ tokens := strings.SplitN(restoreHdr, ",", 2)
+ progressTokens := strings.SplitN(tokens[0], "=", 2)
+ if len(progressTokens) != 2 {
+ return restoreObjStatus{}, errRestoreHDRMalformed
+ }
+ if strings.TrimSpace(progressTokens[0]) != "ongoing-request" {
+ return restoreObjStatus{}, errRestoreHDRMalformed
+ }
+
+ switch progressTokens[1] {
+ case "true", `"true"`: // true without double quotes is deprecated in Feb 2022
+ if len(tokens) == 1 {
+ return ongoingRestoreObj(), nil
+ }
+ case "false", `"false"`: // false without double quotes is deprecated in Feb 2022
+ if len(tokens) != 2 {
+ return restoreObjStatus{}, errRestoreHDRMalformed
+ }
+ expiryTokens := strings.SplitN(tokens[1], "=", 2)
+ if len(expiryTokens) != 2 {
+ return restoreObjStatus{}, errRestoreHDRMalformed
+ }
+ if strings.TrimSpace(expiryTokens[0]) != "expiry-date" {
+ return restoreObjStatus{}, errRestoreHDRMalformed
+ }
+ expiry, err := amztime.ParseHeader(strings.Trim(expiryTokens[1], `"`))
+ if err != nil {
+ return restoreObjStatus{}, errRestoreHDRMalformed
+ }
+ return completedRestoreObj(expiry), nil
+ }
+ return restoreObjStatus{}, errRestoreHDRMalformed
+}
+
+// isRestoredObjectOnDisk returns true if the restored object is on disk. Note
+// this function must be called only if object version's transition status is
+// complete.
+func isRestoredObjectOnDisk(meta map[string]string) (onDisk bool) {
+ if restoreHdr, ok := meta[xhttp.AmzRestore]; ok {
+ if restoreStatus, err := parseRestoreObjStatus(restoreHdr); err == nil {
+ return restoreStatus.OnDisk()
+ }
+ }
+ return onDisk
+}
+
+// ToLifecycleOpts returns lifecycle.ObjectOpts value for oi.
+func (oi ObjectInfo) ToLifecycleOpts() lifecycle.ObjectOpts {
+ return lifecycle.ObjectOpts{
+ Name: oi.Name,
+ UserTags: oi.UserTags,
+ VersionID: oi.VersionID,
+ ModTime: oi.ModTime,
+ Size: oi.Size,
+ IsLatest: oi.IsLatest,
+ NumVersions: oi.NumVersions,
+ DeleteMarker: oi.DeleteMarker,
+ SuccessorModTime: oi.SuccessorModTime,
+ RestoreOngoing: oi.RestoreOngoing,
+ RestoreExpires: oi.RestoreExpires,
+ TransitionStatus: oi.TransitionedObject.Status,
+ UserDefined: oi.UserDefined,
+ VersionPurgeStatus: oi.VersionPurgeStatus,
+ ReplicationStatus: oi.ReplicationStatus,
+ }
+}
diff --git a/cmd/bucket-lifecycle_test.go b/cmd/bucket-lifecycle_test.go
new file mode 100644
index 00000000..a37e3492
--- /dev/null
+++ b/cmd/bucket-lifecycle_test.go
@@ -0,0 +1,247 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/minio/minio/internal/bucket/lifecycle"
+ xhttp "github.com/minio/minio/internal/http"
+)
+
+// TestParseRestoreObjStatus tests parseRestoreObjStatus
+func TestParseRestoreObjStatus(t *testing.T) {
+ testCases := []struct {
+ restoreHdr string
+ expectedStatus restoreObjStatus
+ expectedErr error
+ }{
+ {
+ // valid: represents a restored object, 'pending' expiry.
+ restoreHdr: `ongoing-request="false", expiry-date="Fri, 21 Dec 2012 00:00:00 GMT"`,
+ expectedStatus: restoreObjStatus{
+ ongoing: false,
+ expiry: time.Date(2012, 12, 21, 0, 0, 0, 0, time.UTC),
+ },
+ expectedErr: nil,
+ },
+ {
+ // valid: represents an ongoing restore object request.
+ restoreHdr: `ongoing-request="true"`,
+ expectedStatus: restoreObjStatus{
+ ongoing: true,
+ },
+ expectedErr: nil,
+ },
+ {
+ // invalid; ongoing restore object request can't have expiry set on it.
+ restoreHdr: `ongoing-request="true", expiry-date="Fri, 21 Dec 2012 00:00:00 GMT"`,
+ expectedStatus: restoreObjStatus{},
+ expectedErr: errRestoreHDRMalformed,
+ },
+ {
+ // invalid; completed restore object request must have expiry set on it.
+ restoreHdr: `ongoing-request="false"`,
+ expectedStatus: restoreObjStatus{},
+ expectedErr: errRestoreHDRMalformed,
+ },
+ }
+ for i, tc := range testCases {
+ actual, err := parseRestoreObjStatus(tc.restoreHdr)
+ if err != tc.expectedErr {
+ t.Fatalf("Test %d: got %v expected %v", i+1, err, tc.expectedErr)
+ }
+ if actual != tc.expectedStatus {
+ t.Fatalf("Test %d: got %v expected %v", i+1, actual, tc.expectedStatus)
+ }
+ }
+}
+
+// TestRestoreObjStatusRoundTrip restoreObjStatus roundtrip
+func TestRestoreObjStatusRoundTrip(t *testing.T) {
+ testCases := []restoreObjStatus{
+ ongoingRestoreObj(),
+ completedRestoreObj(time.Now().UTC()),
+ }
+ for i, tc := range testCases {
+ actual, err := parseRestoreObjStatus(tc.String())
+ if err != nil {
+ t.Fatalf("Test %d: parse restore object failed: %v", i+1, err)
+ }
+ if actual.ongoing != tc.ongoing || actual.expiry.Format(http.TimeFormat) != tc.expiry.Format(http.TimeFormat) {
+ t.Fatalf("Test %d: got %v expected %v", i+1, actual, tc)
+ }
+ }
+}
+
+// TestRestoreObjOnDisk tests restoreObjStatus' OnDisk method
+func TestRestoreObjOnDisk(t *testing.T) {
+ testCases := []struct {
+ restoreStatus restoreObjStatus
+ ondisk bool
+ }{
+ {
+ // restore in progress
+ restoreStatus: ongoingRestoreObj(),
+ ondisk: false,
+ },
+ {
+ // restore completed but expired
+ restoreStatus: completedRestoreObj(time.Now().Add(-time.Hour)),
+ ondisk: false,
+ },
+ {
+ // restore completed
+ restoreStatus: completedRestoreObj(time.Now().Add(time.Hour)),
+ ondisk: true,
+ },
+ }
+
+ for i, tc := range testCases {
+ if actual := tc.restoreStatus.OnDisk(); actual != tc.ondisk {
+ t.Fatalf("Test %d: expected %v but got %v", i+1, tc.ondisk, actual)
+ }
+ }
+}
+
+// TestIsRestoredObjectOnDisk tests isRestoredObjectOnDisk helper function
+func TestIsRestoredObjectOnDisk(t *testing.T) {
+ testCases := []struct {
+ meta map[string]string
+ ondisk bool
+ }{
+ {
+ // restore in progress
+ meta: map[string]string{
+ xhttp.AmzRestore: ongoingRestoreObj().String(),
+ },
+ ondisk: false,
+ },
+ {
+ // restore completed
+ meta: map[string]string{
+ xhttp.AmzRestore: completedRestoreObj(time.Now().Add(time.Hour)).String(),
+ },
+ ondisk: true,
+ },
+ {
+ // restore completed but expired
+ meta: map[string]string{
+ xhttp.AmzRestore: completedRestoreObj(time.Now().Add(-time.Hour)).String(),
+ },
+ ondisk: false,
+ },
+ }
+
+ for i, tc := range testCases {
+ if actual := isRestoredObjectOnDisk(tc.meta); actual != tc.ondisk {
+ t.Fatalf("Test %d: expected %v but got %v for %v", i+1, tc.ondisk, actual, tc.meta)
+ }
+ }
+}
+
+func TestObjectIsRemote(t *testing.T) {
+ fi := newFileInfo("object", 8, 8)
+ fi.Erasure.Index = 1
+ if !fi.IsValid() {
+ t.Fatalf("unable to get xl meta")
+ }
+
+ testCases := []struct {
+ meta map[string]string
+ remote bool
+ }{
+ {
+ // restore in progress
+ meta: map[string]string{
+ xhttp.AmzRestore: ongoingRestoreObj().String(),
+ },
+ remote: true,
+ },
+ {
+ // restore completed
+ meta: map[string]string{
+ xhttp.AmzRestore: completedRestoreObj(time.Now().Add(time.Hour)).String(),
+ },
+ remote: false,
+ },
+ {
+ // restore completed but expired
+ meta: map[string]string{
+ xhttp.AmzRestore: completedRestoreObj(time.Now().Add(-time.Hour)).String(),
+ },
+ remote: true,
+ },
+ {
+ // restore never initiated
+ meta: map[string]string{},
+ remote: true,
+ },
+ }
+ for i, tc := range testCases {
+ // Set transition status to complete
+ fi.TransitionStatus = lifecycle.TransitionComplete
+ fi.Metadata = tc.meta
+ if got := fi.IsRemote(); got != tc.remote {
+ t.Fatalf("Test %d.a: expected %v got %v", i+1, tc.remote, got)
+ }
+ oi := fi.ToObjectInfo("bucket", "object", false)
+ if got := oi.IsRemote(); got != tc.remote {
+ t.Fatalf("Test %d.b: expected %v got %v", i+1, tc.remote, got)
+ }
+ }
+ // Reset transition status; An object that's not transitioned is not remote.
+ fi.TransitionStatus = ""
+ fi.Metadata = nil
+ if got := fi.IsRemote(); got != false {
+ t.Fatalf("Expected object not to be remote but got %v", got)
+ }
+}
+
+func TestValidateTransitionTier(t *testing.T) {
+ globalTierConfigMgr = NewTierConfigMgr()
+ testCases := []struct {
+ xml []byte
+ expectedErr error
+ }{
+ {
+ // non-existent storage-class
+ xml: []byte(`ruleEnabled1"NONEXISTENT"`),
+ expectedErr: errInvalidStorageClass,
+ },
+ {
+ // no transition rule
+ xml: []byte(`ruleEnabled1`),
+ expectedErr: nil,
+ },
+ }
+ for i, tc := range testCases {
+ lc, err := lifecycle.ParseLifecycleConfig(bytes.NewReader(tc.xml))
+ if err != nil {
+ t.Fatalf("Test %d: Failed to parse lifecycle config %v", i+1, err)
+ }
+
+ err = validateTransitionTier(lc)
+ if err != tc.expectedErr {
+ t.Fatalf("Test %d: Expected %v but got %v", i+1, tc.expectedErr, err)
+ }
+ }
+}
diff --git a/cmd/bucket-listobjects-handlers.go b/cmd/bucket-listobjects-handlers.go
new file mode 100644
index 00000000..0afd76c4
--- /dev/null
+++ b/cmd/bucket-listobjects-handlers.go
@@ -0,0 +1,325 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/mux"
+
+ "github.com/minio/pkg/v3/policy"
+)
+
+// Validate all the ListObjects query arguments, returns an APIErrorCode
+// if one of the args do not meet the required conditions.
+// Special conditions required by MinIO server are as below
+// - delimiter if set should be equal to '/', otherwise the request is rejected.
+// - marker if set should have a common prefix with 'prefix' param, otherwise
+// the request is rejected.
+func validateListObjectsArgs(prefix, marker, delimiter, encodingType string, maxKeys int) APIErrorCode {
+ // Max keys cannot be negative.
+ if maxKeys < 0 {
+ return ErrInvalidMaxKeys
+ }
+
+ if encodingType != "" {
+ // AWS S3 spec only supports 'url' encoding type
+ if !strings.EqualFold(encodingType, "url") {
+ return ErrInvalidEncodingMethod
+ }
+ }
+
+ if !IsValidObjectPrefix(prefix) {
+ return ErrInvalidObjectName
+ }
+
+ if marker != "" && !HasPrefix(marker, prefix) {
+ return ErrNotImplemented
+ }
+
+ return ErrNone
+}
+
+func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r *http.Request) {
+ api.listObjectVersionsHandler(w, r, false)
+}
+
+func (api objectAPIHandlers) ListObjectVersionsMHandler(w http.ResponseWriter, r *http.Request) {
+ api.listObjectVersionsHandler(w, r, true)
+}
+
+// ListObjectVersionsHandler - GET Bucket Object versions
+// You can use the versions subresource to list metadata about all
+// of the versions of objects in a bucket.
+func (api objectAPIHandlers) listObjectVersionsHandler(w http.ResponseWriter, r *http.Request, metadata bool) {
+ ctx := newContext(r, w, "ListObjectVersions")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.ListBucketVersionsAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+ var checkObjMeta metaCheckFn
+ if metadata {
+ checkObjMeta = func(name string, action policy.Action) (s3Err APIErrorCode) {
+ return checkRequestAuthType(ctx, r, action, bucket, name)
+ }
+ }
+ urlValues := r.Form
+
+ // Extract all the listBucketVersions query params to their native values.
+ prefix, marker, delimiter, maxkeys, encodingType, versionIDMarker, errCode := getListBucketObjectVersionsArgs(urlValues)
+ if errCode != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(errCode), r.URL)
+ return
+ }
+
+ // Validate the query params before beginning to serve the request.
+ if s3Error := validateListObjectsArgs(prefix, marker, delimiter, encodingType, maxkeys); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ listObjectVersions := objectAPI.ListObjectVersions
+
+ // Initiate a list object versions operation based on the input params.
+ // On success would return back ListObjectsInfo object to be
+ // marshaled into S3 compatible XML header.
+ listObjectVersionsInfo, err := listObjectVersions(ctx, bucket, prefix, marker, versionIDMarker, delimiter, maxkeys)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if err = DecryptETags(ctx, GlobalKMS, listObjectVersionsInfo.Objects); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ response := generateListVersionsResponse(ctx, bucket, prefix, marker, versionIDMarker, delimiter, encodingType, maxkeys, listObjectVersionsInfo, checkObjMeta)
+
+ // Write success response.
+ writeSuccessResponseXML(w, encodeResponseList(response))
+}
+
+// ListObjectsV2MHandler - GET Bucket (List Objects) Version 2 with metadata.
+// --------------------------
+// This implementation of the GET operation returns some or all (up to 1000)
+// of the objects in a bucket. You can use the request parameters as selection
+// criteria to return a subset of the objects in a bucket.
+//
+// NOTE: It is recommended that this API to be used for application development.
+// MinIO continues to support ListObjectsV1 and V2 for supporting legacy tools.
+func (api objectAPIHandlers) ListObjectsV2MHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "ListObjectsV2M")
+ api.listObjectsV2Handler(ctx, w, r, true)
+}
+
+// ListObjectsV2Handler - GET Bucket (List Objects) Version 2.
+// --------------------------
+// This implementation of the GET operation returns some or all (up to 1000)
+// of the objects in a bucket. You can use the request parameters as selection
+// criteria to return a subset of the objects in a bucket.
+//
+// NOTE: It is recommended that this API to be used for application development.
+// MinIO continues to support ListObjectsV1 for supporting legacy tools.
+func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "ListObjectsV2")
+ api.listObjectsV2Handler(ctx, w, r, false)
+}
+
+// listObjectsV2Handler performs listing either with or without extra metadata.
+func (api objectAPIHandlers) listObjectsV2Handler(ctx context.Context, w http.ResponseWriter, r *http.Request, metadata bool) {
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.ListBucketAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ var checkObjMeta metaCheckFn
+ if metadata {
+ checkObjMeta = func(name string, action policy.Action) (s3Err APIErrorCode) {
+ return checkRequestAuthType(ctx, r, action, bucket, name)
+ }
+ }
+ urlValues := r.Form
+
+ // Extract all the listObjectsV2 query params to their native values.
+ prefix, token, startAfter, delimiter, fetchOwner, maxKeys, encodingType, errCode := getListObjectsV2Args(urlValues)
+ if errCode != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(errCode), r.URL)
+ return
+ }
+
+ // Validate the query params before beginning to serve the request.
+ if s3Error := validateListObjectsArgs(prefix, token, delimiter, encodingType, maxKeys); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ var (
+ listObjectsV2Info ListObjectsV2Info
+ err error
+ )
+
+ if r.Header.Get(xMinIOExtract) == "true" && strings.Contains(prefix, archivePattern) {
+ // Initiate a list objects operation inside a zip file based in the input params
+ listObjectsV2Info, err = listObjectsV2InArchive(ctx, objectAPI, bucket, prefix, token, delimiter, maxKeys, startAfter, r.Header)
+ } else {
+ // Initiate a list objects operation based on the input params.
+ // On success would return back ListObjectsInfo object to be
+ // marshaled into S3 compatible XML header.
+ listObjectsV2Info, err = objectAPI.ListObjectsV2(ctx, bucket, prefix, token, delimiter, maxKeys, fetchOwner, startAfter)
+ }
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if err = DecryptETags(ctx, GlobalKMS, listObjectsV2Info.Objects); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ response := generateListObjectsV2Response(ctx, bucket, prefix, token, listObjectsV2Info.NextContinuationToken, startAfter,
+ delimiter, encodingType, fetchOwner, listObjectsV2Info.IsTruncated,
+ maxKeys, listObjectsV2Info.Objects, listObjectsV2Info.Prefixes, checkObjMeta)
+
+ // Write success response.
+ writeSuccessResponseXML(w, encodeResponseList(response))
+}
+
+func parseRequestToken(token string) (subToken string, nodeIndex int) {
+ if token == "" {
+ return token, -1
+ }
+ i := strings.Index(token, getKeySeparator())
+ if i < 0 {
+ return token, -1
+ }
+ nodeIndex, err := strconv.Atoi(token[i+1:])
+ if err != nil {
+ return token, -1
+ }
+ subToken = token[:i]
+ return subToken, nodeIndex
+}
+
+func proxyRequestByToken(ctx context.Context, w http.ResponseWriter, r *http.Request, token string, returnErr bool) (subToken string, proxied bool, success bool) {
+ var nodeIndex int
+ if subToken, nodeIndex = parseRequestToken(token); nodeIndex >= 0 {
+ proxied, success = proxyRequestByNodeIndex(ctx, w, r, nodeIndex, returnErr)
+ }
+ return subToken, proxied, success
+}
+
+func proxyRequestByNodeIndex(ctx context.Context, w http.ResponseWriter, r *http.Request, index int, returnErr bool) (proxied, success bool) {
+ if len(globalProxyEndpoints) == 0 {
+ return proxied, success
+ }
+ if index < 0 || index >= len(globalProxyEndpoints) {
+ return proxied, success
+ }
+ ep := globalProxyEndpoints[index]
+ if ep.IsLocal {
+ return proxied, success
+ }
+ return true, proxyRequest(ctx, w, r, ep, returnErr)
+}
+
+// ListObjectsV1Handler - GET Bucket (List Objects) Version 1.
+// --------------------------
+// This implementation of the GET operation returns some or all (up to 1000)
+// of the objects in a bucket. You can use the request parameters as selection
+// criteria to return a subset of the objects in a bucket.
+func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "ListObjectsV1")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.ListBucketAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Extract all the listObjectsV1 query params to their native values.
+ prefix, marker, delimiter, maxKeys, encodingType, s3Error := getListObjectsV1Args(r.Form)
+ if s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Validate all the query params before beginning to serve the request.
+ if s3Error := validateListObjectsArgs(prefix, marker, delimiter, encodingType, maxKeys); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ listObjects := objectAPI.ListObjects
+
+ // Initiate a list objects operation based on the input params.
+ // On success would return back ListObjectsInfo object to be
+ // marshaled into S3 compatible XML header.
+ listObjectsInfo, err := listObjects(ctx, bucket, prefix, marker, delimiter, maxKeys)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if err = DecryptETags(ctx, GlobalKMS, listObjectsInfo.Objects); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ response := generateListObjectsV1Response(ctx, bucket, prefix, marker, delimiter, encodingType, maxKeys, listObjectsInfo)
+
+ // Write success response.
+ writeSuccessResponseXML(w, encodeResponseList(response))
+}
diff --git a/cmd/bucket-metadata-sys.go b/cmd/bucket-metadata-sys.go
new file mode 100644
index 00000000..20be4ffd
--- /dev/null
+++ b/cmd/bucket-metadata-sys.go
@@ -0,0 +1,659 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "math/rand"
+ "sync"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio-go/v7/pkg/set"
+ "github.com/minio/minio-go/v7/pkg/tags"
+ bucketsse "github.com/minio/minio/internal/bucket/encryption"
+ "github.com/minio/minio/internal/bucket/lifecycle"
+ 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/event"
+ "github.com/minio/minio/internal/kms"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/policy"
+ "github.com/minio/pkg/v3/sync/errgroup"
+ "golang.org/x/sync/singleflight"
+)
+
+// BucketMetadataSys captures all bucket metadata for a given cluster.
+type BucketMetadataSys struct {
+ objAPI ObjectLayer
+
+ sync.RWMutex
+ initialized bool
+ group *singleflight.Group
+ metadataMap map[string]BucketMetadata
+}
+
+// Count returns number of bucket metadata map entries.
+func (sys *BucketMetadataSys) Count() int {
+ sys.RLock()
+ defer sys.RUnlock()
+
+ return len(sys.metadataMap)
+}
+
+// Remove bucket metadata from memory.
+func (sys *BucketMetadataSys) Remove(buckets ...string) {
+ sys.Lock()
+ for _, bucket := range buckets {
+ sys.group.Forget(bucket)
+ delete(sys.metadataMap, bucket)
+ globalBucketMonitor.DeleteBucket(bucket)
+ }
+ sys.Unlock()
+}
+
+// RemoveStaleBuckets removes all stale buckets in memory that are not on disk.
+func (sys *BucketMetadataSys) RemoveStaleBuckets(diskBuckets set.StringSet) {
+ sys.Lock()
+ defer sys.Unlock()
+
+ for bucket := range sys.metadataMap {
+ if diskBuckets.Contains(bucket) {
+ continue
+ } // doesn't exist on disk remove from memory.
+ delete(sys.metadataMap, bucket)
+ globalBucketMonitor.DeleteBucket(bucket)
+ }
+}
+
+// Set - sets a new metadata in-memory.
+// Only a shallow copy is saved and fields with references
+// cannot be modified without causing a race condition,
+// so they should be replaced atomically and not appended to, etc.
+// Data is not persisted to disk.
+func (sys *BucketMetadataSys) Set(bucket string, meta BucketMetadata) {
+ if !isMinioMetaBucketName(bucket) {
+ sys.Lock()
+ sys.metadataMap[bucket] = meta
+ sys.Unlock()
+ }
+}
+
+func (sys *BucketMetadataSys) updateAndParse(ctx context.Context, bucket string, configFile string, configData []byte, parse bool) (updatedAt time.Time, err error) {
+ objAPI := newObjectLayerFn()
+ if objAPI == nil {
+ return updatedAt, errServerNotInitialized
+ }
+
+ if isMinioMetaBucketName(bucket) {
+ return updatedAt, errInvalidArgument
+ }
+
+ meta, err := loadBucketMetadataParse(ctx, objAPI, bucket, parse)
+ if err != nil {
+ if !globalIsErasure && !globalIsDistErasure && errors.Is(err, errVolumeNotFound) {
+ // Only single drive mode needs this fallback.
+ meta = newBucketMetadata(bucket)
+ } else {
+ return updatedAt, err
+ }
+ }
+ updatedAt = UTCNow()
+ switch configFile {
+ case bucketPolicyConfig:
+ meta.PolicyConfigJSON = configData
+ meta.PolicyConfigUpdatedAt = updatedAt
+ case bucketNotificationConfig:
+ meta.NotificationConfigXML = configData
+ meta.NotificationConfigUpdatedAt = updatedAt
+ case bucketLifecycleConfig:
+ meta.LifecycleConfigXML = configData
+ meta.LifecycleConfigUpdatedAt = updatedAt
+ case bucketSSEConfig:
+ meta.EncryptionConfigXML = configData
+ meta.EncryptionConfigUpdatedAt = updatedAt
+ case bucketTaggingConfig:
+ meta.TaggingConfigXML = configData
+ meta.TaggingConfigUpdatedAt = updatedAt
+ case bucketQuotaConfigFile:
+ meta.QuotaConfigJSON = configData
+ meta.QuotaConfigUpdatedAt = updatedAt
+ case objectLockConfig:
+ meta.ObjectLockConfigXML = configData
+ meta.ObjectLockConfigUpdatedAt = updatedAt
+ case bucketVersioningConfig:
+ meta.VersioningConfigXML = configData
+ meta.VersioningConfigUpdatedAt = updatedAt
+ case bucketReplicationConfig:
+ meta.ReplicationConfigXML = configData
+ meta.ReplicationConfigUpdatedAt = updatedAt
+ case bucketTargetsFile:
+ meta.BucketTargetsConfigJSON, meta.BucketTargetsConfigMetaJSON, err = encryptBucketMetadata(ctx, meta.Name, configData, kms.Context{
+ bucket: meta.Name,
+ bucketTargetsFile: bucketTargetsFile,
+ })
+ if err != nil {
+ return updatedAt, fmt.Errorf("Error encrypting bucket target metadata %w", err)
+ }
+ meta.BucketTargetsConfigUpdatedAt = updatedAt
+ meta.BucketTargetsConfigMetaUpdatedAt = updatedAt
+ default:
+ return updatedAt, fmt.Errorf("Unknown bucket %s metadata update requested %s", bucket, configFile)
+ }
+
+ return updatedAt, sys.save(ctx, meta)
+}
+
+func (sys *BucketMetadataSys) save(ctx context.Context, meta BucketMetadata) error {
+ objAPI := newObjectLayerFn()
+ if objAPI == nil {
+ return errServerNotInitialized
+ }
+
+ if isMinioMetaBucketName(meta.Name) {
+ return errInvalidArgument
+ }
+
+ if err := meta.Save(ctx, objAPI); err != nil {
+ return err
+ }
+
+ sys.Set(meta.Name, meta)
+ globalNotificationSys.LoadBucketMetadata(bgContext(ctx), meta.Name) // Do not use caller context here
+ return nil
+}
+
+// Delete delete the bucket metadata for the specified bucket.
+// must be used by all callers instead of using Update() with nil configData.
+func (sys *BucketMetadataSys) Delete(ctx context.Context, bucket string, configFile string) (updatedAt time.Time, err error) {
+ if configFile == bucketLifecycleConfig {
+ // Get bucket config from current site
+ meta, e := globalBucketMetadataSys.GetConfigFromDisk(ctx, bucket)
+ if e != nil && !errors.Is(e, errConfigNotFound) {
+ return updatedAt, e
+ }
+ var expiryRuleRemoved bool
+ if len(meta.LifecycleConfigXML) > 0 {
+ var lcCfg lifecycle.Lifecycle
+ if err := xml.Unmarshal(meta.LifecycleConfigXML, &lcCfg); err != nil {
+ return updatedAt, err
+ }
+ // find a single expiry rule set the flag
+ for _, rl := range lcCfg.Rules {
+ if !rl.Expiration.IsNull() || !rl.NoncurrentVersionExpiration.IsNull() {
+ expiryRuleRemoved = true
+ break
+ }
+ }
+ }
+
+ // Form empty ILM details with `ExpiryUpdatedAt` field and save
+ var cfgData []byte
+ if expiryRuleRemoved {
+ var lcCfg lifecycle.Lifecycle
+ currtime := time.Now()
+ lcCfg.ExpiryUpdatedAt = &currtime
+ cfgData, err = xml.Marshal(lcCfg)
+ if err != nil {
+ return updatedAt, err
+ }
+ }
+ return sys.updateAndParse(ctx, bucket, configFile, cfgData, false)
+ }
+ return sys.updateAndParse(ctx, bucket, configFile, nil, false)
+}
+
+// Update update bucket metadata for the specified bucket.
+// The configData data should not be modified after being sent here.
+func (sys *BucketMetadataSys) Update(ctx context.Context, bucket string, configFile string, configData []byte) (updatedAt time.Time, err error) {
+ return sys.updateAndParse(ctx, bucket, configFile, configData, true)
+}
+
+// Get metadata for a bucket.
+// If no metadata exists errConfigNotFound is returned and a new metadata is returned.
+// Only a shallow copy is returned, so referenced data should not be modified,
+// but can be replaced atomically.
+//
+// This function should only be used with
+// - GetBucketInfo
+// - ListBuckets
+// For all other bucket specific metadata, use the relevant
+// calls implemented specifically for each of those features.
+func (sys *BucketMetadataSys) Get(bucket string) (BucketMetadata, error) {
+ if isMinioMetaBucketName(bucket) {
+ return newBucketMetadata(bucket), errConfigNotFound
+ }
+
+ sys.RLock()
+ defer sys.RUnlock()
+
+ meta, ok := sys.metadataMap[bucket]
+ if !ok {
+ return newBucketMetadata(bucket), errConfigNotFound
+ }
+
+ return meta, nil
+}
+
+// GetVersioningConfig returns configured versioning config
+// The returned object may not be modified.
+func (sys *BucketMetadataSys) GetVersioningConfig(bucket string) (*versioning.Versioning, time.Time, error) {
+ meta, _, err := sys.GetConfig(GlobalContext, bucket)
+ if err != nil {
+ if errors.Is(err, errConfigNotFound) {
+ return &versioning.Versioning{XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/"}, meta.Created, nil
+ }
+ return &versioning.Versioning{XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/"}, time.Time{}, err
+ }
+ return meta.versioningConfig, meta.VersioningConfigUpdatedAt, nil
+}
+
+// GetBucketPolicy returns configured bucket policy
+func (sys *BucketMetadataSys) GetBucketPolicy(bucket string) (*policy.BucketPolicy, time.Time, error) {
+ meta, _, err := sys.GetConfig(GlobalContext, bucket)
+ if err != nil {
+ if errors.Is(err, errConfigNotFound) {
+ return nil, time.Time{}, BucketPolicyNotFound{Bucket: bucket}
+ }
+ return nil, time.Time{}, err
+ }
+ if meta.policyConfig == nil {
+ return nil, time.Time{}, BucketPolicyNotFound{Bucket: bucket}
+ }
+ return meta.policyConfig, meta.PolicyConfigUpdatedAt, nil
+}
+
+// GetTaggingConfig returns configured tagging config
+// The returned object may not be modified.
+func (sys *BucketMetadataSys) GetTaggingConfig(bucket string) (*tags.Tags, time.Time, error) {
+ meta, _, err := sys.GetConfig(GlobalContext, bucket)
+ if err != nil {
+ if errors.Is(err, errConfigNotFound) {
+ return nil, time.Time{}, BucketTaggingNotFound{Bucket: bucket}
+ }
+ return nil, time.Time{}, err
+ }
+ if meta.taggingConfig == nil {
+ return nil, time.Time{}, BucketTaggingNotFound{Bucket: bucket}
+ }
+ return meta.taggingConfig, meta.TaggingConfigUpdatedAt, nil
+}
+
+// GetObjectLockConfig returns configured object lock config
+// The returned object may not be modified.
+func (sys *BucketMetadataSys) GetObjectLockConfig(bucket string) (*objectlock.Config, time.Time, error) {
+ meta, _, err := sys.GetConfig(GlobalContext, bucket)
+ if err != nil {
+ if errors.Is(err, errConfigNotFound) {
+ return nil, time.Time{}, BucketObjectLockConfigNotFound{Bucket: bucket}
+ }
+ return nil, time.Time{}, err
+ }
+ if meta.objectLockConfig == nil {
+ return nil, time.Time{}, BucketObjectLockConfigNotFound{Bucket: bucket}
+ }
+ return meta.objectLockConfig, meta.ObjectLockConfigUpdatedAt, nil
+}
+
+// GetLifecycleConfig returns configured lifecycle config
+// The returned object may not be modified.
+func (sys *BucketMetadataSys) GetLifecycleConfig(bucket string) (*lifecycle.Lifecycle, time.Time, error) {
+ meta, _, err := sys.GetConfig(GlobalContext, bucket)
+ if err != nil {
+ if errors.Is(err, errConfigNotFound) {
+ return nil, time.Time{}, BucketLifecycleNotFound{Bucket: bucket}
+ }
+ return nil, time.Time{}, err
+ }
+ // there could be just `ExpiryUpdatedAt` field populated as part
+ // of last delete all. Treat this situation as not lifecycle configuration
+ // available
+ if meta.lifecycleConfig == nil || len(meta.lifecycleConfig.Rules) == 0 {
+ return nil, time.Time{}, BucketLifecycleNotFound{Bucket: bucket}
+ }
+ return meta.lifecycleConfig, meta.LifecycleConfigUpdatedAt, nil
+}
+
+// GetNotificationConfig returns configured notification config
+// The returned object may not be modified.
+func (sys *BucketMetadataSys) GetNotificationConfig(bucket string) (*event.Config, error) {
+ meta, _, err := sys.GetConfig(GlobalContext, bucket)
+ if err != nil {
+ return nil, err
+ }
+ return meta.notificationConfig, nil
+}
+
+// GetSSEConfig returns configured SSE config
+// The returned object may not be modified.
+func (sys *BucketMetadataSys) GetSSEConfig(bucket string) (*bucketsse.BucketSSEConfig, time.Time, error) {
+ meta, _, err := sys.GetConfig(GlobalContext, bucket)
+ if err != nil {
+ if errors.Is(err, errConfigNotFound) {
+ return nil, time.Time{}, BucketSSEConfigNotFound{Bucket: bucket}
+ }
+ return nil, time.Time{}, err
+ }
+ if meta.sseConfig == nil {
+ return nil, time.Time{}, BucketSSEConfigNotFound{Bucket: bucket}
+ }
+ return meta.sseConfig, meta.EncryptionConfigUpdatedAt, nil
+}
+
+// CreatedAt returns the time of creation of bucket
+func (sys *BucketMetadataSys) CreatedAt(bucket string) (time.Time, error) {
+ meta, _, err := sys.GetConfig(GlobalContext, bucket)
+ if err != nil {
+ return time.Time{}, err
+ }
+ return meta.Created.UTC(), nil
+}
+
+// GetPolicyConfig returns configured bucket policy
+// The returned object may not be modified.
+func (sys *BucketMetadataSys) GetPolicyConfig(bucket string) (*policy.BucketPolicy, time.Time, error) {
+ meta, _, err := sys.GetConfig(GlobalContext, bucket)
+ if err != nil {
+ if errors.Is(err, errConfigNotFound) {
+ return nil, time.Time{}, BucketPolicyNotFound{Bucket: bucket}
+ }
+ return nil, time.Time{}, err
+ }
+ if meta.policyConfig == nil {
+ return nil, time.Time{}, BucketPolicyNotFound{Bucket: bucket}
+ }
+ return meta.policyConfig, meta.PolicyConfigUpdatedAt, nil
+}
+
+// GetQuotaConfig returns configured bucket quota
+// The returned object may not be modified.
+func (sys *BucketMetadataSys) GetQuotaConfig(ctx context.Context, bucket string) (*madmin.BucketQuota, time.Time, error) {
+ meta, _, err := sys.GetConfig(ctx, bucket)
+ if err != nil {
+ if errors.Is(err, errConfigNotFound) {
+ return nil, time.Time{}, BucketQuotaConfigNotFound{Bucket: bucket}
+ }
+ return nil, time.Time{}, err
+ }
+ return meta.quotaConfig, meta.QuotaConfigUpdatedAt, nil
+}
+
+// GetReplicationConfig returns configured bucket replication config
+// The returned object may not be modified.
+func (sys *BucketMetadataSys) GetReplicationConfig(ctx context.Context, bucket string) (*replication.Config, time.Time, error) {
+ meta, reloaded, err := sys.GetConfig(ctx, bucket)
+ if err != nil {
+ if errors.Is(err, errConfigNotFound) {
+ return nil, time.Time{}, BucketReplicationConfigNotFound{Bucket: bucket}
+ }
+ return nil, time.Time{}, err
+ }
+
+ if meta.replicationConfig == nil {
+ return nil, time.Time{}, BucketReplicationConfigNotFound{Bucket: bucket}
+ }
+ if reloaded {
+ globalBucketTargetSys.set(bucket, meta)
+ }
+ return meta.replicationConfig, meta.ReplicationConfigUpdatedAt, nil
+}
+
+// GetBucketTargetsConfig returns configured bucket targets for this bucket
+// The returned object may not be modified.
+func (sys *BucketMetadataSys) GetBucketTargetsConfig(bucket string) (*madmin.BucketTargets, error) {
+ meta, reloaded, err := sys.GetConfig(GlobalContext, bucket)
+ if err != nil {
+ if errors.Is(err, errConfigNotFound) {
+ return nil, BucketRemoteTargetNotFound{Bucket: bucket}
+ }
+ return nil, err
+ }
+ if meta.bucketTargetConfig == nil {
+ return nil, BucketRemoteTargetNotFound{Bucket: bucket}
+ }
+ if reloaded {
+ globalBucketTargetSys.set(bucket, meta)
+ }
+ return meta.bucketTargetConfig, nil
+}
+
+// GetConfigFromDisk read bucket metadata config from disk.
+func (sys *BucketMetadataSys) GetConfigFromDisk(ctx context.Context, bucket string) (BucketMetadata, error) {
+ objAPI := newObjectLayerFn()
+ if objAPI == nil {
+ return newBucketMetadata(bucket), errServerNotInitialized
+ }
+
+ if isMinioMetaBucketName(bucket) {
+ return newBucketMetadata(bucket), errInvalidArgument
+ }
+
+ return loadBucketMetadata(ctx, objAPI, bucket)
+}
+
+var errBucketMetadataNotInitialized = errors.New("bucket metadata not initialized yet")
+
+// GetConfig returns a specific configuration from the bucket metadata.
+// The returned object may not be modified.
+// reloaded will be true if metadata refreshed from disk
+func (sys *BucketMetadataSys) GetConfig(ctx context.Context, bucket string) (meta BucketMetadata, reloaded bool, err error) {
+ objAPI := newObjectLayerFn()
+ if objAPI == nil {
+ return newBucketMetadata(bucket), reloaded, errServerNotInitialized
+ }
+
+ if isMinioMetaBucketName(bucket) {
+ return newBucketMetadata(bucket), reloaded, errInvalidArgument
+ }
+
+ sys.RLock()
+ meta, ok := sys.metadataMap[bucket]
+ sys.RUnlock()
+ if ok {
+ return meta, reloaded, nil
+ }
+
+ val, err, _ := sys.group.Do(bucket, func() (val any, err error) {
+ meta, err = loadBucketMetadata(ctx, objAPI, bucket)
+ if err != nil {
+ if !sys.Initialized() {
+ // bucket metadata not yet initialized
+ return newBucketMetadata(bucket), errBucketMetadataNotInitialized
+ }
+ }
+ return meta, err
+ })
+ meta, _ = val.(BucketMetadata)
+ if err != nil {
+ return meta, false, err
+ }
+ sys.Lock()
+ sys.metadataMap[bucket] = meta
+ sys.Unlock()
+
+ return meta, true, nil
+}
+
+// Init - initializes bucket metadata system for all buckets.
+func (sys *BucketMetadataSys) Init(ctx context.Context, buckets []string, objAPI ObjectLayer) error {
+ if objAPI == nil {
+ return errServerNotInitialized
+ }
+
+ sys.objAPI = objAPI
+
+ // Load bucket metadata sys.
+ sys.init(ctx, buckets)
+ return nil
+}
+
+// concurrently load bucket metadata to speed up loading bucket metadata.
+func (sys *BucketMetadataSys) concurrentLoad(ctx context.Context, buckets []string) {
+ g := errgroup.WithNErrs(len(buckets))
+ bucketMetas := make([]BucketMetadata, len(buckets))
+ for index := range buckets {
+ g.Go(func() error {
+ // Sleep and stagger to avoid blocked CPU and thundering
+ // herd upon start up sequence.
+ time.Sleep(25*time.Millisecond + time.Duration(rand.Int63n(int64(100*time.Millisecond))))
+
+ _, _ = sys.objAPI.HealBucket(ctx, buckets[index], madmin.HealOpts{Recreate: true})
+ meta, err := loadBucketMetadata(ctx, sys.objAPI, buckets[index])
+ if err != nil {
+ return err
+ }
+ bucketMetas[index] = meta
+ return nil
+ }, index)
+ }
+
+ errs := g.Wait()
+ for index, err := range errs {
+ if err != nil {
+ internalLogOnceIf(ctx, fmt.Errorf("Unable to load bucket metadata, will be retried: %w", err),
+ "load-bucket-metadata-"+buckets[index], logger.WarningKind)
+ }
+ }
+
+ // Hold lock here to update in-memory map at once,
+ // instead of serializing the Go routines.
+ sys.Lock()
+ for i, meta := range bucketMetas {
+ if errs[i] != nil {
+ continue
+ }
+ sys.metadataMap[buckets[i]] = meta
+ }
+ sys.Unlock()
+
+ for i, meta := range bucketMetas {
+ if errs[i] != nil {
+ continue
+ }
+ globalEventNotifier.set(buckets[i], meta) // set notification targets
+ globalBucketTargetSys.set(buckets[i], meta) // set remote replication targets
+ }
+}
+
+func (sys *BucketMetadataSys) refreshBucketsMetadataLoop(ctx context.Context) {
+ const bucketMetadataRefresh = 15 * time.Minute
+
+ sleeper := newDynamicSleeper(2, 150*time.Millisecond, false)
+
+ t := time.NewTimer(bucketMetadataRefresh)
+ defer t.Stop()
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-t.C:
+ buckets, err := sys.objAPI.ListBuckets(ctx, BucketOptions{NoMetadata: true})
+ if err != nil {
+ internalLogIf(ctx, err, logger.WarningKind)
+ break
+ }
+
+ // Handle if we have some buckets in-memory those are stale.
+ // first delete them and then replace the newer state()
+ // from disk.
+ diskBuckets := set.CreateStringSet()
+ for _, bucket := range buckets {
+ diskBuckets.Add(bucket.Name)
+ }
+ sys.RemoveStaleBuckets(diskBuckets)
+
+ for i := range buckets {
+ wait := sleeper.Timer(ctx)
+
+ bucket := buckets[i].Name
+ updated := false
+
+ meta, err := loadBucketMetadata(ctx, sys.objAPI, bucket)
+ if err != nil {
+ internalLogIf(ctx, err, logger.WarningKind)
+ wait() // wait to proceed to next entry.
+ continue
+ }
+
+ sys.Lock()
+ // Update if the bucket metadata in the memory is older than on-disk one
+ if lu := sys.metadataMap[bucket].lastUpdate(); lu.Before(meta.lastUpdate()) {
+ updated = true
+ sys.metadataMap[bucket] = meta
+ }
+ sys.Unlock()
+
+ if updated {
+ globalEventNotifier.set(bucket, meta)
+ globalBucketTargetSys.set(bucket, meta)
+ }
+
+ wait() // wait to proceed to next entry.
+ }
+ }
+ t.Reset(bucketMetadataRefresh)
+ }
+}
+
+// Initialized indicates if bucket metadata sys is initialized atleast once.
+func (sys *BucketMetadataSys) Initialized() bool {
+ sys.RLock()
+ defer sys.RUnlock()
+
+ return sys.initialized
+}
+
+// Loads bucket metadata for all buckets into BucketMetadataSys.
+func (sys *BucketMetadataSys) init(ctx context.Context, buckets []string) {
+ count := globalEndpoints.ESCount() * 10
+ for {
+ if len(buckets) < count {
+ sys.concurrentLoad(ctx, buckets)
+ break
+ }
+ sys.concurrentLoad(ctx, buckets[:count])
+ buckets = buckets[count:]
+ }
+
+ sys.Lock()
+ sys.initialized = true
+ sys.Unlock()
+
+ if globalIsDistErasure {
+ go sys.refreshBucketsMetadataLoop(ctx)
+ }
+}
+
+// Reset the state of the BucketMetadataSys.
+func (sys *BucketMetadataSys) Reset() {
+ sys.Lock()
+ clear(sys.metadataMap)
+ sys.Unlock()
+}
+
+// NewBucketMetadataSys - creates new policy system.
+func NewBucketMetadataSys() *BucketMetadataSys {
+ return &BucketMetadataSys{
+ metadataMap: make(map[string]BucketMetadata),
+ group: &singleflight.Group{},
+ }
+}
diff --git a/cmd/bucket-metadata.go b/cmd/bucket-metadata.go
new file mode 100644
index 00000000..e7811817
--- /dev/null
+++ b/cmd/bucket-metadata.go
@@ -0,0 +1,594 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "crypto/rand"
+ "encoding/binary"
+ "encoding/json"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "path"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio-go/v7/pkg/tags"
+ bucketsse "github.com/minio/minio/internal/bucket/encryption"
+ "github.com/minio/minio/internal/bucket/lifecycle"
+ 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/crypto"
+ "github.com/minio/minio/internal/event"
+ "github.com/minio/minio/internal/kms"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/policy"
+ "github.com/minio/sio"
+)
+
+const (
+ legacyBucketObjectLockEnabledConfigFile = "object-lock-enabled.json"
+ legacyBucketObjectLockEnabledConfig = `{"x-amz-bucket-object-lock-enabled":true}`
+
+ bucketMetadataFile = ".metadata.bin"
+ bucketMetadataFormat = 1
+ bucketMetadataVersion = 1
+)
+
+var (
+ enabledBucketObjectLockConfig = []byte(`Enabled`)
+ enabledBucketVersioningConfig = []byte(`Enabled`)
+)
+
+//go:generate msgp -file $GOFILE
+
+// BucketMetadata contains bucket metadata.
+// When adding/removing fields, regenerate the marshal code using the go generate above.
+// Only changing meaning of fields requires a version bump.
+// bucketMetadataFormat refers to the format.
+// bucketMetadataVersion can be used to track a rolling upgrade of a field.
+type BucketMetadata struct {
+ Name string
+ Created time.Time
+ LockEnabled bool // legacy not used anymore.
+ PolicyConfigJSON []byte
+ NotificationConfigXML []byte
+ LifecycleConfigXML []byte
+ ObjectLockConfigXML []byte
+ VersioningConfigXML []byte
+ EncryptionConfigXML []byte
+ TaggingConfigXML []byte
+ QuotaConfigJSON []byte
+ ReplicationConfigXML []byte
+ BucketTargetsConfigJSON []byte
+ BucketTargetsConfigMetaJSON []byte
+
+ PolicyConfigUpdatedAt time.Time
+ ObjectLockConfigUpdatedAt time.Time
+ EncryptionConfigUpdatedAt time.Time
+ TaggingConfigUpdatedAt time.Time
+ QuotaConfigUpdatedAt time.Time
+ ReplicationConfigUpdatedAt time.Time
+ VersioningConfigUpdatedAt time.Time
+ LifecycleConfigUpdatedAt time.Time
+ NotificationConfigUpdatedAt time.Time
+ BucketTargetsConfigUpdatedAt time.Time
+ BucketTargetsConfigMetaUpdatedAt time.Time
+ // Add a new UpdatedAt field and update lastUpdate function
+
+ // Unexported fields. Must be updated atomically.
+ policyConfig *policy.BucketPolicy
+ notificationConfig *event.Config
+ lifecycleConfig *lifecycle.Lifecycle
+ objectLockConfig *objectlock.Config
+ versioningConfig *versioning.Versioning
+ sseConfig *bucketsse.BucketSSEConfig
+ taggingConfig *tags.Tags
+ quotaConfig *madmin.BucketQuota
+ replicationConfig *replication.Config
+ bucketTargetConfig *madmin.BucketTargets
+ bucketTargetConfigMeta map[string]string
+}
+
+// newBucketMetadata creates BucketMetadata with the supplied name and Created to Now.
+func newBucketMetadata(name string) BucketMetadata {
+ return BucketMetadata{
+ Name: name,
+ notificationConfig: &event.Config{
+ XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/",
+ },
+ quotaConfig: &madmin.BucketQuota{},
+ versioningConfig: &versioning.Versioning{
+ XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/",
+ },
+ bucketTargetConfig: &madmin.BucketTargets{},
+ bucketTargetConfigMeta: make(map[string]string),
+ }
+}
+
+// Return the last update of this bucket metadata, which
+// means, the last update of any policy document.
+func (b BucketMetadata) lastUpdate() (t time.Time) {
+ if b.PolicyConfigUpdatedAt.After(t) {
+ t = b.PolicyConfigUpdatedAt
+ }
+ if b.ObjectLockConfigUpdatedAt.After(t) {
+ t = b.ObjectLockConfigUpdatedAt
+ }
+ if b.EncryptionConfigUpdatedAt.After(t) {
+ t = b.EncryptionConfigUpdatedAt
+ }
+ if b.TaggingConfigUpdatedAt.After(t) {
+ t = b.TaggingConfigUpdatedAt
+ }
+ if b.QuotaConfigUpdatedAt.After(t) {
+ t = b.QuotaConfigUpdatedAt
+ }
+ if b.ReplicationConfigUpdatedAt.After(t) {
+ t = b.ReplicationConfigUpdatedAt
+ }
+ if b.VersioningConfigUpdatedAt.After(t) {
+ t = b.VersioningConfigUpdatedAt
+ }
+ if b.LifecycleConfigUpdatedAt.After(t) {
+ t = b.LifecycleConfigUpdatedAt
+ }
+ if b.NotificationConfigUpdatedAt.After(t) {
+ t = b.NotificationConfigUpdatedAt
+ }
+ if b.BucketTargetsConfigUpdatedAt.After(t) {
+ t = b.BucketTargetsConfigUpdatedAt
+ }
+ if b.BucketTargetsConfigMetaUpdatedAt.After(t) {
+ t = b.BucketTargetsConfigMetaUpdatedAt
+ }
+
+ return t
+}
+
+// Versioning returns true if versioning is enabled
+func (b BucketMetadata) Versioning() bool {
+ return b.LockEnabled || (b.versioningConfig != nil && b.versioningConfig.Enabled()) || (b.objectLockConfig != nil && b.objectLockConfig.Enabled())
+}
+
+// ObjectLocking returns true if object locking is enabled
+func (b BucketMetadata) ObjectLocking() bool {
+ return b.LockEnabled || (b.objectLockConfig != nil && b.objectLockConfig.Enabled())
+}
+
+// SetCreatedAt preserves the CreatedAt time for bucket across sites in site replication. It defaults to
+// creation time of bucket on this cluster in all other cases.
+func (b *BucketMetadata) SetCreatedAt(createdAt time.Time) {
+ if b.Created.IsZero() {
+ b.Created = UTCNow()
+ }
+ if !createdAt.IsZero() {
+ b.Created = createdAt.UTC()
+ }
+}
+
+// Load - loads the metadata of bucket by name from ObjectLayer api.
+// If an error is returned the returned metadata will be default initialized.
+func readBucketMetadata(ctx context.Context, api ObjectLayer, name string) (BucketMetadata, error) {
+ if name == "" {
+ internalLogIf(ctx, errors.New("bucket name cannot be empty"), logger.WarningKind)
+ return BucketMetadata{}, errInvalidArgument
+ }
+ b := newBucketMetadata(name)
+ configFile := path.Join(bucketMetaPrefix, name, bucketMetadataFile)
+ data, err := readConfig(ctx, api, configFile)
+ if err != nil {
+ return b, err
+ }
+ if len(data) <= 4 {
+ return b, fmt.Errorf("loadBucketMetadata: no data")
+ }
+ // Read header
+ switch binary.LittleEndian.Uint16(data[0:2]) {
+ case bucketMetadataFormat:
+ default:
+ return b, fmt.Errorf("loadBucketMetadata: unknown format: %d", binary.LittleEndian.Uint16(data[0:2]))
+ }
+ switch binary.LittleEndian.Uint16(data[2:4]) {
+ case bucketMetadataVersion:
+ default:
+ return b, fmt.Errorf("loadBucketMetadata: unknown version: %d", binary.LittleEndian.Uint16(data[2:4]))
+ }
+ _, err = b.UnmarshalMsg(data[4:])
+ return b, err
+}
+
+func loadBucketMetadataParse(ctx context.Context, objectAPI ObjectLayer, bucket string, parse bool) (BucketMetadata, error) {
+ b, err := readBucketMetadata(ctx, objectAPI, bucket)
+ b.Name = bucket // in-case parsing failed for some reason, make sure bucket name is not empty.
+ if err != nil && !errors.Is(err, errConfigNotFound) {
+ return b, err
+ }
+ if err == nil {
+ b.defaultTimestamps()
+ }
+
+ // If bucket metadata is missing look for legacy files,
+ // since we only ever had b.Created as non-zero when
+ // migration was complete in 2020-May release. So this
+ // a check to avoid migrating for buckets that already
+ // have this field set.
+ if b.Created.IsZero() {
+ configs, err := b.getAllLegacyConfigs(ctx, objectAPI)
+ if err != nil {
+ return b, err
+ }
+
+ if len(configs) > 0 {
+ // Old bucket without bucket metadata. Hence we migrate existing settings.
+ if err = b.convertLegacyConfigs(ctx, objectAPI, configs); err != nil {
+ return b, err
+ }
+ }
+ }
+
+ if parse {
+ // nothing to update, parse and proceed.
+ if err = b.parseAllConfigs(ctx, objectAPI); err != nil {
+ return b, err
+ }
+ }
+
+ // migrate unencrypted remote targets
+ if err = b.migrateTargetConfig(ctx, objectAPI); err != nil {
+ return b, err
+ }
+
+ return b, nil
+}
+
+// loadBucketMetadata loads and migrates to bucket metadata.
+func loadBucketMetadata(ctx context.Context, objectAPI ObjectLayer, bucket string) (BucketMetadata, error) {
+ return loadBucketMetadataParse(ctx, objectAPI, bucket, true)
+}
+
+// parseAllConfigs will parse all configs and populate the private fields.
+// The first error encountered is returned.
+func (b *BucketMetadata) parseAllConfigs(ctx context.Context, objectAPI ObjectLayer) (err error) {
+ if len(b.PolicyConfigJSON) != 0 {
+ b.policyConfig, err = policy.ParseBucketPolicyConfig(bytes.NewReader(b.PolicyConfigJSON), b.Name)
+ if err != nil {
+ return err
+ }
+ } else {
+ b.policyConfig = nil
+ }
+
+ if len(b.NotificationConfigXML) != 0 {
+ if err = xml.Unmarshal(b.NotificationConfigXML, b.notificationConfig); err != nil {
+ return err
+ }
+ }
+
+ if len(b.LifecycleConfigXML) != 0 {
+ b.lifecycleConfig, err = lifecycle.ParseLifecycleConfig(bytes.NewReader(b.LifecycleConfigXML))
+ if err != nil {
+ return err
+ }
+ } else {
+ b.lifecycleConfig = nil
+ }
+
+ if len(b.EncryptionConfigXML) != 0 {
+ b.sseConfig, err = bucketsse.ParseBucketSSEConfig(bytes.NewReader(b.EncryptionConfigXML))
+ if err != nil {
+ return err
+ }
+ } else {
+ b.sseConfig = nil
+ }
+
+ if len(b.TaggingConfigXML) != 0 {
+ b.taggingConfig, err = tags.ParseBucketXML(bytes.NewReader(b.TaggingConfigXML))
+ if err != nil {
+ return err
+ }
+ } else {
+ b.taggingConfig = nil
+ }
+
+ if bytes.Equal(b.ObjectLockConfigXML, enabledBucketObjectLockConfig) {
+ b.VersioningConfigXML = enabledBucketVersioningConfig
+ }
+
+ if len(b.ObjectLockConfigXML) != 0 {
+ b.objectLockConfig, err = objectlock.ParseObjectLockConfig(bytes.NewReader(b.ObjectLockConfigXML))
+ if err != nil {
+ return err
+ }
+ } else {
+ b.objectLockConfig = nil
+ }
+
+ if len(b.VersioningConfigXML) != 0 {
+ b.versioningConfig, err = versioning.ParseConfig(bytes.NewReader(b.VersioningConfigXML))
+ if err != nil {
+ return err
+ }
+ }
+
+ if len(b.QuotaConfigJSON) != 0 {
+ b.quotaConfig, err = parseBucketQuota(b.Name, b.QuotaConfigJSON)
+ if err != nil {
+ return err
+ }
+ }
+
+ if len(b.ReplicationConfigXML) != 0 {
+ b.replicationConfig, err = replication.ParseConfig(bytes.NewReader(b.ReplicationConfigXML))
+ if err != nil {
+ return err
+ }
+ } else {
+ b.replicationConfig = nil
+ }
+
+ if len(b.BucketTargetsConfigJSON) != 0 {
+ b.bucketTargetConfig, err = parseBucketTargetConfig(b.Name, b.BucketTargetsConfigJSON, b.BucketTargetsConfigMetaJSON)
+ if err != nil {
+ return err
+ }
+ } else {
+ b.bucketTargetConfig = &madmin.BucketTargets{}
+ }
+ return nil
+}
+
+func (b *BucketMetadata) getAllLegacyConfigs(ctx context.Context, objectAPI ObjectLayer) (map[string][]byte, error) {
+ legacyConfigs := []string{
+ legacyBucketObjectLockEnabledConfigFile,
+ bucketPolicyConfig,
+ bucketNotificationConfig,
+ bucketLifecycleConfig,
+ bucketQuotaConfigFile,
+ bucketSSEConfig,
+ bucketTaggingConfig,
+ bucketReplicationConfig,
+ bucketTargetsFile,
+ objectLockConfig,
+ }
+
+ configs := make(map[string][]byte, len(legacyConfigs))
+
+ // Handle migration from lockEnabled to newer format.
+ if b.LockEnabled {
+ configs[objectLockConfig] = enabledBucketObjectLockConfig
+ b.LockEnabled = false // legacy value unset it
+ // we are only interested in b.ObjectLockConfigXML or objectLockConfig value
+ }
+
+ for _, legacyFile := range legacyConfigs {
+ configFile := path.Join(bucketMetaPrefix, b.Name, legacyFile)
+
+ configData, info, err := readConfigWithMetadata(ctx, objectAPI, configFile, ObjectOptions{})
+ if err != nil {
+ if _, ok := err.(ObjectExistsAsDirectory); ok {
+ // in FS mode it possible that we have actual
+ // files in this folder with `.minio.sys/buckets/bucket/configFile`
+ continue
+ }
+ if errors.Is(err, errConfigNotFound) {
+ // legacy file config not found, proceed to look for new metadata.
+ continue
+ }
+
+ return nil, err
+ }
+ configs[legacyFile] = configData
+ b.Created = info.ModTime
+ }
+
+ return configs, nil
+}
+
+func (b *BucketMetadata) convertLegacyConfigs(ctx context.Context, objectAPI ObjectLayer, configs map[string][]byte) error {
+ for legacyFile, configData := range configs {
+ switch legacyFile {
+ case legacyBucketObjectLockEnabledConfigFile:
+ if string(configData) == legacyBucketObjectLockEnabledConfig {
+ b.ObjectLockConfigXML = enabledBucketObjectLockConfig
+ b.VersioningConfigXML = enabledBucketVersioningConfig
+ b.LockEnabled = false // legacy value unset it
+ // we are only interested in b.ObjectLockConfigXML
+ }
+ case bucketPolicyConfig:
+ b.PolicyConfigJSON = configData
+ case bucketNotificationConfig:
+ b.NotificationConfigXML = configData
+ case bucketLifecycleConfig:
+ b.LifecycleConfigXML = configData
+ case bucketSSEConfig:
+ b.EncryptionConfigXML = configData
+ case bucketTaggingConfig:
+ b.TaggingConfigXML = configData
+ case objectLockConfig:
+ b.ObjectLockConfigXML = configData
+ b.VersioningConfigXML = enabledBucketVersioningConfig
+ case bucketQuotaConfigFile:
+ b.QuotaConfigJSON = configData
+ case bucketReplicationConfig:
+ b.ReplicationConfigXML = configData
+ case bucketTargetsFile:
+ b.BucketTargetsConfigJSON = configData
+ }
+ }
+ b.defaultTimestamps()
+
+ if err := b.Save(ctx, objectAPI); err != nil {
+ return err
+ }
+
+ for legacyFile := range configs {
+ configFile := path.Join(bucketMetaPrefix, b.Name, legacyFile)
+ if err := deleteConfig(ctx, objectAPI, configFile); err != nil && !errors.Is(err, errConfigNotFound) {
+ internalLogIf(ctx, err, logger.WarningKind)
+ }
+ }
+
+ return nil
+}
+
+// default timestamps to metadata Created timestamp if unset.
+func (b *BucketMetadata) defaultTimestamps() {
+ if b.PolicyConfigUpdatedAt.IsZero() {
+ b.PolicyConfigUpdatedAt = b.Created
+ }
+
+ if b.EncryptionConfigUpdatedAt.IsZero() {
+ b.EncryptionConfigUpdatedAt = b.Created
+ }
+
+ if b.TaggingConfigUpdatedAt.IsZero() {
+ b.TaggingConfigUpdatedAt = b.Created
+ }
+
+ if b.ObjectLockConfigUpdatedAt.IsZero() {
+ b.ObjectLockConfigUpdatedAt = b.Created
+ }
+
+ if b.QuotaConfigUpdatedAt.IsZero() {
+ b.QuotaConfigUpdatedAt = b.Created
+ }
+
+ if b.ReplicationConfigUpdatedAt.IsZero() {
+ b.ReplicationConfigUpdatedAt = b.Created
+ }
+
+ if b.VersioningConfigUpdatedAt.IsZero() {
+ b.VersioningConfigUpdatedAt = b.Created
+ }
+
+ if b.LifecycleConfigUpdatedAt.IsZero() {
+ b.LifecycleConfigUpdatedAt = b.Created
+ }
+
+ if b.NotificationConfigUpdatedAt.IsZero() {
+ b.NotificationConfigUpdatedAt = b.Created
+ }
+
+ if b.BucketTargetsConfigUpdatedAt.IsZero() {
+ b.BucketTargetsConfigUpdatedAt = b.Created
+ }
+
+ if b.BucketTargetsConfigMetaUpdatedAt.IsZero() {
+ b.BucketTargetsConfigMetaUpdatedAt = b.Created
+ }
+}
+
+// Save config to supplied ObjectLayer api.
+func (b *BucketMetadata) Save(ctx context.Context, api ObjectLayer) error {
+ if err := b.parseAllConfigs(ctx, api); err != nil {
+ return err
+ }
+
+ data := make([]byte, 4, b.Msgsize()+4)
+
+ // Initialize the header.
+ binary.LittleEndian.PutUint16(data[0:2], bucketMetadataFormat)
+ binary.LittleEndian.PutUint16(data[2:4], bucketMetadataVersion)
+
+ // Marshal the bucket metadata
+ data, err := b.MarshalMsg(data)
+ if err != nil {
+ return err
+ }
+
+ configFile := path.Join(bucketMetaPrefix, b.Name, bucketMetadataFile)
+ return saveConfig(ctx, api, configFile, data)
+}
+
+// migrate config for remote targets by encrypting data if currently unencrypted and kms is configured.
+func (b *BucketMetadata) migrateTargetConfig(ctx context.Context, objectAPI ObjectLayer) error {
+ var err error
+ // early return if no targets or already encrypted
+ if len(b.BucketTargetsConfigJSON) == 0 || GlobalKMS == nil || len(b.BucketTargetsConfigMetaJSON) != 0 {
+ return nil
+ }
+
+ encBytes, metaBytes, err := encryptBucketMetadata(ctx, b.Name, b.BucketTargetsConfigJSON, kms.Context{b.Name: b.Name, bucketTargetsFile: bucketTargetsFile})
+ if err != nil {
+ return err
+ }
+
+ b.BucketTargetsConfigJSON = encBytes
+ b.BucketTargetsConfigMetaJSON = metaBytes
+ return b.Save(ctx, objectAPI)
+}
+
+// encrypt bucket metadata if kms is configured.
+func encryptBucketMetadata(ctx context.Context, bucket string, input []byte, kmsContext kms.Context) (output, metabytes []byte, err error) {
+ if GlobalKMS == nil {
+ output = input
+ return output, metabytes, err
+ }
+
+ metadata := make(map[string]string)
+ key, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{AssociatedData: kmsContext})
+ if err != nil {
+ return output, metabytes, err
+ }
+
+ outbuf := bytes.NewBuffer(nil)
+ objectKey := crypto.GenerateKey(key.Plaintext, rand.Reader)
+ sealedKey := objectKey.Seal(key.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, "")
+ crypto.S3.CreateMetadata(metadata, key.KeyID, key.Ciphertext, sealedKey)
+ _, err = sio.Encrypt(outbuf, bytes.NewBuffer(input), sio.Config{Key: objectKey[:], MinVersion: sio.Version20})
+ if err != nil {
+ return output, metabytes, err
+ }
+ metabytes, err = json.Marshal(metadata)
+ if err != nil {
+ return output, metabytes, err
+ }
+ return outbuf.Bytes(), metabytes, nil
+}
+
+// decrypt bucket metadata if kms is configured.
+func decryptBucketMetadata(input []byte, bucket string, meta map[string]string, kmsContext kms.Context) ([]byte, error) {
+ if GlobalKMS == nil {
+ return nil, errKMSNotConfigured
+ }
+ keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(meta)
+ if err != nil {
+ return nil, err
+ }
+ extKey, err := GlobalKMS.Decrypt(context.TODO(), &kms.DecryptRequest{
+ Name: keyID,
+ Ciphertext: kmsKey,
+ AssociatedData: kmsContext,
+ })
+ if err != nil {
+ return nil, err
+ }
+ var objectKey crypto.ObjectKey
+ if err = objectKey.Unseal(extKey, sealedKey, crypto.S3.String(), bucket, ""); err != nil {
+ return nil, err
+ }
+
+ outbuf := bytes.NewBuffer(nil)
+ _, err = sio.Decrypt(outbuf, bytes.NewBuffer(input), sio.Config{Key: objectKey[:], MinVersion: sio.Version20})
+ return outbuf.Bytes(), err
+}
diff --git a/cmd/bucket-metadata_gen.go b/cmd/bucket-metadata_gen.go
new file mode 100644
index 00000000..0407b66e
--- /dev/null
+++ b/cmd/bucket-metadata_gen.go
@@ -0,0 +1,710 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "github.com/tinylib/msgp/msgp"
+)
+
+// DecodeMsg implements msgp.Decodable
+func (z *BucketMetadata) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Name":
+ z.Name, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Name")
+ return
+ }
+ case "Created":
+ z.Created, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "Created")
+ return
+ }
+ case "LockEnabled":
+ z.LockEnabled, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "LockEnabled")
+ return
+ }
+ case "PolicyConfigJSON":
+ z.PolicyConfigJSON, err = dc.ReadBytes(z.PolicyConfigJSON)
+ if err != nil {
+ err = msgp.WrapError(err, "PolicyConfigJSON")
+ return
+ }
+ case "NotificationConfigXML":
+ z.NotificationConfigXML, err = dc.ReadBytes(z.NotificationConfigXML)
+ if err != nil {
+ err = msgp.WrapError(err, "NotificationConfigXML")
+ return
+ }
+ case "LifecycleConfigXML":
+ z.LifecycleConfigXML, err = dc.ReadBytes(z.LifecycleConfigXML)
+ if err != nil {
+ err = msgp.WrapError(err, "LifecycleConfigXML")
+ return
+ }
+ case "ObjectLockConfigXML":
+ z.ObjectLockConfigXML, err = dc.ReadBytes(z.ObjectLockConfigXML)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjectLockConfigXML")
+ return
+ }
+ case "VersioningConfigXML":
+ z.VersioningConfigXML, err = dc.ReadBytes(z.VersioningConfigXML)
+ if err != nil {
+ err = msgp.WrapError(err, "VersioningConfigXML")
+ return
+ }
+ case "EncryptionConfigXML":
+ z.EncryptionConfigXML, err = dc.ReadBytes(z.EncryptionConfigXML)
+ if err != nil {
+ err = msgp.WrapError(err, "EncryptionConfigXML")
+ return
+ }
+ case "TaggingConfigXML":
+ z.TaggingConfigXML, err = dc.ReadBytes(z.TaggingConfigXML)
+ if err != nil {
+ err = msgp.WrapError(err, "TaggingConfigXML")
+ return
+ }
+ case "QuotaConfigJSON":
+ z.QuotaConfigJSON, err = dc.ReadBytes(z.QuotaConfigJSON)
+ if err != nil {
+ err = msgp.WrapError(err, "QuotaConfigJSON")
+ return
+ }
+ case "ReplicationConfigXML":
+ z.ReplicationConfigXML, err = dc.ReadBytes(z.ReplicationConfigXML)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationConfigXML")
+ return
+ }
+ case "BucketTargetsConfigJSON":
+ z.BucketTargetsConfigJSON, err = dc.ReadBytes(z.BucketTargetsConfigJSON)
+ if err != nil {
+ err = msgp.WrapError(err, "BucketTargetsConfigJSON")
+ return
+ }
+ case "BucketTargetsConfigMetaJSON":
+ z.BucketTargetsConfigMetaJSON, err = dc.ReadBytes(z.BucketTargetsConfigMetaJSON)
+ if err != nil {
+ err = msgp.WrapError(err, "BucketTargetsConfigMetaJSON")
+ return
+ }
+ case "PolicyConfigUpdatedAt":
+ z.PolicyConfigUpdatedAt, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "PolicyConfigUpdatedAt")
+ return
+ }
+ case "ObjectLockConfigUpdatedAt":
+ z.ObjectLockConfigUpdatedAt, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "ObjectLockConfigUpdatedAt")
+ return
+ }
+ case "EncryptionConfigUpdatedAt":
+ z.EncryptionConfigUpdatedAt, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "EncryptionConfigUpdatedAt")
+ return
+ }
+ case "TaggingConfigUpdatedAt":
+ z.TaggingConfigUpdatedAt, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "TaggingConfigUpdatedAt")
+ return
+ }
+ case "QuotaConfigUpdatedAt":
+ z.QuotaConfigUpdatedAt, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "QuotaConfigUpdatedAt")
+ return
+ }
+ case "ReplicationConfigUpdatedAt":
+ z.ReplicationConfigUpdatedAt, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationConfigUpdatedAt")
+ return
+ }
+ case "VersioningConfigUpdatedAt":
+ z.VersioningConfigUpdatedAt, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "VersioningConfigUpdatedAt")
+ return
+ }
+ case "LifecycleConfigUpdatedAt":
+ z.LifecycleConfigUpdatedAt, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "LifecycleConfigUpdatedAt")
+ return
+ }
+ case "NotificationConfigUpdatedAt":
+ z.NotificationConfigUpdatedAt, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "NotificationConfigUpdatedAt")
+ return
+ }
+ case "BucketTargetsConfigUpdatedAt":
+ z.BucketTargetsConfigUpdatedAt, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "BucketTargetsConfigUpdatedAt")
+ return
+ }
+ case "BucketTargetsConfigMetaUpdatedAt":
+ z.BucketTargetsConfigMetaUpdatedAt, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "BucketTargetsConfigMetaUpdatedAt")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *BucketMetadata) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 25
+ // write "Name"
+ err = en.Append(0xde, 0x0, 0x19, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Name)
+ if err != nil {
+ err = msgp.WrapError(err, "Name")
+ return
+ }
+ // write "Created"
+ err = en.Append(0xa7, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.Created)
+ if err != nil {
+ err = msgp.WrapError(err, "Created")
+ return
+ }
+ // write "LockEnabled"
+ err = en.Append(0xab, 0x4c, 0x6f, 0x63, 0x6b, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.LockEnabled)
+ if err != nil {
+ err = msgp.WrapError(err, "LockEnabled")
+ return
+ }
+ // write "PolicyConfigJSON"
+ err = en.Append(0xb0, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4a, 0x53, 0x4f, 0x4e)
+ if err != nil {
+ return
+ }
+ err = en.WriteBytes(z.PolicyConfigJSON)
+ if err != nil {
+ err = msgp.WrapError(err, "PolicyConfigJSON")
+ return
+ }
+ // write "NotificationConfigXML"
+ err = en.Append(0xb5, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
+ if err != nil {
+ return
+ }
+ err = en.WriteBytes(z.NotificationConfigXML)
+ if err != nil {
+ err = msgp.WrapError(err, "NotificationConfigXML")
+ return
+ }
+ // write "LifecycleConfigXML"
+ err = en.Append(0xb2, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
+ if err != nil {
+ return
+ }
+ err = en.WriteBytes(z.LifecycleConfigXML)
+ if err != nil {
+ err = msgp.WrapError(err, "LifecycleConfigXML")
+ return
+ }
+ // write "ObjectLockConfigXML"
+ err = en.Append(0xb3, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4c, 0x6f, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
+ if err != nil {
+ return
+ }
+ err = en.WriteBytes(z.ObjectLockConfigXML)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjectLockConfigXML")
+ return
+ }
+ // write "VersioningConfigXML"
+ err = en.Append(0xb3, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
+ if err != nil {
+ return
+ }
+ err = en.WriteBytes(z.VersioningConfigXML)
+ if err != nil {
+ err = msgp.WrapError(err, "VersioningConfigXML")
+ return
+ }
+ // write "EncryptionConfigXML"
+ err = en.Append(0xb3, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
+ if err != nil {
+ return
+ }
+ err = en.WriteBytes(z.EncryptionConfigXML)
+ if err != nil {
+ err = msgp.WrapError(err, "EncryptionConfigXML")
+ return
+ }
+ // write "TaggingConfigXML"
+ err = en.Append(0xb0, 0x54, 0x61, 0x67, 0x67, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
+ if err != nil {
+ return
+ }
+ err = en.WriteBytes(z.TaggingConfigXML)
+ if err != nil {
+ err = msgp.WrapError(err, "TaggingConfigXML")
+ return
+ }
+ // write "QuotaConfigJSON"
+ err = en.Append(0xaf, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4a, 0x53, 0x4f, 0x4e)
+ if err != nil {
+ return
+ }
+ err = en.WriteBytes(z.QuotaConfigJSON)
+ if err != nil {
+ err = msgp.WrapError(err, "QuotaConfigJSON")
+ return
+ }
+ // write "ReplicationConfigXML"
+ err = en.Append(0xb4, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
+ if err != nil {
+ return
+ }
+ err = en.WriteBytes(z.ReplicationConfigXML)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationConfigXML")
+ return
+ }
+ // write "BucketTargetsConfigJSON"
+ err = en.Append(0xb7, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4a, 0x53, 0x4f, 0x4e)
+ if err != nil {
+ return
+ }
+ err = en.WriteBytes(z.BucketTargetsConfigJSON)
+ if err != nil {
+ err = msgp.WrapError(err, "BucketTargetsConfigJSON")
+ return
+ }
+ // write "BucketTargetsConfigMetaJSON"
+ err = en.Append(0xbb, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4d, 0x65, 0x74, 0x61, 0x4a, 0x53, 0x4f, 0x4e)
+ if err != nil {
+ return
+ }
+ err = en.WriteBytes(z.BucketTargetsConfigMetaJSON)
+ if err != nil {
+ err = msgp.WrapError(err, "BucketTargetsConfigMetaJSON")
+ return
+ }
+ // write "PolicyConfigUpdatedAt"
+ err = en.Append(0xb5, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.PolicyConfigUpdatedAt)
+ if err != nil {
+ err = msgp.WrapError(err, "PolicyConfigUpdatedAt")
+ return
+ }
+ // write "ObjectLockConfigUpdatedAt"
+ err = en.Append(0xb9, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4c, 0x6f, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.ObjectLockConfigUpdatedAt)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjectLockConfigUpdatedAt")
+ return
+ }
+ // write "EncryptionConfigUpdatedAt"
+ err = en.Append(0xb9, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.EncryptionConfigUpdatedAt)
+ if err != nil {
+ err = msgp.WrapError(err, "EncryptionConfigUpdatedAt")
+ return
+ }
+ // write "TaggingConfigUpdatedAt"
+ err = en.Append(0xb6, 0x54, 0x61, 0x67, 0x67, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.TaggingConfigUpdatedAt)
+ if err != nil {
+ err = msgp.WrapError(err, "TaggingConfigUpdatedAt")
+ return
+ }
+ // write "QuotaConfigUpdatedAt"
+ err = en.Append(0xb4, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.QuotaConfigUpdatedAt)
+ if err != nil {
+ err = msgp.WrapError(err, "QuotaConfigUpdatedAt")
+ return
+ }
+ // write "ReplicationConfigUpdatedAt"
+ err = en.Append(0xba, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.ReplicationConfigUpdatedAt)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationConfigUpdatedAt")
+ return
+ }
+ // write "VersioningConfigUpdatedAt"
+ err = en.Append(0xb9, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.VersioningConfigUpdatedAt)
+ if err != nil {
+ err = msgp.WrapError(err, "VersioningConfigUpdatedAt")
+ return
+ }
+ // write "LifecycleConfigUpdatedAt"
+ err = en.Append(0xb8, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.LifecycleConfigUpdatedAt)
+ if err != nil {
+ err = msgp.WrapError(err, "LifecycleConfigUpdatedAt")
+ return
+ }
+ // write "NotificationConfigUpdatedAt"
+ err = en.Append(0xbb, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.NotificationConfigUpdatedAt)
+ if err != nil {
+ err = msgp.WrapError(err, "NotificationConfigUpdatedAt")
+ return
+ }
+ // write "BucketTargetsConfigUpdatedAt"
+ err = en.Append(0xbc, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.BucketTargetsConfigUpdatedAt)
+ if err != nil {
+ err = msgp.WrapError(err, "BucketTargetsConfigUpdatedAt")
+ return
+ }
+ // write "BucketTargetsConfigMetaUpdatedAt"
+ err = en.Append(0xd9, 0x20, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4d, 0x65, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.BucketTargetsConfigMetaUpdatedAt)
+ if err != nil {
+ err = msgp.WrapError(err, "BucketTargetsConfigMetaUpdatedAt")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *BucketMetadata) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 25
+ // string "Name"
+ o = append(o, 0xde, 0x0, 0x19, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
+ o = msgp.AppendString(o, z.Name)
+ // string "Created"
+ o = append(o, 0xa7, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64)
+ o = msgp.AppendTime(o, z.Created)
+ // string "LockEnabled"
+ o = append(o, 0xab, 0x4c, 0x6f, 0x63, 0x6b, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64)
+ o = msgp.AppendBool(o, z.LockEnabled)
+ // string "PolicyConfigJSON"
+ o = append(o, 0xb0, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4a, 0x53, 0x4f, 0x4e)
+ o = msgp.AppendBytes(o, z.PolicyConfigJSON)
+ // string "NotificationConfigXML"
+ o = append(o, 0xb5, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
+ o = msgp.AppendBytes(o, z.NotificationConfigXML)
+ // string "LifecycleConfigXML"
+ o = append(o, 0xb2, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
+ o = msgp.AppendBytes(o, z.LifecycleConfigXML)
+ // string "ObjectLockConfigXML"
+ o = append(o, 0xb3, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4c, 0x6f, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
+ o = msgp.AppendBytes(o, z.ObjectLockConfigXML)
+ // string "VersioningConfigXML"
+ o = append(o, 0xb3, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
+ o = msgp.AppendBytes(o, z.VersioningConfigXML)
+ // string "EncryptionConfigXML"
+ o = append(o, 0xb3, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
+ o = msgp.AppendBytes(o, z.EncryptionConfigXML)
+ // string "TaggingConfigXML"
+ o = append(o, 0xb0, 0x54, 0x61, 0x67, 0x67, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
+ o = msgp.AppendBytes(o, z.TaggingConfigXML)
+ // string "QuotaConfigJSON"
+ o = append(o, 0xaf, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4a, 0x53, 0x4f, 0x4e)
+ o = msgp.AppendBytes(o, z.QuotaConfigJSON)
+ // string "ReplicationConfigXML"
+ o = append(o, 0xb4, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
+ o = msgp.AppendBytes(o, z.ReplicationConfigXML)
+ // string "BucketTargetsConfigJSON"
+ o = append(o, 0xb7, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4a, 0x53, 0x4f, 0x4e)
+ o = msgp.AppendBytes(o, z.BucketTargetsConfigJSON)
+ // string "BucketTargetsConfigMetaJSON"
+ o = append(o, 0xbb, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4d, 0x65, 0x74, 0x61, 0x4a, 0x53, 0x4f, 0x4e)
+ o = msgp.AppendBytes(o, z.BucketTargetsConfigMetaJSON)
+ // string "PolicyConfigUpdatedAt"
+ o = append(o, 0xb5, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ o = msgp.AppendTime(o, z.PolicyConfigUpdatedAt)
+ // string "ObjectLockConfigUpdatedAt"
+ o = append(o, 0xb9, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4c, 0x6f, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ o = msgp.AppendTime(o, z.ObjectLockConfigUpdatedAt)
+ // string "EncryptionConfigUpdatedAt"
+ o = append(o, 0xb9, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ o = msgp.AppendTime(o, z.EncryptionConfigUpdatedAt)
+ // string "TaggingConfigUpdatedAt"
+ o = append(o, 0xb6, 0x54, 0x61, 0x67, 0x67, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ o = msgp.AppendTime(o, z.TaggingConfigUpdatedAt)
+ // string "QuotaConfigUpdatedAt"
+ o = append(o, 0xb4, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ o = msgp.AppendTime(o, z.QuotaConfigUpdatedAt)
+ // string "ReplicationConfigUpdatedAt"
+ o = append(o, 0xba, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ o = msgp.AppendTime(o, z.ReplicationConfigUpdatedAt)
+ // string "VersioningConfigUpdatedAt"
+ o = append(o, 0xb9, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ o = msgp.AppendTime(o, z.VersioningConfigUpdatedAt)
+ // string "LifecycleConfigUpdatedAt"
+ o = append(o, 0xb8, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ o = msgp.AppendTime(o, z.LifecycleConfigUpdatedAt)
+ // string "NotificationConfigUpdatedAt"
+ o = append(o, 0xbb, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ o = msgp.AppendTime(o, z.NotificationConfigUpdatedAt)
+ // string "BucketTargetsConfigUpdatedAt"
+ o = append(o, 0xbc, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ o = msgp.AppendTime(o, z.BucketTargetsConfigUpdatedAt)
+ // string "BucketTargetsConfigMetaUpdatedAt"
+ o = append(o, 0xd9, 0x20, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4d, 0x65, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ o = msgp.AppendTime(o, z.BucketTargetsConfigMetaUpdatedAt)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BucketMetadata) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Name":
+ z.Name, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Name")
+ return
+ }
+ case "Created":
+ z.Created, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Created")
+ return
+ }
+ case "LockEnabled":
+ z.LockEnabled, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "LockEnabled")
+ return
+ }
+ case "PolicyConfigJSON":
+ z.PolicyConfigJSON, bts, err = msgp.ReadBytesBytes(bts, z.PolicyConfigJSON)
+ if err != nil {
+ err = msgp.WrapError(err, "PolicyConfigJSON")
+ return
+ }
+ case "NotificationConfigXML":
+ z.NotificationConfigXML, bts, err = msgp.ReadBytesBytes(bts, z.NotificationConfigXML)
+ if err != nil {
+ err = msgp.WrapError(err, "NotificationConfigXML")
+ return
+ }
+ case "LifecycleConfigXML":
+ z.LifecycleConfigXML, bts, err = msgp.ReadBytesBytes(bts, z.LifecycleConfigXML)
+ if err != nil {
+ err = msgp.WrapError(err, "LifecycleConfigXML")
+ return
+ }
+ case "ObjectLockConfigXML":
+ z.ObjectLockConfigXML, bts, err = msgp.ReadBytesBytes(bts, z.ObjectLockConfigXML)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjectLockConfigXML")
+ return
+ }
+ case "VersioningConfigXML":
+ z.VersioningConfigXML, bts, err = msgp.ReadBytesBytes(bts, z.VersioningConfigXML)
+ if err != nil {
+ err = msgp.WrapError(err, "VersioningConfigXML")
+ return
+ }
+ case "EncryptionConfigXML":
+ z.EncryptionConfigXML, bts, err = msgp.ReadBytesBytes(bts, z.EncryptionConfigXML)
+ if err != nil {
+ err = msgp.WrapError(err, "EncryptionConfigXML")
+ return
+ }
+ case "TaggingConfigXML":
+ z.TaggingConfigXML, bts, err = msgp.ReadBytesBytes(bts, z.TaggingConfigXML)
+ if err != nil {
+ err = msgp.WrapError(err, "TaggingConfigXML")
+ return
+ }
+ case "QuotaConfigJSON":
+ z.QuotaConfigJSON, bts, err = msgp.ReadBytesBytes(bts, z.QuotaConfigJSON)
+ if err != nil {
+ err = msgp.WrapError(err, "QuotaConfigJSON")
+ return
+ }
+ case "ReplicationConfigXML":
+ z.ReplicationConfigXML, bts, err = msgp.ReadBytesBytes(bts, z.ReplicationConfigXML)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationConfigXML")
+ return
+ }
+ case "BucketTargetsConfigJSON":
+ z.BucketTargetsConfigJSON, bts, err = msgp.ReadBytesBytes(bts, z.BucketTargetsConfigJSON)
+ if err != nil {
+ err = msgp.WrapError(err, "BucketTargetsConfigJSON")
+ return
+ }
+ case "BucketTargetsConfigMetaJSON":
+ z.BucketTargetsConfigMetaJSON, bts, err = msgp.ReadBytesBytes(bts, z.BucketTargetsConfigMetaJSON)
+ if err != nil {
+ err = msgp.WrapError(err, "BucketTargetsConfigMetaJSON")
+ return
+ }
+ case "PolicyConfigUpdatedAt":
+ z.PolicyConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "PolicyConfigUpdatedAt")
+ return
+ }
+ case "ObjectLockConfigUpdatedAt":
+ z.ObjectLockConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjectLockConfigUpdatedAt")
+ return
+ }
+ case "EncryptionConfigUpdatedAt":
+ z.EncryptionConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "EncryptionConfigUpdatedAt")
+ return
+ }
+ case "TaggingConfigUpdatedAt":
+ z.TaggingConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "TaggingConfigUpdatedAt")
+ return
+ }
+ case "QuotaConfigUpdatedAt":
+ z.QuotaConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "QuotaConfigUpdatedAt")
+ return
+ }
+ case "ReplicationConfigUpdatedAt":
+ z.ReplicationConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationConfigUpdatedAt")
+ return
+ }
+ case "VersioningConfigUpdatedAt":
+ z.VersioningConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "VersioningConfigUpdatedAt")
+ return
+ }
+ case "LifecycleConfigUpdatedAt":
+ z.LifecycleConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "LifecycleConfigUpdatedAt")
+ return
+ }
+ case "NotificationConfigUpdatedAt":
+ z.NotificationConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "NotificationConfigUpdatedAt")
+ return
+ }
+ case "BucketTargetsConfigUpdatedAt":
+ z.BucketTargetsConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "BucketTargetsConfigUpdatedAt")
+ return
+ }
+ case "BucketTargetsConfigMetaUpdatedAt":
+ z.BucketTargetsConfigMetaUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "BucketTargetsConfigMetaUpdatedAt")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *BucketMetadata) Msgsize() (s int) {
+ s = 3 + 5 + msgp.StringPrefixSize + len(z.Name) + 8 + msgp.TimeSize + 12 + msgp.BoolSize + 17 + msgp.BytesPrefixSize + len(z.PolicyConfigJSON) + 22 + msgp.BytesPrefixSize + len(z.NotificationConfigXML) + 19 + msgp.BytesPrefixSize + len(z.LifecycleConfigXML) + 20 + msgp.BytesPrefixSize + len(z.ObjectLockConfigXML) + 20 + msgp.BytesPrefixSize + len(z.VersioningConfigXML) + 20 + msgp.BytesPrefixSize + len(z.EncryptionConfigXML) + 17 + msgp.BytesPrefixSize + len(z.TaggingConfigXML) + 16 + msgp.BytesPrefixSize + len(z.QuotaConfigJSON) + 21 + msgp.BytesPrefixSize + len(z.ReplicationConfigXML) + 24 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigJSON) + 28 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigMetaJSON) + 22 + msgp.TimeSize + 26 + msgp.TimeSize + 26 + msgp.TimeSize + 23 + msgp.TimeSize + 21 + msgp.TimeSize + 27 + msgp.TimeSize + 26 + msgp.TimeSize + 25 + msgp.TimeSize + 28 + msgp.TimeSize + 29 + msgp.TimeSize + 34 + msgp.TimeSize
+ return
+}
diff --git a/cmd/bucket-metadata_gen_test.go b/cmd/bucket-metadata_gen_test.go
new file mode 100644
index 00000000..4c2d25c7
--- /dev/null
+++ b/cmd/bucket-metadata_gen_test.go
@@ -0,0 +1,123 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+func TestMarshalUnmarshalBucketMetadata(t *testing.T) {
+ v := BucketMetadata{}
+ bts, err := v.MarshalMsg(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ left, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
+ }
+
+ left, err = msgp.Skip(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
+ }
+}
+
+func BenchmarkMarshalMsgBucketMetadata(b *testing.B) {
+ v := BucketMetadata{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBucketMetadata(b *testing.B) {
+ v := BucketMetadata{}
+ bts := make([]byte, 0, v.Msgsize())
+ bts, _ = v.MarshalMsg(bts[0:0])
+ b.SetBytes(int64(len(bts)))
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ bts, _ = v.MarshalMsg(bts[0:0])
+ }
+}
+
+func BenchmarkUnmarshalBucketMetadata(b *testing.B) {
+ v := BucketMetadata{}
+ bts, _ := v.MarshalMsg(nil)
+ b.ReportAllocs()
+ b.SetBytes(int64(len(bts)))
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func TestEncodeDecodeBucketMetadata(t *testing.T) {
+ v := BucketMetadata{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBucketMetadata Msgsize() is inaccurate")
+ }
+
+ vn := BucketMetadata{}
+ err := msgp.Decode(&buf, &vn)
+ if err != nil {
+ t.Error(err)
+ }
+
+ buf.Reset()
+ msgp.Encode(&buf, &v)
+ err = msgp.NewReader(&buf).Skip()
+ if err != nil {
+ t.Error(err)
+ }
+}
+
+func BenchmarkEncodeBucketMetadata(b *testing.B) {
+ v := BucketMetadata{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+ b.SetBytes(int64(buf.Len()))
+ en := msgp.NewWriter(msgp.Nowhere)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.EncodeMsg(en)
+ }
+ en.Flush()
+}
+
+func BenchmarkDecodeBucketMetadata(b *testing.B) {
+ v := BucketMetadata{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+ b.SetBytes(int64(buf.Len()))
+ rd := msgp.NewEndlessReader(buf.Bytes(), b)
+ dc := msgp.NewReader(rd)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ err := v.DecodeMsg(dc)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
diff --git a/cmd/bucket-notification-handlers.go b/cmd/bucket-notification-handlers.go
new file mode 100644
index 00000000..c41823b4
--- /dev/null
+++ b/cmd/bucket-notification-handlers.go
@@ -0,0 +1,163 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "encoding/xml"
+ "io"
+ "net/http"
+ "reflect"
+
+ "github.com/minio/minio/internal/event"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/mux"
+ "github.com/minio/pkg/v3/policy"
+)
+
+const (
+ bucketNotificationConfig = "notification.xml"
+)
+
+// GetBucketNotificationHandler - This HTTP handler returns event notification configuration
+// as per http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html.
+// It returns empty configuration if its not set.
+func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetBucketNotification")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucketName := vars["bucket"]
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketNotificationAction, bucketName, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ _, err := objAPI.GetBucketInfo(ctx, bucketName, BucketOptions{})
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ config, err := globalBucketMetadataSys.GetNotificationConfig(bucketName)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ region := globalSite.Region()
+ config.SetRegion(region)
+ if err = config.Validate(region, globalEventNotifier.targetList); err != nil {
+ arnErr, ok := err.(*event.ErrARNNotFound)
+ if ok {
+ for i, queue := range config.QueueList {
+ // Remove ARN not found queues, because we previously allowed
+ // adding unexpected entries into the config.
+ //
+ // With newer config disallowing changing / turning off
+ // notification targets without removing ARN in notification
+ // configuration we won't see this problem anymore.
+ if reflect.DeepEqual(queue.ARN, arnErr.ARN) && i < len(config.QueueList) {
+ config.QueueList = append(config.QueueList[:i],
+ config.QueueList[i+1:]...)
+ }
+ // This is a one time activity we shall do this
+ // here and allow stale ARN to be removed. We shall
+ // never reach a stage where we will have stale
+ // notification configs.
+ }
+ } else {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ }
+
+ configData, err := xml.Marshal(config)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ writeSuccessResponseXML(w, configData)
+}
+
+// PutBucketNotificationHandler - This HTTP handler stores given notification configuration as per
+// http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html.
+func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "PutBucketNotification")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucketName := vars["bucket"]
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketNotificationAction, bucketName, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ _, err := objectAPI.GetBucketInfo(ctx, bucketName, BucketOptions{})
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // PutBucketNotification always needs a Content-Length.
+ if r.ContentLength <= 0 {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL)
+ return
+ }
+
+ config, err := event.ParseConfig(io.LimitReader(r.Body, r.ContentLength), globalSite.Region(), globalEventNotifier.targetList)
+ if err != nil {
+ apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
+ if event.IsEventError(err) {
+ apiErr = toAPIError(ctx, err)
+ }
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+
+ configData, err := xml.Marshal(config)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if _, err = globalBucketMetadataSys.Update(ctx, bucketName, bucketNotificationConfig, configData); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ rulesMap := config.ToRulesMap()
+ globalEventNotifier.AddRulesMap(bucketName, rulesMap)
+
+ writeSuccessResponseHeadersOnly(w)
+}
diff --git a/cmd/bucket-object-lock.go b/cmd/bucket-object-lock.go
new file mode 100644
index 00000000..d0ad8514
--- /dev/null
+++ b/cmd/bucket-object-lock.go
@@ -0,0 +1,345 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "errors"
+ "math"
+ "net/http"
+
+ "github.com/minio/minio/internal/auth"
+ objectlock "github.com/minio/minio/internal/bucket/object/lock"
+ "github.com/minio/minio/internal/bucket/replication"
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/policy"
+)
+
+// BucketObjectLockSys - map of bucket and retention configuration.
+type BucketObjectLockSys struct{}
+
+// Get - Get retention configuration.
+func (sys *BucketObjectLockSys) Get(bucketName string) (r objectlock.Retention, err error) {
+ config, _, err := globalBucketMetadataSys.GetObjectLockConfig(bucketName)
+ if err != nil {
+ if errors.Is(err, BucketObjectLockConfigNotFound{Bucket: bucketName}) {
+ return r, nil
+ }
+ if errors.Is(err, errInvalidArgument) {
+ return r, err
+ }
+ return r, err
+ }
+ return config.ToRetention(), nil
+}
+
+// enforceRetentionForDeletion checks if it is appropriate to remove an
+// object according to locking configuration when this is lifecycle/ bucket quota asking.
+func enforceRetentionForDeletion(ctx context.Context, objInfo ObjectInfo) (locked bool) {
+ if objInfo.DeleteMarker {
+ return false
+ }
+
+ lhold := objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined)
+ if lhold.Status.Valid() && lhold.Status == objectlock.LegalHoldOn {
+ return true
+ }
+
+ ret := objectlock.GetObjectRetentionMeta(objInfo.UserDefined)
+ if ret.Mode.Valid() && (ret.Mode == objectlock.RetCompliance || ret.Mode == objectlock.RetGovernance) {
+ t, err := objectlock.UTCNowNTP()
+ if err != nil {
+ internalLogIf(ctx, err, logger.WarningKind)
+ return true
+ }
+ if ret.RetainUntilDate.After(t) {
+ return true
+ }
+ }
+ return false
+}
+
+// enforceRetentionBypassForDelete enforces whether an existing object under governance can be deleted
+// with governance bypass headers set in the request.
+// Objects under site wide WORM can never be overwritten.
+// For objects in "Governance" mode, overwrite is allowed if a) object retention date is past OR
+// governance bypass headers are set and user has governance bypass permissions.
+// Objects in "Compliance" mode can be overwritten only if retention date is past.
+func enforceRetentionBypassForDelete(ctx context.Context, r *http.Request, bucket string, object ObjectToDelete, oi ObjectInfo, gerr error) error {
+ if gerr != nil { // error from GetObjectInfo
+ if _, ok := gerr.(MethodNotAllowed); ok {
+ // This happens usually for a delete marker
+ if oi.DeleteMarker || !oi.VersionPurgeStatus.Empty() {
+ // Delete marker should be present and valid.
+ return nil
+ }
+ }
+ if isErrObjectNotFound(gerr) || isErrVersionNotFound(gerr) {
+ return nil
+ }
+ return gerr
+ }
+
+ lhold := objectlock.GetObjectLegalHoldMeta(oi.UserDefined)
+ if lhold.Status.Valid() && lhold.Status == objectlock.LegalHoldOn {
+ return ObjectLocked{}
+ }
+
+ ret := objectlock.GetObjectRetentionMeta(oi.UserDefined)
+ if ret.Mode.Valid() {
+ switch ret.Mode {
+ case objectlock.RetCompliance:
+ // In compliance mode, a protected object version can't be overwritten
+ // or deleted by any user, including the root user in your AWS account.
+ // When an object is locked in compliance mode, its retention mode can't
+ // be changed, and its retention period can't be shortened. Compliance mode
+ // ensures that an object version can't be overwritten or deleted for the
+ // duration of the retention period.
+ t, err := objectlock.UTCNowNTP()
+ if err != nil {
+ internalLogIf(ctx, err, logger.WarningKind)
+ return ObjectLocked{}
+ }
+
+ if !ret.RetainUntilDate.Before(t) {
+ return ObjectLocked{}
+ }
+ return nil
+ case objectlock.RetGovernance:
+ // In governance mode, users can't overwrite or delete an object
+ // version or alter its lock settings unless they have special
+ // permissions. With governance mode, you protect objects against
+ // being deleted by most users, but you can still grant some users
+ // permission to alter the retention settings or delete the object
+ // if necessary. You can also use governance mode to test retention-period
+ // settings before creating a compliance-mode retention period.
+ // To override or remove governance-mode retention settings, a
+ // user must have the s3:BypassGovernanceRetention permission
+ // and must explicitly include x-amz-bypass-governance-retention:true
+ // as a request header with any request that requires overriding
+ // governance mode.
+ //
+ byPassSet := objectlock.IsObjectLockGovernanceBypassSet(r.Header)
+ if !byPassSet {
+ t, err := objectlock.UTCNowNTP()
+ if err != nil {
+ internalLogIf(ctx, err, logger.WarningKind)
+ return ObjectLocked{}
+ }
+
+ if !ret.RetainUntilDate.Before(t) {
+ return ObjectLocked{}
+ }
+ return nil
+ }
+ // https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-overview.html#object-lock-retention-modes
+ // If you try to delete objects protected by governance mode and have s3:BypassGovernanceRetention, the operation will succeed.
+ if checkRequestAuthType(ctx, r, policy.BypassGovernanceRetentionAction, bucket, object.ObjectName) != ErrNone {
+ return errAuthentication
+ }
+ }
+ }
+ return nil
+}
+
+// enforceRetentionBypassForPut enforces whether an existing object under governance can be overwritten
+// with governance bypass headers set in the request.
+// Objects under site wide WORM cannot be overwritten.
+// For objects in "Governance" mode, overwrite is allowed if a) object retention date is past OR
+// governance bypass headers are set and user has governance bypass permissions.
+// Objects in compliance mode can be overwritten only if retention date is being extended. No mode change is permitted.
+func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, oi ObjectInfo, objRetention *objectlock.ObjectRetention, cred auth.Credentials, owner bool) error {
+ byPassSet := objectlock.IsObjectLockGovernanceBypassSet(r.Header)
+
+ t, err := objectlock.UTCNowNTP()
+ if err != nil {
+ internalLogIf(ctx, err, logger.WarningKind)
+ return ObjectLocked{Bucket: oi.Bucket, Object: oi.Name, VersionID: oi.VersionID}
+ }
+
+ // Pass in relative days from current time, to additionally
+ // to verify "object-lock-remaining-retention-days" policy if any.
+ days := int(math.Ceil(math.Abs(objRetention.RetainUntilDate.Sub(t).Hours()) / 24))
+
+ ret := objectlock.GetObjectRetentionMeta(oi.UserDefined)
+ if ret.Mode.Valid() {
+ // Retention has expired you may change whatever you like.
+ if ret.RetainUntilDate.Before(t) {
+ apiErr := isPutRetentionAllowed(oi.Bucket, oi.Name,
+ days, objRetention.RetainUntilDate.Time,
+ objRetention.Mode, byPassSet, r, cred,
+ owner)
+ if apiErr == ErrAccessDenied {
+ return errAuthentication
+ }
+ return nil
+ }
+
+ switch ret.Mode {
+ case objectlock.RetGovernance:
+ govPerm := isPutRetentionAllowed(oi.Bucket, oi.Name, days,
+ objRetention.RetainUntilDate.Time, objRetention.Mode,
+ byPassSet, r, cred, owner)
+ // Governance mode retention period cannot be shortened, if x-amz-bypass-governance is not set.
+ if !byPassSet {
+ if objRetention.Mode != objectlock.RetGovernance || objRetention.RetainUntilDate.Before((ret.RetainUntilDate.Time)) {
+ return ObjectLocked{Bucket: oi.Bucket, Object: oi.Name, VersionID: oi.VersionID}
+ }
+ }
+ if govPerm == ErrAccessDenied {
+ return errAuthentication
+ }
+ return nil
+ case objectlock.RetCompliance:
+ // Compliance retention mode cannot be changed or shortened.
+ // https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-overview.html#object-lock-retention-modes
+ if objRetention.Mode != objectlock.RetCompliance || objRetention.RetainUntilDate.Before((ret.RetainUntilDate.Time)) {
+ return ObjectLocked{Bucket: oi.Bucket, Object: oi.Name, VersionID: oi.VersionID}
+ }
+ apiErr := isPutRetentionAllowed(oi.Bucket, oi.Name,
+ days, objRetention.RetainUntilDate.Time, objRetention.Mode,
+ false, r, cred, owner)
+ if apiErr == ErrAccessDenied {
+ return errAuthentication
+ }
+ return nil
+ }
+ return nil
+ } // No pre-existing retention metadata present.
+
+ apiErr := isPutRetentionAllowed(oi.Bucket, oi.Name,
+ days, objRetention.RetainUntilDate.Time,
+ objRetention.Mode, byPassSet, r, cred, owner)
+ if apiErr == ErrAccessDenied {
+ return errAuthentication
+ }
+ return nil
+}
+
+// checkPutObjectLockAllowed enforces object retention policy and legal hold policy
+// for requests with WORM headers
+// See https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-managing.html for the spec.
+// For non-existing objects with object retention headers set, this method returns ErrNone if bucket has
+// locking enabled and user has requisite permissions (s3:PutObjectRetention)
+// If object exists on object store and site wide WORM enabled - this method
+// returns an error. For objects in "Governance" mode, overwrite is allowed if the retention date has expired.
+// For objects in "Compliance" mode, retention date cannot be shortened, and mode cannot be altered.
+// For objects with legal hold header set, the s3:PutObjectLegalHold permission is expected to be set
+// Both legal hold and retention can be applied independently on an object
+func checkPutObjectLockAllowed(ctx context.Context, rq *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, retentionPermErr, legalHoldPermErr APIErrorCode) (objectlock.RetMode, objectlock.RetentionDate, objectlock.ObjectLegalHold, APIErrorCode) {
+ var mode objectlock.RetMode
+ var retainDate objectlock.RetentionDate
+ var legalHold objectlock.ObjectLegalHold
+
+ retentionRequested := objectlock.IsObjectLockRetentionRequested(rq.Header)
+ legalHoldRequested := objectlock.IsObjectLockLegalHoldRequested(rq.Header)
+
+ retentionCfg, err := globalBucketObjectLockSys.Get(bucket)
+ if err != nil {
+ return mode, retainDate, legalHold, ErrInvalidBucketObjectLockConfiguration
+ }
+
+ if !retentionCfg.LockEnabled {
+ if legalHoldRequested || retentionRequested {
+ return mode, retainDate, legalHold, ErrInvalidBucketObjectLockConfiguration
+ }
+
+ // If this not a WORM enabled bucket, we should return right here.
+ return mode, retainDate, legalHold, ErrNone
+ }
+
+ opts, err := getOpts(ctx, rq, bucket, object)
+ if err != nil {
+ return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
+ }
+
+ replica := rq.Header.Get(xhttp.AmzBucketReplicationStatus) == replication.Replica.String()
+
+ if opts.VersionID != "" && !replica {
+ if objInfo, err := getObjectInfoFn(ctx, bucket, object, opts); err == nil {
+ r := objectlock.GetObjectRetentionMeta(objInfo.UserDefined)
+ t, err := objectlock.UTCNowNTP()
+ if err != nil {
+ internalLogIf(ctx, err, logger.WarningKind)
+ return mode, retainDate, legalHold, ErrObjectLocked
+ }
+ if r.Mode == objectlock.RetCompliance && r.RetainUntilDate.After(t) {
+ return mode, retainDate, legalHold, ErrObjectLocked
+ }
+ mode = r.Mode
+ retainDate = r.RetainUntilDate
+ legalHold = objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined)
+ // Disallow overwriting an object on legal hold
+ if legalHold.Status == objectlock.LegalHoldOn {
+ return mode, retainDate, legalHold, ErrObjectLocked
+ }
+ }
+ }
+
+ if legalHoldRequested {
+ var lerr error
+ if legalHold, lerr = objectlock.ParseObjectLockLegalHoldHeaders(rq.Header); lerr != nil {
+ return mode, retainDate, legalHold, toAPIErrorCode(ctx, lerr)
+ }
+ if legalHoldPermErr != ErrNone {
+ return mode, retainDate, legalHold, legalHoldPermErr
+ }
+ }
+
+ if retentionRequested {
+ legalHold, err := objectlock.ParseObjectLockLegalHoldHeaders(rq.Header)
+ if err != nil {
+ return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
+ }
+ rMode, rDate, err := objectlock.ParseObjectLockRetentionHeaders(rq.Header)
+ if err != nil && (!replica || rMode != "" || !rDate.IsZero()) {
+ return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
+ }
+ if retentionPermErr != ErrNone {
+ return mode, retainDate, legalHold, retentionPermErr
+ }
+ return rMode, rDate, legalHold, ErrNone
+ }
+ if replica { // replica inherits retention metadata only from source
+ return "", objectlock.RetentionDate{}, legalHold, ErrNone
+ }
+ if !retentionRequested && retentionCfg.Validity > 0 {
+ if retentionPermErr != ErrNone {
+ return mode, retainDate, legalHold, retentionPermErr
+ }
+
+ t, err := objectlock.UTCNowNTP()
+ if err != nil {
+ internalLogIf(ctx, err, logger.WarningKind)
+ return mode, retainDate, legalHold, ErrObjectLocked
+ }
+
+ if !legalHoldRequested && retentionCfg.LockEnabled {
+ // inherit retention from bucket configuration
+ return retentionCfg.Mode, objectlock.RetentionDate{Time: t.Add(retentionCfg.Validity)}, legalHold, ErrNone
+ }
+ return "", objectlock.RetentionDate{}, legalHold, ErrNone
+ }
+ return mode, retainDate, legalHold, ErrNone
+}
+
+// NewBucketObjectLockSys returns initialized BucketObjectLockSys
+func NewBucketObjectLockSys() *BucketObjectLockSys {
+ return &BucketObjectLockSys{}
+}
diff --git a/cmd/bucket-policy-handlers.go b/cmd/bucket-policy-handlers.go
new file mode 100644
index 00000000..994b0b0d
--- /dev/null
+++ b/cmd/bucket-policy-handlers.go
@@ -0,0 +1,211 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+ "net/http"
+
+ humanize "github.com/dustin/go-humanize"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/mux"
+ "github.com/minio/pkg/v3/policy"
+)
+
+const (
+ // As per AWS S3 specification, 20KiB policy JSON data is allowed.
+ maxBucketPolicySize = 20 * humanize.KiByte
+
+ // Policy configuration file.
+ bucketPolicyConfig = "policy.json"
+)
+
+// PutBucketPolicyHandler - This HTTP handler stores given bucket policy configuration as per
+// https://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html
+func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "PutBucketPolicy")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketPolicyAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Check if bucket exists.
+ if _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Error out if Content-Length is missing.
+ // PutBucketPolicy always needs Content-Length.
+ if r.ContentLength <= 0 {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL)
+ return
+ }
+
+ // Error out if Content-Length is beyond allowed size.
+ if r.ContentLength > maxBucketPolicySize {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrPolicyTooLarge), r.URL)
+ return
+ }
+
+ bucketPolicyBytes, err := io.ReadAll(io.LimitReader(r.Body, r.ContentLength))
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ bucketPolicy, err := policy.ParseBucketPolicyConfig(bytes.NewReader(bucketPolicyBytes), bucket)
+ if err != nil {
+ writeErrorResponse(ctx, w, APIError{
+ Code: "MalformedPolicy",
+ HTTPStatusCode: http.StatusBadRequest,
+ Description: err.Error(),
+ }, r.URL)
+ return
+ }
+
+ // Version in policy must not be empty
+ if bucketPolicy.Version == "" {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrPolicyInvalidVersion), r.URL)
+ return
+ }
+
+ configData, err := json.Marshal(bucketPolicy)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketPolicyConfig, configData)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Call site replication hook.
+ replLogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
+ Type: madmin.SRBucketMetaTypePolicy,
+ Bucket: bucket,
+ Policy: bucketPolicyBytes,
+ UpdatedAt: updatedAt,
+ }))
+
+ // Success.
+ writeSuccessNoContent(w)
+}
+
+// DeleteBucketPolicyHandler - This HTTP handler removes bucket policy configuration.
+func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "DeleteBucketPolicy")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.DeleteBucketPolicyAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Check if bucket exists.
+ if _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ updatedAt, err := globalBucketMetadataSys.Delete(ctx, bucket, bucketPolicyConfig)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Call site replication hook.
+ replLogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
+ Type: madmin.SRBucketMetaTypePolicy,
+ Bucket: bucket,
+ UpdatedAt: updatedAt,
+ }))
+
+ // Success.
+ writeSuccessNoContent(w)
+}
+
+// GetBucketPolicyHandler - This HTTP handler returns bucket policy configuration.
+func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetBucketPolicy")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketPolicyAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Check if bucket exists.
+ if _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Read bucket access policy.
+ config, err := globalPolicySys.Get(bucket)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ configData, err := json.Marshal(config)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Write to client.
+ writeSuccessResponseJSON(w, configData)
+}
diff --git a/cmd/bucket-policy-handlers_test.go b/cmd/bucket-policy-handlers_test.go
new file mode 100644
index 00000000..e506aceb
--- /dev/null
+++ b/cmd/bucket-policy-handlers_test.go
@@ -0,0 +1,777 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "reflect"
+ "strings"
+ "sync"
+ "testing"
+
+ "github.com/minio/minio/internal/auth"
+ "github.com/minio/pkg/v3/policy"
+ "github.com/minio/pkg/v3/policy/condition"
+)
+
+func getAnonReadOnlyBucketPolicy(bucketName string) *policy.BucketPolicy {
+ return &policy.BucketPolicy{
+ Version: policy.DefaultVersion,
+ Statements: []policy.BPStatement{
+ policy.NewBPStatement(
+ "",
+ policy.Allow,
+ policy.NewPrincipal("*"),
+ policy.NewActionSet(policy.GetBucketLocationAction, policy.ListBucketAction),
+ policy.NewResourceSet(policy.NewResource(bucketName)),
+ condition.NewFunctions(),
+ ),
+ },
+ }
+}
+
+func getAnonWriteOnlyBucketPolicy(bucketName string) *policy.BucketPolicy {
+ return &policy.BucketPolicy{
+ Version: policy.DefaultVersion,
+ Statements: []policy.BPStatement{
+ policy.NewBPStatement(
+ "",
+ policy.Allow,
+ policy.NewPrincipal("*"),
+ policy.NewActionSet(
+ policy.GetBucketLocationAction,
+ policy.ListBucketMultipartUploadsAction,
+ ),
+ policy.NewResourceSet(policy.NewResource(bucketName)),
+ condition.NewFunctions(),
+ ),
+ },
+ }
+}
+
+func getAnonReadOnlyObjectPolicy(bucketName, prefix string) *policy.BucketPolicy {
+ return &policy.BucketPolicy{
+ Version: policy.DefaultVersion,
+ Statements: []policy.BPStatement{
+ policy.NewBPStatement(
+ "",
+ policy.Allow,
+ policy.NewPrincipal("*"),
+ policy.NewActionSet(policy.GetObjectAction),
+ policy.NewResourceSet(policy.NewResource(bucketName+"/"+prefix)),
+ condition.NewFunctions(),
+ ),
+ },
+ }
+}
+
+func getAnonWriteOnlyObjectPolicy(bucketName, prefix string) *policy.BucketPolicy {
+ return &policy.BucketPolicy{
+ Version: policy.DefaultVersion,
+ Statements: []policy.BPStatement{
+ policy.NewBPStatement(
+ "",
+ policy.Allow,
+ policy.NewPrincipal("*"),
+ policy.NewActionSet(
+ policy.AbortMultipartUploadAction,
+ policy.DeleteObjectAction,
+ policy.ListMultipartUploadPartsAction,
+ policy.PutObjectAction,
+ ),
+ policy.NewResourceSet(policy.NewResource(bucketName+"/"+prefix)),
+ condition.NewFunctions(),
+ ),
+ },
+ }
+}
+
+// Wrapper for calling Create Bucket and ensure we get one and only one success.
+func TestCreateBucket(t *testing.T) {
+ ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testCreateBucket, endpoints: []string{"MakeBucket"}})
+}
+
+// testCreateBucket - Test for calling Create Bucket and ensure we get one and only one success.
+func testCreateBucket(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ credentials auth.Credentials, t *testing.T,
+) {
+ bucketName1 := fmt.Sprintf("%s-1", bucketName)
+
+ const n = 100
+ start := make(chan struct{})
+ var ok, errs int
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+ wg.Add(n)
+ for range n {
+ go func() {
+ defer wg.Done()
+ // Sync start.
+ <-start
+ if err := obj.MakeBucket(GlobalContext, bucketName1, MakeBucketOptions{}); err != nil {
+ if _, ok := err.(BucketExists); !ok {
+ t.Logf("unexpected error: %T: %v", err, err)
+ return
+ }
+ mu.Lock()
+ errs++
+ mu.Unlock()
+ return
+ }
+ mu.Lock()
+ ok++
+ mu.Unlock()
+ }()
+ }
+ close(start)
+ wg.Wait()
+ if ok != 1 {
+ t.Fatalf("want 1 ok, got %d", ok)
+ }
+ if errs != n-1 {
+ t.Fatalf("want %d errors, got %d", n-1, errs)
+ }
+}
+
+// Wrapper for calling Put Bucket Policy HTTP handler tests for both Erasure multiple disks and single node setup.
+func TestPutBucketPolicyHandler(t *testing.T) {
+ ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testPutBucketPolicyHandler, endpoints: []string{"PutBucketPolicy"}})
+}
+
+// testPutBucketPolicyHandler - Test for Bucket policy end point.
+func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ credentials auth.Credentials, t *testing.T,
+) {
+ bucketName1 := fmt.Sprintf("%s-1", bucketName)
+ if err := obj.MakeBucket(GlobalContext, bucketName1, MakeBucketOptions{}); err != nil {
+ t.Fatal(err)
+ }
+
+ // template for constructing HTTP request body for PUT bucket policy.
+ bucketPolicyTemplate := `{"Version":"2012-10-17","Statement":[{"Sid":"","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetBucketLocation","s3:ListBucket"],"Resource":["arn:aws:s3:::%s"]},{"Sid":"","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetObject"],"Resource":["arn:aws:s3:::%s/this*"]}]}`
+
+ bucketPolicyTemplateWithoutVersion := `{"Version":"","Statement":[{"Sid":"","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetBucketLocation","s3:ListBucket"],"Resource":["arn:aws:s3:::%s"]},{"Sid":"","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetObject"],"Resource":["arn:aws:s3:::%s/this*"]}]}`
+
+ // test cases with sample input and expected output.
+ testCases := []struct {
+ bucketName string
+ // bucket policy to be set,
+ // set as request body.
+ bucketPolicyReader io.ReadSeeker
+ // length in bytes of the bucket policy being set.
+ policyLen int
+ accessKey string
+ secretKey string
+ // expected Response.
+ expectedRespStatus int
+ }{
+ // Test case - 1.
+ {
+ bucketName: bucketName,
+ bucketPolicyReader: bytes.NewReader(fmt.Appendf(nil, bucketPolicyTemplate, bucketName, bucketName)),
+
+ policyLen: len(fmt.Sprintf(bucketPolicyTemplate, bucketName, bucketName)),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusNoContent,
+ },
+ // Test case - 2.
+ // Setting the content length to be more than max allowed size.
+ // Expecting StatusBadRequest (400).
+ {
+ bucketName: bucketName,
+ bucketPolicyReader: bytes.NewReader(fmt.Appendf(nil, bucketPolicyTemplate, bucketName, bucketName)),
+
+ policyLen: maxBucketPolicySize + 1,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusBadRequest,
+ },
+ // Test case - 3.
+ // Case with content-length of the HTTP request set to 0.
+ // Expecting the HTTP response status to be StatusLengthRequired (411).
+ {
+ bucketName: bucketName,
+ bucketPolicyReader: bytes.NewReader(fmt.Appendf(nil, bucketPolicyTemplate, bucketName, bucketName)),
+
+ policyLen: 0,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusLengthRequired,
+ },
+ // Test case - 4.
+ // setting the readSeeker to `nil`, bucket policy parser will fail.
+ {
+ bucketName: bucketName,
+ bucketPolicyReader: nil,
+
+ policyLen: 10,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusBadRequest,
+ },
+ // Test case - 5.
+ // setting the keys to be empty.
+ // Expecting statusForbidden.
+ {
+ bucketName: bucketName,
+ bucketPolicyReader: nil,
+
+ policyLen: 10,
+ accessKey: "",
+ secretKey: "",
+ expectedRespStatus: http.StatusForbidden,
+ },
+ // Test case - 6.
+ // setting an invalid bucket policy.
+ // the bucket policy parser will fail.
+ {
+ bucketName: bucketName,
+ bucketPolicyReader: bytes.NewReader([]byte("dummy-policy")),
+
+ policyLen: len([]byte("dummy-policy")),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusBadRequest,
+ },
+ // Test case - 7.
+ // Different bucket name used in the HTTP request and the policy string.
+ // checkBucketPolicyResources should fail.
+ {
+ bucketName: bucketName1,
+ bucketPolicyReader: bytes.NewReader(fmt.Appendf(nil, bucketPolicyTemplate, bucketName, bucketName)),
+
+ policyLen: len(fmt.Sprintf(bucketPolicyTemplate, bucketName, bucketName)),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusBadRequest,
+ },
+ // Test case - 8.
+ // non-existent bucket is used.
+ // writing BucketPolicy should fail.
+ // should result in 404 StatusNotFound
+ {
+ bucketName: "non-existent-bucket",
+ bucketPolicyReader: bytes.NewReader(fmt.Appendf(nil, bucketPolicyTemplate, "non-existent-bucket", "non-existent-bucket")),
+
+ policyLen: len(fmt.Sprintf(bucketPolicyTemplate, bucketName, bucketName)),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusNotFound,
+ },
+ // Test case - 9.
+ // non-existent bucket is used (with invalid bucket name)
+ // writing BucketPolicy should fail.
+ // should result in 404 StatusNotFound
+ {
+ bucketName: ".invalid-bucket",
+ bucketPolicyReader: bytes.NewReader(fmt.Appendf(nil, bucketPolicyTemplate, ".invalid-bucket", ".invalid-bucket")),
+
+ policyLen: len(fmt.Sprintf(bucketPolicyTemplate, bucketName, bucketName)),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusBadRequest,
+ },
+ // Test case - 10.
+ // Existent bucket with policy with Version field empty.
+ // writing BucketPolicy should fail.
+ // should result in 400 StatusBadRequest.
+ {
+ bucketName: bucketName,
+ bucketPolicyReader: bytes.NewReader(fmt.Appendf(nil, bucketPolicyTemplateWithoutVersion, bucketName, bucketName)),
+
+ policyLen: len(fmt.Sprintf(bucketPolicyTemplateWithoutVersion, bucketName, bucketName)),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusBadRequest,
+ },
+ }
+
+ // Iterating over the test cases, calling the function under test and asserting the response.
+ for i, testCase := range testCases {
+ // obtain the put bucket policy request body.
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ recV4 := httptest.NewRecorder()
+ // construct HTTP request for PUT bucket policy endpoint.
+ reqV4, err := newTestSignedRequestV4(http.MethodPut, getPutPolicyURL("", testCase.bucketName),
+ int64(testCase.policyLen), testCase.bucketPolicyReader, testCase.accessKey, testCase.secretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: %v", i+1, instanceType, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler.
+ apiRouter.ServeHTTP(recV4, reqV4)
+ if recV4.Code != testCase.expectedRespStatus {
+ t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, recV4.Code)
+ }
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ recV2 := httptest.NewRecorder()
+ // construct HTTP request for PUT bucket policy endpoint.
+ reqV2, err := newTestSignedRequestV2(http.MethodPut, getPutPolicyURL("", testCase.bucketName),
+ int64(testCase.policyLen), testCase.bucketPolicyReader, testCase.accessKey, testCase.secretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: %v", i+1, instanceType, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler.
+ apiRouter.ServeHTTP(recV2, reqV2)
+ 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)
+ }
+ }
+
+ // Test for Anonymous/unsigned http request.
+ // Bucket policy related functions doesn't support anonymous requests, setting policies shouldn't make a difference.
+ bucketPolicyStr := fmt.Sprintf(bucketPolicyTemplate, bucketName, bucketName)
+ // create unsigned HTTP request for PutBucketPolicyHandler.
+ anonReq, err := newTestRequest(http.MethodPut, getPutPolicyURL("", bucketName),
+ int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)))
+ if err != nil {
+ t.Fatalf("MinIO %s: Failed to create an anonymous request for bucket \"%s\": %v",
+ instanceType, bucketName, err)
+ }
+
+ // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
+ // sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
+ // unsigned request goes through and its validated again.
+ ExecObjectLayerAPIAnonTest(t, obj, "PutBucketPolicyHandler", bucketName, "", instanceType, apiRouter, anonReq, getAnonWriteOnlyBucketPolicy(bucketName))
+
+ // HTTP request for testing when `objectLayer` is set to `nil`.
+ // There is no need to use an existing bucket and valid input for creating the request
+ // since the `objectLayer==nil` check is performed before any other checks inside the handlers.
+ // The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
+ nilBucket := "dummy-bucket"
+
+ nilReq, err := newTestSignedRequestV4(http.MethodPut, getPutPolicyURL("", nilBucket),
+ 0, nil, "", "", nil)
+ if err != nil {
+ t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
+ }
+ // execute the object layer set to `nil` test.
+ // `ExecObjectLayerAPINilTest` manages the operation.
+ ExecObjectLayerAPINilTest(t, nilBucket, "", instanceType, apiRouter, nilReq)
+}
+
+// Wrapper for calling Get Bucket Policy HTTP handler tests for both Erasure multiple disks and single node setup.
+func TestGetBucketPolicyHandler(t *testing.T) {
+ ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testGetBucketPolicyHandler, endpoints: []string{"PutBucketPolicy", "GetBucketPolicy"}})
+}
+
+// testGetBucketPolicyHandler - Test for end point which fetches the access policy json of the given bucket.
+func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ credentials auth.Credentials, t *testing.T,
+) {
+ // template for constructing HTTP request body for PUT bucket policy.
+ bucketPolicyTemplate := `{"Version":"2012-10-17","Statement":[{"Action":["s3:GetBucketLocation","s3:ListBucket"],"Effect":"Allow","Principal":{"AWS":["*"]},"Resource":["arn:aws:s3:::%s"]},{"Action":["s3:GetObject"],"Effect":"Allow","Principal":{"AWS":["*"]},"Resource":["arn:aws:s3:::%s/this*"]}]}`
+
+ // Writing bucket policy before running test on GetBucketPolicy.
+ putTestPolicies := []struct {
+ bucketName string
+ accessKey string
+ secretKey string
+ // expected Response.
+ expectedRespStatus int
+ }{
+ {bucketName, credentials.AccessKey, credentials.SecretKey, http.StatusNoContent},
+ }
+
+ // Iterating over the cases and writing the bucket policy.
+ // its required to write the policies first before running tests on GetBucketPolicy.
+ for i, testPolicy := range putTestPolicies {
+ // obtain the put bucket policy request body.
+ bucketPolicyStr := fmt.Sprintf(bucketPolicyTemplate, testPolicy.bucketName, testPolicy.bucketName)
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ recV4 := httptest.NewRecorder()
+ // construct HTTP request for PUT bucket policy endpoint.
+ reqV4, err := newTestSignedRequestV4(http.MethodPut, getPutPolicyURL("", testPolicy.bucketName),
+ int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: %v", i+1, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler.
+ apiRouter.ServeHTTP(recV4, reqV4)
+ if recV4.Code != testPolicy.expectedRespStatus {
+ t.Fatalf("Case %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testPolicy.expectedRespStatus, recV4.Code)
+ }
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ recV2 := httptest.NewRecorder()
+ // construct HTTP request for PUT bucket policy endpoint.
+ reqV2, err := newTestSignedRequestV2(http.MethodPut, getPutPolicyURL("", testPolicy.bucketName),
+ int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: %v", i+1, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler.
+ apiRouter.ServeHTTP(recV2, reqV2)
+ if recV2.Code != testPolicy.expectedRespStatus {
+ t.Fatalf("Case %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testPolicy.expectedRespStatus, recV2.Code)
+ }
+ }
+
+ // test cases with inputs and expected result for GetBucketPolicyHandler.
+ testCases := []struct {
+ bucketName string
+ accessKey string
+ secretKey string
+ // expected output.
+ expectedBucketPolicy string
+ expectedRespStatus int
+ }{
+ // Test case - 1.
+ // Case which valid inputs, expected to return success status of 200OK.
+ {
+ bucketName: bucketName,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedBucketPolicy: bucketPolicyTemplate,
+ expectedRespStatus: http.StatusOK,
+ },
+ // Test case - 2.
+ // Case with non-existent bucket name.
+ {
+ bucketName: "non-existent-bucket",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedBucketPolicy: bucketPolicyTemplate,
+ expectedRespStatus: http.StatusNotFound,
+ },
+ // Test case - 3.
+ // Case with non-existent bucket name.
+ {
+ bucketName: ".invalid-bucket-name",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedBucketPolicy: "",
+ expectedRespStatus: http.StatusBadRequest,
+ },
+ }
+ // Iterating over the cases, fetching the policy and validating the response.
+ for i, testCase := range testCases {
+ // expected bucket policy json string.
+ expectedBucketPolicyStr := fmt.Sprintf(testCase.expectedBucketPolicy, testCase.bucketName, testCase.bucketName)
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ recV4 := httptest.NewRecorder()
+ // construct HTTP request for PUT bucket policy endpoint.
+ reqV4, err := newTestSignedRequestV4(http.MethodGet, getGetPolicyURL("", testCase.bucketName),
+ 0, nil, testCase.accessKey, testCase.secretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: %v", i+1, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler, GetBucketPolicyHandler handles the request.
+ apiRouter.ServeHTTP(recV4, reqV4)
+ // Assert the response code with the expected status.
+ if recV4.Code != testCase.expectedRespStatus {
+ t.Fatalf("Case %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testCase.expectedRespStatus, recV4.Code)
+ }
+ // read the response body.
+ bucketPolicyReadBuf, err := io.ReadAll(recV4.Body)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed parsing response body: %v", i+1, instanceType, err)
+ }
+
+ if recV4.Code != testCase.expectedRespStatus {
+ // Verify whether the bucket policy fetched is same as the one inserted.
+ var expectedPolicy *policy.BucketPolicy
+ expectedPolicy, err = policy.ParseBucketPolicyConfig(strings.NewReader(expectedBucketPolicyStr), testCase.bucketName)
+ if err != nil {
+ t.Fatalf("unexpected error. %v", err)
+ }
+ var gotPolicy *policy.BucketPolicy
+ gotPolicy, err = policy.ParseBucketPolicyConfig(bytes.NewReader(bucketPolicyReadBuf), testCase.bucketName)
+ if err != nil {
+ t.Fatalf("unexpected error. %v", err)
+ }
+
+ if !reflect.DeepEqual(expectedPolicy, gotPolicy) {
+ t.Errorf("Test %d: %s: Bucket policy differs from expected value.", i+1, instanceType)
+ }
+ }
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ recV2 := httptest.NewRecorder()
+ // construct HTTP request for PUT bucket policy endpoint.
+ reqV2, err := newTestSignedRequestV2(http.MethodGet, getGetPolicyURL("", testCase.bucketName),
+ 0, nil, testCase.accessKey, testCase.secretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: %v", i+1, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler, GetBucketPolicyHandler handles the request.
+ apiRouter.ServeHTTP(recV2, reqV2)
+ // Assert the response code with the expected status.
+ if recV2.Code != testCase.expectedRespStatus {
+ t.Fatalf("Case %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testCase.expectedRespStatus, recV2.Code)
+ }
+ // read the response body.
+ bucketPolicyReadBuf, err = io.ReadAll(recV2.Body)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed parsing response body: %v", i+1, instanceType, err)
+ }
+ if recV2.Code == http.StatusOK {
+ // Verify whether the bucket policy fetched is same as the one inserted.
+ expectedPolicy, err := policy.ParseBucketPolicyConfig(strings.NewReader(expectedBucketPolicyStr), testCase.bucketName)
+ if err != nil {
+ t.Fatalf("unexpected error. %v", err)
+ }
+ gotPolicy, err := policy.ParseBucketPolicyConfig(bytes.NewReader(bucketPolicyReadBuf), testCase.bucketName)
+ if err != nil {
+ t.Fatalf("unexpected error. %v", err)
+ }
+
+ if !reflect.DeepEqual(expectedPolicy, gotPolicy) {
+ t.Errorf("Test %d: %s: Bucket policy differs from expected value.", i+1, instanceType)
+ }
+ }
+ }
+
+ // Test for Anonymous/unsigned http request.
+ // Bucket policy related functions doesn't support anonymous requests, setting policies shouldn't make a difference.
+ // create unsigned HTTP request for PutBucketPolicyHandler.
+ anonReq, err := newTestRequest(http.MethodGet, getPutPolicyURL("", bucketName), 0, nil)
+ if err != nil {
+ t.Fatalf("MinIO %s: Failed to create an anonymous request for bucket \"%s\": %v",
+ instanceType, bucketName, err)
+ }
+
+ // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
+ // sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
+ // unsigned request goes through and its validated again.
+ ExecObjectLayerAPIAnonTest(t, obj, "GetBucketPolicyHandler", bucketName, "", instanceType, apiRouter, anonReq, getAnonReadOnlyBucketPolicy(bucketName))
+
+ // HTTP request for testing when `objectLayer` is set to `nil`.
+ // There is no need to use an existing bucket and valid input for creating the request
+ // since the `objectLayer==nil` check is performed before any other checks inside the handlers.
+ // The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
+ nilBucket := "dummy-bucket"
+
+ nilReq, err := newTestSignedRequestV4(http.MethodGet, getGetPolicyURL("", nilBucket),
+ 0, nil, "", "", nil)
+ if err != nil {
+ t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
+ }
+ // execute the object layer set to `nil` test.
+ // `ExecObjectLayerAPINilTest` manages the operation.
+ ExecObjectLayerAPINilTest(t, nilBucket, "", instanceType, apiRouter, nilReq)
+}
+
+// Wrapper for calling Delete Bucket Policy HTTP handler tests for both Erasure multiple disks and single node setup.
+func TestDeleteBucketPolicyHandler(t *testing.T) {
+ ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testDeleteBucketPolicyHandler, endpoints: []string{"PutBucketPolicy", "DeleteBucketPolicy"}})
+}
+
+// testDeleteBucketPolicyHandler - Test for Delete bucket policy end point.
+func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ credentials auth.Credentials, t *testing.T,
+) {
+ // template for constructing HTTP request body for PUT bucket policy.
+ bucketPolicyTemplate := `{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Action": [
+ "s3:GetBucketLocation",
+ "s3:ListBucket"
+ ],
+ "Effect": "Allow",
+ "Principal": {
+ "AWS": [
+ "*"
+ ]
+ },
+ "Resource": [
+ "arn:aws:s3:::%s"
+ ]
+ },
+ {
+ "Action": [
+ "s3:GetObject"
+ ],
+ "Effect": "Allow",
+ "Principal": {
+ "AWS": [
+ "*"
+ ]
+ },
+ "Resource": [
+ "arn:aws:s3:::%s/this*"
+ ]
+ }
+ ]
+}`
+
+ // Writing bucket policy before running test on DeleteBucketPolicy.
+ putTestPolicies := []struct {
+ bucketName string
+ accessKey string
+ secretKey string
+ // expected Response.
+ expectedRespStatus int
+ }{
+ {
+ bucketName: bucketName,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusNoContent,
+ },
+ }
+
+ // Iterating over the cases and writing the bucket policy.
+ // its required to write the policies first before running tests on GetBucketPolicy.
+ for i, testPolicy := range putTestPolicies {
+ // obtain the put bucket policy request body.
+ bucketPolicyStr := fmt.Sprintf(bucketPolicyTemplate, testPolicy.bucketName, testPolicy.bucketName)
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ recV4 := httptest.NewRecorder()
+ // construct HTTP request for PUT bucket policy endpoint.
+ reqV4, err := newTestSignedRequestV4(http.MethodPut, getPutPolicyURL("", testPolicy.bucketName),
+ int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: %v", i+1, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler.
+ apiRouter.ServeHTTP(recV4, reqV4)
+ if recV4.Code != testPolicy.expectedRespStatus {
+ t.Fatalf("Case %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testPolicy.expectedRespStatus, recV4.Code)
+ }
+ }
+
+ // testcases with input and expected output for DeleteBucketPolicyHandler.
+ testCases := []struct {
+ bucketName string
+ accessKey string
+ secretKey string
+ // expected response.
+ expectedRespStatus int
+ }{
+ // Test case - 1.
+ {
+ bucketName: bucketName,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusNoContent,
+ },
+ // Test case - 2.
+ // Case with non-existent-bucket.
+ {
+ bucketName: "non-existent-bucket",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusNotFound,
+ },
+ // Test case - 3.
+ // Case with non-existent-bucket.
+ {
+ bucketName: ".invalid-bucket-name",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusBadRequest,
+ },
+ }
+ // Iterating over the cases and deleting the bucket policy and then asserting response.
+ for i, testCase := range testCases {
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ recV4 := httptest.NewRecorder()
+ // construct HTTP request for Delete bucket policy endpoint.
+ reqV4, err := newTestSignedRequestV4(http.MethodDelete, getDeletePolicyURL("", testCase.bucketName),
+ 0, nil, testCase.accessKey, testCase.secretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: %v", i+1, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler, DeleteBucketPolicyHandler handles the request.
+ apiRouter.ServeHTTP(recV4, reqV4)
+ // Assert the response code with the expected status.
+ if recV4.Code != testCase.expectedRespStatus {
+ t.Fatalf("Case %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testCase.expectedRespStatus, recV4.Code)
+ }
+ }
+
+ // Iterating over the cases and writing the bucket policy.
+ // its required to write the policies first before running tests on GetBucketPolicy.
+ for i, testPolicy := range putTestPolicies {
+ // obtain the put bucket policy request body.
+ bucketPolicyStr := fmt.Sprintf(bucketPolicyTemplate, testPolicy.bucketName, testPolicy.bucketName)
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ recV2 := httptest.NewRecorder()
+ // construct HTTP request for PUT bucket policy endpoint.
+ reqV2, err := newTestSignedRequestV2(http.MethodPut, getPutPolicyURL("", testPolicy.bucketName),
+ int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: %v", i+1, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler.
+ apiRouter.ServeHTTP(recV2, reqV2)
+ if recV2.Code != testPolicy.expectedRespStatus {
+ t.Fatalf("Case %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testPolicy.expectedRespStatus, recV2.Code)
+ }
+ }
+
+ for i, testCase := range testCases {
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ recV2 := httptest.NewRecorder()
+ // construct HTTP request for Delete bucket policy endpoint.
+ reqV2, err := newTestSignedRequestV2(http.MethodDelete, getDeletePolicyURL("", testCase.bucketName),
+ 0, nil, testCase.accessKey, testCase.secretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: %v", i+1, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler, DeleteBucketPolicyHandler handles the request.
+ apiRouter.ServeHTTP(recV2, reqV2)
+ // Assert the response code with the expected status.
+ if recV2.Code != testCase.expectedRespStatus {
+ t.Fatalf("Case %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testCase.expectedRespStatus, recV2.Code)
+ }
+ }
+ // Test for Anonymous/unsigned http request.
+ // Bucket policy related functions doesn't support anonymous requests, setting policies shouldn't make a difference.
+ // create unsigned HTTP request for PutBucketPolicyHandler.
+ anonReq, err := newTestRequest(http.MethodDelete, getPutPolicyURL("", bucketName), 0, nil)
+ if err != nil {
+ t.Fatalf("MinIO %s: Failed to create an anonymous request for bucket \"%s\": %v",
+ instanceType, bucketName, err)
+ }
+
+ // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
+ // sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
+ // unsigned request goes through and its validated again.
+ ExecObjectLayerAPIAnonTest(t, obj, "DeleteBucketPolicyHandler", bucketName, "", instanceType, apiRouter, anonReq, getAnonWriteOnlyBucketPolicy(bucketName))
+
+ // HTTP request for testing when `objectLayer` is set to `nil`.
+ // There is no need to use an existing bucket and valid input for creating the request
+ // since the `objectLayer==nil` check is performed before any other checks inside the handlers.
+ // The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
+ nilBucket := "dummy-bucket"
+
+ nilReq, err := newTestSignedRequestV4(http.MethodDelete, getDeletePolicyURL("", nilBucket),
+ 0, nil, "", "", nil)
+ if err != nil {
+ t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
+ }
+ // execute the object layer set to `nil` test.
+ // `ExecObjectLayerAPINilTest` manages the operation.
+ ExecObjectLayerAPINilTest(t, nilBucket, "", instanceType, apiRouter, nilReq)
+}
diff --git a/cmd/bucket-policy.go b/cmd/bucket-policy.go
new file mode 100644
index 00000000..0bcbe2e6
--- /dev/null
+++ b/cmd/bucket-policy.go
@@ -0,0 +1,288 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "encoding/json"
+ "maps"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+
+ jsoniter "github.com/json-iterator/go"
+ miniogopolicy "github.com/minio/minio-go/v7/pkg/policy"
+ "github.com/minio/minio-go/v7/pkg/tags"
+ "github.com/minio/minio/internal/auth"
+ "github.com/minio/minio/internal/handlers"
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/policy"
+)
+
+// PolicySys - policy subsystem.
+type PolicySys struct{}
+
+// Get returns stored bucket policy
+func (sys *PolicySys) Get(bucket string) (*policy.BucketPolicy, error) {
+ policy, _, err := globalBucketMetadataSys.GetPolicyConfig(bucket)
+ return policy, err
+}
+
+// IsAllowed - checks given policy args is allowed to continue the Rest API.
+func (sys *PolicySys) IsAllowed(args policy.BucketPolicyArgs) bool {
+ p, err := sys.Get(args.BucketName)
+ if err == nil {
+ return p.IsAllowed(args)
+ }
+
+ // Log unhandled errors.
+ if _, ok := err.(BucketPolicyNotFound); !ok {
+ internalLogIf(GlobalContext, err, logger.WarningKind)
+ }
+
+ // As policy is not available for given bucket name, returns IsOwner i.e.
+ // operation is allowed only for owner.
+ return args.IsOwner
+}
+
+// NewPolicySys - creates new policy system.
+func NewPolicySys() *PolicySys {
+ return &PolicySys{}
+}
+
+func getSTSConditionValues(r *http.Request, lc string, cred auth.Credentials) map[string][]string {
+ m := make(map[string][]string)
+ if d := r.Form.Get("DurationSeconds"); d != "" {
+ m["DurationSeconds"] = []string{d}
+ }
+ return m
+}
+
+func getConditionValues(r *http.Request, lc string, cred auth.Credentials) map[string][]string {
+ currTime := UTCNow()
+
+ var (
+ username = cred.AccessKey
+ claims = cred.Claims
+ groups = cred.Groups
+ )
+
+ if cred.IsTemp() || cred.IsServiceAccount() {
+ // For derived credentials, check the parent user's permissions.
+ username = cred.ParentUser
+ }
+
+ principalType := "Anonymous"
+ if username != "" {
+ principalType = "User"
+ if len(claims) > 0 {
+ principalType = "AssumedRole"
+ }
+ if username == globalActiveCred.AccessKey {
+ principalType = "Account"
+ }
+ }
+
+ vid := r.Form.Get(xhttp.VersionID)
+ if vid == "" {
+ if u, err := url.Parse(r.Header.Get(xhttp.AmzCopySource)); err == nil {
+ vid = u.Query().Get(xhttp.VersionID)
+ }
+ }
+
+ authType := getRequestAuthType(r)
+ var signatureVersion string
+ switch authType {
+ case authTypeSignedV2, authTypePresignedV2:
+ signatureVersion = signV2Algorithm
+ case authTypeSigned, authTypePresigned, authTypeStreamingSigned, authTypePostPolicy:
+ signatureVersion = signV4Algorithm
+ }
+
+ var authtype string
+ switch authType {
+ case authTypePresignedV2, authTypePresigned:
+ authtype = "REST-QUERY-STRING"
+ case authTypeSignedV2, authTypeSigned, authTypeStreamingSigned:
+ authtype = "REST-HEADER"
+ case authTypePostPolicy:
+ authtype = "POST"
+ }
+
+ args := map[string][]string{
+ "CurrentTime": {currTime.Format(time.RFC3339)},
+ "EpochTime": {strconv.FormatInt(currTime.Unix(), 10)},
+ "SecureTransport": {strconv.FormatBool(r.TLS != nil)},
+ "SourceIp": {handlers.GetSourceIPRaw(r)},
+ "UserAgent": {r.UserAgent()},
+ "Referer": {r.Referer()},
+ "principaltype": {principalType},
+ "userid": {username},
+ "username": {username},
+ "versionid": {vid},
+ "signatureversion": {signatureVersion},
+ "authType": {authtype},
+ }
+
+ if lc != "" {
+ args["LocationConstraint"] = []string{lc}
+ }
+
+ cloneHeader := r.Header.Clone()
+ if v := cloneHeader.Get("x-amz-signature-age"); v != "" {
+ args["signatureAge"] = []string{v}
+ cloneHeader.Del("x-amz-signature-age")
+ }
+
+ if userTags := cloneHeader.Get(xhttp.AmzObjectTagging); userTags != "" {
+ tag, _ := tags.ParseObjectTags(userTags)
+ if tag != nil {
+ tagMap := tag.ToMap()
+ keys := make([]string, 0, len(tagMap))
+ for k, v := range tagMap {
+ args[pathJoin("ExistingObjectTag", k)] = []string{v}
+ args[pathJoin("RequestObjectTag", k)] = []string{v}
+ keys = append(keys, k)
+ }
+ args["RequestObjectTagKeys"] = keys
+ }
+ }
+
+ for _, objLock := range []string{
+ xhttp.AmzObjectLockMode,
+ xhttp.AmzObjectLockLegalHold,
+ xhttp.AmzObjectLockRetainUntilDate,
+ } {
+ if values, ok := cloneHeader[objLock]; ok {
+ args[strings.TrimPrefix(objLock, "X-Amz-")] = values
+ }
+ cloneHeader.Del(objLock)
+ }
+
+ for key, values := range cloneHeader {
+ if strings.EqualFold(key, xhttp.AmzObjectTagging) {
+ continue
+ }
+ if existingValues, found := args[key]; found {
+ args[key] = append(existingValues, values...)
+ } else {
+ args[key] = values
+ }
+ }
+
+ cloneURLValues := make(url.Values, len(r.Form))
+ maps.Copy(cloneURLValues, r.Form)
+
+ for _, objLock := range []string{
+ xhttp.AmzObjectLockMode,
+ xhttp.AmzObjectLockLegalHold,
+ xhttp.AmzObjectLockRetainUntilDate,
+ } {
+ if values, ok := cloneURLValues[objLock]; ok {
+ args[strings.TrimPrefix(objLock, "X-Amz-")] = values
+ }
+ cloneURLValues.Del(objLock)
+ }
+
+ for key, values := range cloneURLValues {
+ if existingValues, found := args[key]; found {
+ args[key] = append(existingValues, values...)
+ } else {
+ args[key] = values
+ }
+ }
+
+ // JWT specific values
+ //
+ // Add all string claims
+ for k, v := range claims {
+ vStr, ok := v.(string)
+ if ok {
+ // Trim any LDAP specific prefix
+ args[strings.ToLower(strings.TrimPrefix(k, "ldap"))] = []string{vStr}
+ }
+ }
+
+ // Add groups claim which could be a list. This will ensure that the claim
+ // `jwt:groups` works.
+ if grpsVal, ok := claims["groups"]; ok {
+ if grpsIs, ok := grpsVal.([]any); ok {
+ grps := []string{}
+ for _, gI := range grpsIs {
+ if g, ok := gI.(string); ok {
+ grps = append(grps, g)
+ }
+ }
+ if len(grps) > 0 {
+ args["groups"] = grps
+ }
+ }
+ }
+
+ // if not claim groups are available use the one with auth.Credentials
+ if _, ok := args["groups"]; !ok {
+ if len(groups) > 0 {
+ args["groups"] = groups
+ }
+ }
+
+ return args
+}
+
+// PolicyToBucketAccessPolicy converts a MinIO policy into a minio-go policy data structure.
+func PolicyToBucketAccessPolicy(bucketPolicy *policy.BucketPolicy) (*miniogopolicy.BucketAccessPolicy, error) {
+ // Return empty BucketAccessPolicy for empty bucket policy.
+ if bucketPolicy == nil {
+ return &miniogopolicy.BucketAccessPolicy{Version: policy.DefaultVersion}, nil
+ }
+
+ data, err := json.Marshal(bucketPolicy)
+ if err != nil {
+ // This should not happen because bucketPolicy is valid to convert to JSON data.
+ return nil, err
+ }
+
+ var policyInfo miniogopolicy.BucketAccessPolicy
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ if err = json.Unmarshal(data, &policyInfo); err != nil {
+ // This should not happen because data is valid to JSON data.
+ return nil, err
+ }
+
+ return &policyInfo, nil
+}
+
+// BucketAccessPolicyToPolicy - converts minio-go/policy.BucketAccessPolicy to policy.BucketPolicy.
+func BucketAccessPolicyToPolicy(policyInfo *miniogopolicy.BucketAccessPolicy) (*policy.BucketPolicy, error) {
+ data, err := json.Marshal(policyInfo)
+ if err != nil {
+ // This should not happen because policyInfo is valid to convert to JSON data.
+ return nil, err
+ }
+
+ var bucketPolicy policy.BucketPolicy
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ if err = json.Unmarshal(data, &bucketPolicy); err != nil {
+ // This should not happen because data is valid to JSON data.
+ return nil, err
+ }
+
+ return &bucketPolicy, nil
+}
diff --git a/cmd/bucket-quota.go b/cmd/bucket-quota.go
new file mode 100644
index 00000000..d9779c21
--- /dev/null
+++ b/cmd/bucket-quota.go
@@ -0,0 +1,140 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/cachevalue"
+ "github.com/minio/minio/internal/logger"
+)
+
+// BucketQuotaSys - map of bucket and quota configuration.
+type BucketQuotaSys struct{}
+
+// Get - Get quota configuration.
+func (sys *BucketQuotaSys) Get(ctx context.Context, bucketName string) (*madmin.BucketQuota, error) {
+ cfg, _, err := globalBucketMetadataSys.GetQuotaConfig(ctx, bucketName)
+ return cfg, err
+}
+
+// NewBucketQuotaSys returns initialized BucketQuotaSys
+func NewBucketQuotaSys() *BucketQuotaSys {
+ return &BucketQuotaSys{}
+}
+
+var bucketStorageCache = cachevalue.New[DataUsageInfo]()
+
+// Init initialize bucket quota.
+func (sys *BucketQuotaSys) Init(objAPI ObjectLayer) {
+ bucketStorageCache.InitOnce(10*time.Second,
+ cachevalue.Opts{ReturnLastGood: true, NoWait: true},
+ func(ctx context.Context) (DataUsageInfo, error) {
+ if objAPI == nil {
+ return DataUsageInfo{}, errServerNotInitialized
+ }
+ ctx, done := context.WithTimeout(ctx, 2*time.Second)
+ defer done()
+
+ return loadDataUsageFromBackend(ctx, objAPI)
+ },
+ )
+}
+
+// GetBucketUsageInfo return bucket usage info for a given bucket
+func (sys *BucketQuotaSys) GetBucketUsageInfo(ctx context.Context, bucket string) BucketUsageInfo {
+ sys.Init(newObjectLayerFn())
+
+ dui, err := bucketStorageCache.GetWithCtx(ctx)
+ timedout := OperationTimedOut{}
+ if err != nil && !errors.Is(err, context.DeadlineExceeded) && !errors.As(err, &timedout) {
+ if len(dui.BucketsUsage) > 0 {
+ internalLogOnceIf(GlobalContext, fmt.Errorf("unable to retrieve usage information for bucket: %s, relying on older value cached in-memory: err(%v)", bucket, err), "bucket-usage-cache-"+bucket)
+ } else {
+ internalLogOnceIf(GlobalContext, errors.New("unable to retrieve usage information for bucket: %s, no reliable usage value available - quota will not be enforced"), "bucket-usage-empty-"+bucket)
+ }
+ }
+
+ if len(dui.BucketsUsage) > 0 {
+ bui, ok := dui.BucketsUsage[bucket]
+ if ok {
+ return bui
+ }
+ }
+ return BucketUsageInfo{}
+}
+
+// parseBucketQuota parses BucketQuota from json
+func parseBucketQuota(bucket string, data []byte) (quotaCfg *madmin.BucketQuota, err error) {
+ quotaCfg = &madmin.BucketQuota{}
+ if err = json.Unmarshal(data, quotaCfg); err != nil {
+ return quotaCfg, err
+ }
+ if !quotaCfg.IsValid() {
+ if quotaCfg.Type == "fifo" {
+ internalLogIf(GlobalContext, errors.New("Detected older 'fifo' quota config, 'fifo' feature is removed and not supported anymore. Please clear your quota configs using 'mc quota clear alias/bucket' and use 'mc ilm add' for expiration of objects"), logger.WarningKind)
+ return quotaCfg, fmt.Errorf("invalid quota type 'fifo'")
+ }
+ return quotaCfg, fmt.Errorf("Invalid quota config %#v", quotaCfg)
+ }
+ return quotaCfg, err
+}
+
+func (sys *BucketQuotaSys) enforceQuotaHard(ctx context.Context, bucket string, size int64) error {
+ if size < 0 {
+ return nil
+ }
+
+ q, err := sys.Get(ctx, bucket)
+ if err != nil {
+ return err
+ }
+
+ var quotaSize uint64
+ if q != nil && q.Type == madmin.HardQuota {
+ if q.Size > 0 {
+ quotaSize = q.Size
+ } else if q.Quota > 0 {
+ quotaSize = q.Quota
+ }
+ }
+ if quotaSize > 0 {
+ if uint64(size) >= quotaSize { // check if file size already exceeds the quota
+ return BucketQuotaExceeded{Bucket: bucket}
+ }
+
+ bui := sys.GetBucketUsageInfo(ctx, bucket)
+ if bui.Size > 0 && ((bui.Size + uint64(size)) >= quotaSize) {
+ return BucketQuotaExceeded{Bucket: bucket}
+ }
+ }
+
+ return nil
+}
+
+func enforceBucketQuotaHard(ctx context.Context, bucket string, size int64) error {
+ if globalBucketQuotaSys == nil {
+ return nil
+ }
+ return globalBucketQuotaSys.enforceQuotaHard(ctx, bucket, size)
+}
diff --git a/cmd/bucket-replication-handlers.go b/cmd/bucket-replication-handlers.go
new file mode 100644
index 00000000..05a9d2e8
--- /dev/null
+++ b/cmd/bucket-replication-handlers.go
@@ -0,0 +1,660 @@
+// Copyright (c) 2015-2022 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "encoding/json"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "path"
+ "time"
+
+ "github.com/minio/minio-go/v7"
+ objectlock "github.com/minio/minio/internal/bucket/object/lock"
+ "github.com/minio/minio/internal/bucket/replication"
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/mux"
+ "github.com/minio/pkg/v3/policy"
+)
+
+// PutBucketReplicationConfigHandler - PUT Bucket replication configuration.
+// ----------
+// Add a replication configuration on the specified bucket as specified in https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketReplication.html
+func (api objectAPIHandlers) PutBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "PutBucketReplicationConfig")
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+ if s3Error := checkRequestAuthType(ctx, r, policy.PutReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+ // Check if bucket exists.
+ if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ if globalSiteReplicationSys.isEnabled() && logger.GetReqInfo(ctx).Cred.AccessKey != globalActiveCred.AccessKey {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationDenyEditError), r.URL)
+ return
+ }
+ if versioned := globalBucketVersioningSys.Enabled(bucket); !versioned {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationNeedsVersioningError), r.URL)
+ return
+ }
+ replicationConfig, err := replication.ParseConfig(io.LimitReader(r.Body, r.ContentLength))
+ if err != nil {
+ apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
+ apiErr.Description = err.Error()
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+ sameTarget, apiErr := validateReplicationDestination(ctx, bucket, replicationConfig, &validateReplicationDestinationOptions{CheckRemoteBucket: true})
+ if apiErr != noError {
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+ // Validate the received bucket replication config
+ if err = replicationConfig.Validate(bucket, sameTarget); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ configData, err := xml.Marshal(replicationConfig)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketReplicationConfig, configData); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Write success response.
+ writeSuccessResponseHeadersOnly(w)
+}
+
+// GetBucketReplicationConfigHandler - GET Bucket replication configuration.
+// ----------
+// Gets the replication configuration for a bucket.
+func (api objectAPIHandlers) GetBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetBucketReplicationConfig")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ // check if user has permissions to perform this operation
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+ // Check if bucket exists.
+ if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ config, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ configData, err := xml.Marshal(config)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Write success response.
+ writeSuccessResponseXML(w, configData)
+}
+
+// DeleteBucketReplicationConfigHandler - DELETE Bucket replication config.
+// ----------
+func (api objectAPIHandlers) DeleteBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "DeleteBucketReplicationConfig")
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.PutReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+ // Check if bucket exists.
+ if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ if globalSiteReplicationSys.isEnabled() {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationDenyEditError), r.URL)
+ return
+ }
+ if _, err := globalBucketMetadataSys.Delete(ctx, bucket, bucketReplicationConfig); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ targets, err := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ for _, tgt := range targets.Targets {
+ if err := globalBucketTargetSys.RemoveTarget(ctx, bucket, tgt.Arn); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ }
+ if _, err := globalBucketMetadataSys.Delete(ctx, bucket, bucketTargetsFile); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ // Write success response.
+ writeSuccessResponseHeadersOnly(w)
+}
+
+// GetBucketReplicationMetricsHandler - GET Bucket replication metrics. // Deprecated Aug 2023
+// ----------
+// Gets the replication metrics for a bucket.
+func (api objectAPIHandlers) GetBucketReplicationMetricsHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetBucketReplicationMetrics")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ // check if user has permissions to perform this operation
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Check if bucket exists.
+ if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if _, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ w.Header().Set(xhttp.ContentType, string(mimeJSON))
+
+ enc := json.NewEncoder(w)
+ stats := globalReplicationStats.Load().getLatestReplicationStats(bucket)
+ bwRpt := globalNotificationSys.GetBandwidthReports(ctx, bucket)
+ bwMap := bwRpt.BucketStats
+ for arn, st := range stats.ReplicationStats.Stats {
+ for opts, bw := range bwMap {
+ if opts.ReplicationARN != "" && opts.ReplicationARN == arn {
+ st.BandWidthLimitInBytesPerSecond = bw.LimitInBytesPerSecond
+ st.CurrentBandwidthInBytesPerSecond = bw.CurrentBandwidthInBytesPerSecond
+ stats.ReplicationStats.Stats[arn] = st
+ }
+ }
+ }
+
+ if err := enc.Encode(stats.ReplicationStats); err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+}
+
+// GetBucketReplicationMetricsV2Handler - GET Bucket replication metrics.
+// ----------
+// Gets the replication metrics for a bucket.
+func (api objectAPIHandlers) GetBucketReplicationMetricsV2Handler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetBucketReplicationMetricsV2")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ // check if user has permissions to perform this operation
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Check if bucket exists.
+ if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if _, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ w.Header().Set(xhttp.ContentType, string(mimeJSON))
+
+ enc := json.NewEncoder(w)
+ stats := globalReplicationStats.Load().getLatestReplicationStats(bucket)
+ bwRpt := globalNotificationSys.GetBandwidthReports(ctx, bucket)
+ bwMap := bwRpt.BucketStats
+ for arn, st := range stats.ReplicationStats.Stats {
+ for opts, bw := range bwMap {
+ if opts.ReplicationARN != "" && opts.ReplicationARN == arn {
+ st.BandWidthLimitInBytesPerSecond = bw.LimitInBytesPerSecond
+ st.CurrentBandwidthInBytesPerSecond = bw.CurrentBandwidthInBytesPerSecond
+ stats.ReplicationStats.Stats[arn] = st
+ }
+ }
+ }
+ stats.Uptime = UTCNow().Unix() - globalBootTime.Unix()
+
+ if err := enc.Encode(stats); err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+}
+
+// ResetBucketReplicationStartHandler - starts a replication reset for all objects in a bucket which
+// qualify for replication and re-sync the object(s) to target, provided ExistingObjectReplication is
+// enabled for the qualifying rule. This API is a MinIO only extension provided for situations where
+// remote target is entirely lost,and previously replicated objects need to be re-synced. If resync is
+// already in progress it returns an error
+func (api objectAPIHandlers) ResetBucketReplicationStartHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "ResetBucketReplicationStart")
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+ durationStr := r.URL.Query().Get("older-than")
+ arn := r.URL.Query().Get("arn")
+ resetID := r.URL.Query().Get("reset-id")
+ if resetID == "" {
+ resetID = mustGetUUID()
+ }
+ var (
+ days time.Duration
+ err error
+ )
+ if durationStr != "" {
+ days, err = time.ParseDuration(durationStr)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, InvalidArgument{
+ Bucket: bucket,
+ Err: fmt.Errorf("invalid query parameter older-than %s for %s : %w", durationStr, bucket, err),
+ }), r.URL)
+ return
+ }
+ }
+ resetBeforeDate := UTCNow().AddDate(0, 0, -1*int(days/24))
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.ResetBucketReplicationStateAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Check if bucket exists.
+ if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ config, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ hasARN, hasExistingObjEnabled := config.HasExistingObjectReplication(arn)
+ if !hasARN {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrRemoteTargetNotFoundError), r.URL)
+ return
+ }
+
+ if !hasExistingObjEnabled {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationNoExistingObjects), r.URL)
+ return
+ }
+
+ tgtArns := config.FilterTargetArns(
+ replication.ObjectOpts{
+ OpType: replication.ResyncReplicationType,
+ TargetArn: arn,
+ })
+
+ if len(tgtArns) == 0 {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
+ Bucket: bucket,
+ Err: fmt.Errorf("Remote target ARN %s missing or ineligible for replication resync", arn),
+ }), r.URL)
+ return
+ }
+
+ if len(tgtArns) > 1 && arn == "" {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
+ Bucket: bucket,
+ Err: fmt.Errorf("ARN should be specified for replication reset"),
+ }), r.URL)
+ return
+ }
+ var rinfo ResyncTargetsInfo
+ target := globalBucketTargetSys.GetRemoteBucketTargetByArn(ctx, bucket, tgtArns[0])
+ target.ResetBeforeDate = UTCNow().AddDate(0, 0, -1*int(days/24))
+ target.ResetID = resetID
+ rinfo.Targets = append(rinfo.Targets, ResyncTarget{Arn: tgtArns[0], ResetID: target.ResetID})
+ if err = globalBucketTargetSys.SetTarget(ctx, bucket, &target, true); err != nil {
+ switch err.(type) {
+ case RemoteTargetConnectionErr:
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationRemoteConnectionError, err), r.URL)
+ default:
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ }
+ return
+ }
+ targets, err := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ tgtBytes, err := json.Marshal(&targets)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
+ return
+ }
+ if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketTargetsFile, tgtBytes); err != nil {
+ writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if err := globalReplicationPool.Get().resyncer.start(ctx, objectAPI, resyncOpts{
+ bucket: bucket,
+ arn: arn,
+ resyncID: resetID,
+ resyncBefore: resetBeforeDate,
+ }); err != nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
+ Bucket: bucket,
+ Err: err,
+ }), r.URL)
+ return
+ }
+
+ data, err := json.Marshal(rinfo)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ // Write success response.
+ writeSuccessResponseJSON(w, data)
+}
+
+// ResetBucketReplicationStatusHandler - returns the status of replication reset.
+// This API is a MinIO only extension
+func (api objectAPIHandlers) ResetBucketReplicationStatusHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "ResetBucketReplicationStatus")
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+ arn := r.URL.Query().Get("arn")
+ var err error
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.ResetBucketReplicationStateAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Check if bucket exists.
+ if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if _, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ brs, err := loadBucketResyncMetadata(ctx, bucket, objectAPI)
+ if err != nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
+ Bucket: bucket,
+ Err: fmt.Errorf("replication resync status not available for %s (%s)", arn, err.Error()),
+ }), r.URL)
+ return
+ }
+
+ var rinfo ResyncTargetsInfo
+ for tarn, st := range brs.TargetsMap {
+ if arn != "" && tarn != arn {
+ continue
+ }
+ rinfo.Targets = append(rinfo.Targets, ResyncTarget{
+ Arn: tarn,
+ ResetID: st.ResyncID,
+ StartTime: st.StartTime,
+ EndTime: st.LastUpdate,
+ ResyncStatus: st.ResyncStatus.String(),
+ ReplicatedSize: st.ReplicatedSize,
+ ReplicatedCount: st.ReplicatedCount,
+ FailedSize: st.FailedSize,
+ FailedCount: st.FailedCount,
+ Bucket: st.Bucket,
+ Object: st.Object,
+ })
+ }
+ data, err := json.Marshal(rinfo)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Write success response.
+ writeSuccessResponseJSON(w, data)
+}
+
+// ValidateBucketReplicationCredsHandler - validate replication credentials for a bucket.
+// ----------
+func (api objectAPIHandlers) ValidateBucketReplicationCredsHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "ValidateBucketReplicationCreds")
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+ // Check if bucket exists.
+ if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationValidationError, err), r.URL)
+ return
+ }
+
+ if versioned := globalBucketVersioningSys.Enabled(bucket); !versioned {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationNeedsVersioningError), r.URL)
+ return
+ }
+ replicationConfig, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
+ if err != nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationConfigurationNotFoundError, err), r.URL)
+ return
+ }
+
+ lockEnabled := false
+ lcfg, _, err := globalBucketMetadataSys.GetObjectLockConfig(bucket)
+ if err != nil {
+ if !errors.Is(err, BucketObjectLockConfigNotFound{Bucket: bucket}) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationValidationError, err), r.URL)
+ return
+ }
+ }
+ if lcfg != nil {
+ lockEnabled = lcfg.Enabled()
+ }
+
+ sameTarget, apiErr := validateReplicationDestination(ctx, bucket, replicationConfig, &validateReplicationDestinationOptions{CheckRemoteBucket: true})
+ if apiErr != noError {
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+
+ // Validate the bucket replication config
+ if err = replicationConfig.Validate(bucket, sameTarget); err != nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationValidationError, err), r.URL)
+ return
+ }
+ buf := bytes.Repeat([]byte("a"), 8)
+ for _, rule := range replicationConfig.Rules {
+ if rule.Status == replication.Disabled {
+ continue
+ }
+ clnt := globalBucketTargetSys.GetRemoteTargetClient(bucket, rule.Destination.Bucket)
+ if clnt == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrRemoteTargetNotFoundError, fmt.Errorf("replication config with rule ID %s has a stale target", rule.ID)), r.URL)
+ return
+ }
+ if lockEnabled {
+ lock, _, _, _, err := clnt.GetObjectLockConfig(ctx, clnt.Bucket)
+ if err != nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationValidationError, err), r.URL)
+ return
+ }
+ if lock != objectlock.Enabled {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationDestinationMissingLock, fmt.Errorf("target bucket %s is not object lock enabled", clnt.Bucket)), r.URL)
+ return
+ }
+ }
+ vcfg, err := clnt.GetBucketVersioning(ctx, clnt.Bucket)
+ if err != nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationValidationError, err), r.URL)
+ return
+ }
+ if !vcfg.Enabled() {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrRemoteTargetNotVersionedError, fmt.Errorf("target bucket %s is not versioned", clnt.Bucket)), r.URL)
+ return
+ }
+ if sameTarget && bucket == clnt.Bucket {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBucketRemoteIdenticalToSource), r.URL)
+ return
+ }
+
+ reader := bytes.NewReader(buf)
+ // fake a PutObject and RemoveObject call to validate permissions
+ c := &minio.Core{Client: clnt.Client}
+ putOpts := minio.PutObjectOptions{
+ Internal: minio.AdvancedPutOptions{
+ SourceVersionID: mustGetUUID(),
+ ReplicationStatus: minio.ReplicationStatusReplica,
+ SourceMTime: time.Now(),
+ ReplicationRequest: true, // always set this to distinguish between `mc mirror` replication and serverside
+ ReplicationValidityCheck: true, // set this to validate the replication config
+ },
+ }
+ obj := path.Join(minioReservedBucket, globalLocalNodeNameHex, "deleteme")
+ ui, err := c.PutObject(ctx, clnt.Bucket, obj, reader, int64(len(buf)), "", "", putOpts)
+ if err != nil && !isReplicationPermissionCheck(ErrorRespToObjectError(err, bucket, obj)) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationValidationError, fmt.Errorf("s3:ReplicateObject permissions missing for replication user: %w", err)), r.URL)
+ return
+ }
+
+ err = c.RemoveObject(ctx, clnt.Bucket, obj, minio.RemoveObjectOptions{
+ VersionID: ui.VersionID,
+ Internal: minio.AdvancedRemoveOptions{
+ ReplicationDeleteMarker: true,
+ ReplicationMTime: time.Now(),
+ ReplicationStatus: minio.ReplicationStatusReplica,
+ ReplicationRequest: true, // always set this to distinguish between `mc mirror` replication and serverside
+ ReplicationValidityCheck: true, // set this to validate the replication config
+ },
+ })
+ if err != nil && !isReplicationPermissionCheck(ErrorRespToObjectError(err, bucket, obj)) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationValidationError, fmt.Errorf("s3:ReplicateDelete permissions missing for replication user: %w", err)), r.URL)
+ return
+ }
+ // fake a versioned delete - to ensure deny policies are not in place
+ err = c.RemoveObject(ctx, clnt.Bucket, obj, minio.RemoveObjectOptions{
+ VersionID: ui.VersionID,
+ Internal: minio.AdvancedRemoveOptions{
+ ReplicationDeleteMarker: false,
+ ReplicationMTime: time.Now(),
+ ReplicationStatus: minio.ReplicationStatusReplica,
+ ReplicationRequest: true, // always set this to distinguish between `mc mirror` replication and serverside
+ ReplicationValidityCheck: true, // set this to validate the replication config
+ },
+ })
+ if err != nil && !isReplicationPermissionCheck(ErrorRespToObjectError(err, bucket, obj)) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationValidationError, fmt.Errorf("s3:ReplicateDelete/s3:DeleteObject permissions missing for replication user: %w", err)), r.URL)
+ return
+ }
+ }
+
+ // Write success response.
+ writeSuccessResponseHeadersOnly(w)
+}
diff --git a/cmd/bucket-replication-metrics.go b/cmd/bucket-replication-metrics.go
new file mode 100644
index 00000000..aa4cfb96
--- /dev/null
+++ b/cmd/bucket-replication-metrics.go
@@ -0,0 +1,523 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "fmt"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/rcrowley/go-metrics"
+)
+
+//go:generate msgp -file $GOFILE
+
+const (
+ // beta is the weight used to calculate exponential moving average
+ beta = 0.1 // Number of averages considered = 1/(1-beta)
+)
+
+// rateMeasurement captures the transfer details for one bucket/target
+//msgp:ignore rateMeasurement
+
+type rateMeasurement struct {
+ lock sync.Mutex
+ bytesSinceLastWindow uint64 // Total bytes since last window was processed
+ startTime time.Time // Start time for window
+ expMovingAvg float64 // Previously calculated exponential moving average
+}
+
+// newRateMeasurement creates a new instance of the measurement with the initial start time.
+func newRateMeasurement(initTime time.Time) *rateMeasurement {
+ return &rateMeasurement{
+ startTime: initTime,
+ }
+}
+
+// incrementBytes add bytes reported for a bucket/target.
+func (m *rateMeasurement) incrementBytes(bytes uint64) {
+ atomic.AddUint64(&m.bytesSinceLastWindow, bytes)
+}
+
+// updateExponentialMovingAverage processes the measurements captured so far.
+func (m *rateMeasurement) updateExponentialMovingAverage(endTime time.Time) {
+ // Calculate aggregate avg bandwidth and exp window avg
+ m.lock.Lock()
+ defer func() {
+ m.startTime = endTime
+ m.lock.Unlock()
+ }()
+
+ if m.startTime.IsZero() {
+ return
+ }
+
+ if endTime.Before(m.startTime) {
+ return
+ }
+
+ duration := endTime.Sub(m.startTime)
+
+ bytesSinceLastWindow := atomic.SwapUint64(&m.bytesSinceLastWindow, 0)
+
+ if m.expMovingAvg == 0 {
+ // Should address initial calculation and should be fine for resuming from 0
+ m.expMovingAvg = float64(bytesSinceLastWindow) / duration.Seconds()
+ return
+ }
+
+ increment := float64(bytesSinceLastWindow) / duration.Seconds()
+ m.expMovingAvg = exponentialMovingAverage(beta, m.expMovingAvg, increment)
+}
+
+// exponentialMovingAverage calculates the exponential moving average
+func exponentialMovingAverage(beta, previousAvg, incrementAvg float64) float64 {
+ return (1-beta)*incrementAvg + beta*previousAvg
+}
+
+// getExpMovingAvgBytesPerSecond returns the exponential moving average for the bucket/target in bytes
+func (m *rateMeasurement) getExpMovingAvgBytesPerSecond() float64 {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+ return m.expMovingAvg
+}
+
+// ActiveWorkerStat is stat for active replication workers
+type ActiveWorkerStat struct {
+ Curr int `json:"curr"`
+ Avg float32 `json:"avg"`
+ Max int `json:"max"`
+ hist metrics.Histogram
+}
+
+func newActiveWorkerStat(r metrics.Registry) *ActiveWorkerStat {
+ h := metrics.NewHistogram(metrics.NewUniformSample(100))
+ r.Register("replication.active_workers", h)
+ return &ActiveWorkerStat{
+ hist: h,
+ }
+}
+
+// update curr and max workers;
+func (a *ActiveWorkerStat) update() {
+ if a == nil {
+ return
+ }
+ a.Curr = globalReplicationPool.Get().ActiveWorkers()
+ a.hist.Update(int64(a.Curr))
+ a.Avg = float32(a.hist.Mean())
+ a.Max = int(a.hist.Max())
+}
+
+func (a *ActiveWorkerStat) get() ActiveWorkerStat {
+ w := ActiveWorkerStat{
+ Curr: a.Curr,
+ Avg: a.Avg,
+ Max: a.Max,
+ }
+ return w
+}
+
+// QStat holds queue stats for replication
+type QStat struct {
+ Count float64 `json:"count"`
+ Bytes float64 `json:"bytes"`
+}
+
+func (q *QStat) add(o QStat) QStat {
+ return QStat{Bytes: q.Bytes + o.Bytes, Count: q.Count + o.Count}
+}
+
+// InQueueMetric holds queue stats for replication
+type InQueueMetric struct {
+ Curr QStat `json:"curr" msg:"cq"`
+ Avg QStat `json:"avg" msg:"aq"`
+ Max QStat `json:"max" msg:"pq"`
+}
+
+func (qm InQueueMetric) merge(o InQueueMetric) InQueueMetric {
+ return InQueueMetric{
+ Curr: qm.Curr.add(o.Curr),
+ Avg: qm.Avg.add(o.Avg),
+ Max: qm.Max.add(o.Max),
+ }
+}
+
+type queueCache struct {
+ srQueueStats InQueueStats
+ bucketStats map[string]InQueueStats
+ sync.RWMutex // mutex for queue stats
+}
+
+func newQueueCache(r metrics.Registry) queueCache {
+ return queueCache{
+ bucketStats: make(map[string]InQueueStats),
+ srQueueStats: newInQueueStats(r, "site"),
+ }
+}
+
+func (q *queueCache) update() {
+ q.Lock()
+ defer q.Unlock()
+ q.srQueueStats.update()
+ for _, s := range q.bucketStats {
+ s.update()
+ }
+}
+
+func (q *queueCache) getBucketStats(bucket string) InQueueMetric {
+ q.RLock()
+ defer q.RUnlock()
+ v, ok := q.bucketStats[bucket]
+ if !ok {
+ return InQueueMetric{}
+ }
+ return InQueueMetric{
+ Curr: QStat{Bytes: float64(v.nowBytes), Count: float64(v.nowCount)},
+ Max: QStat{Bytes: float64(v.histBytes.Max()), Count: float64(v.histCounts.Max())},
+ Avg: QStat{Bytes: v.histBytes.Mean(), Count: v.histCounts.Mean()},
+ }
+}
+
+func (q *queueCache) getSiteStats() InQueueMetric {
+ q.RLock()
+ defer q.RUnlock()
+ v := q.srQueueStats
+ return InQueueMetric{
+ Curr: QStat{Bytes: float64(v.nowBytes), Count: float64(v.nowCount)},
+ Max: QStat{Bytes: float64(v.histBytes.Max()), Count: float64(v.histCounts.Max())},
+ Avg: QStat{Bytes: v.histBytes.Mean(), Count: v.histCounts.Mean()},
+ }
+}
+
+// InQueueStats holds queue stats for replication
+type InQueueStats struct {
+ nowBytes int64 `json:"-"`
+ nowCount int64 `json:"-"`
+ histCounts metrics.Histogram
+ histBytes metrics.Histogram
+}
+
+func newInQueueStats(r metrics.Registry, lbl string) InQueueStats {
+ histCounts := metrics.NewHistogram(metrics.NewUniformSample(100))
+ histBytes := metrics.NewHistogram(metrics.NewUniformSample(100))
+
+ r.Register("replication.queue.counts."+lbl, histCounts)
+ r.Register("replication.queue.bytes."+lbl, histBytes)
+
+ return InQueueStats{
+ histCounts: histCounts,
+ histBytes: histBytes,
+ }
+}
+
+func (q *InQueueStats) update() {
+ q.histBytes.Update(atomic.LoadInt64(&q.nowBytes))
+ q.histCounts.Update(atomic.LoadInt64(&q.nowCount))
+}
+
+// XferStats has transfer stats for replication
+type XferStats struct {
+ Curr float64 `json:"currRate" msg:"cr"`
+ Avg float64 `json:"avgRate" msg:"av"`
+ Peak float64 `json:"peakRate" msg:"p"`
+ N int64 `json:"n" msg:"n"`
+ measure *rateMeasurement `json:"-"`
+ sma *SMA `json:"-"`
+}
+
+// Clone returns a copy of XferStats
+func (rx *XferStats) Clone() *XferStats {
+ curr := rx.curr()
+ peak := rx.Peak
+ if curr > peak {
+ peak = curr
+ }
+ return &XferStats{
+ Curr: curr,
+ Avg: rx.Avg,
+ Peak: peak,
+ N: rx.N,
+ measure: rx.measure,
+ }
+}
+
+func newXferStats() *XferStats {
+ return &XferStats{
+ measure: newRateMeasurement(time.Now()),
+ sma: newSMA(50),
+ }
+}
+
+func (rx *XferStats) String() string {
+ return fmt.Sprintf("curr=%f avg=%f, peak=%f", rx.curr(), rx.Avg, rx.Peak)
+}
+
+func (rx *XferStats) curr() float64 {
+ if rx.measure == nil {
+ return 0.0
+ }
+ return rx.measure.getExpMovingAvgBytesPerSecond()
+}
+
+func (rx *XferStats) merge(o XferStats) XferStats {
+ curr := calcAvg(rx.curr(), o.curr(), rx.N, o.N)
+ peak := rx.Peak
+ if o.Peak > peak {
+ peak = o.Peak
+ }
+ if curr > peak {
+ peak = curr
+ }
+ return XferStats{
+ Avg: calcAvg(rx.Avg, o.Avg, rx.N, o.N),
+ Peak: peak,
+ Curr: curr,
+ measure: rx.measure,
+ N: rx.N + o.N,
+ }
+}
+
+func calcAvg(x, y float64, n1, n2 int64) float64 {
+ if n1+n2 == 0 {
+ return 0
+ }
+ avg := (x*float64(n1) + y*float64(n2)) / float64(n1+n2)
+ return avg
+}
+
+// Add a new transfer
+func (rx *XferStats) addSize(sz int64, t time.Duration) {
+ if rx.measure == nil {
+ rx.measure = newRateMeasurement(time.Now())
+ }
+ rx.measure.incrementBytes(uint64(sz))
+ rx.Curr = rx.measure.getExpMovingAvgBytesPerSecond()
+ rx.sma.addSample(rx.Curr)
+ rx.Avg = rx.sma.simpleMovingAvg()
+ if rx.Curr > rx.Peak {
+ rx.Peak = rx.Curr
+ }
+ rx.N++
+}
+
+// ReplicationMRFStats holds stats of MRF backlog saved to disk in the last 5 minutes
+// and number of entries that failed replication after 3 retries
+type ReplicationMRFStats struct {
+ LastFailedCount uint64 `json:"failedCount_last5min"`
+ // Count of unreplicated entries that were dropped after MRF retry limit reached since cluster start.
+ TotalDroppedCount uint64 `json:"droppedCount_since_uptime"`
+ // Bytes of unreplicated entries that were dropped after MRF retry limit reached since cluster start.
+ TotalDroppedBytes uint64 `json:"droppedBytes_since_uptime"`
+}
+
+// SMA struct for calculating simple moving average
+type SMA struct {
+ buf []float64
+ window int // len of buf
+ idx int // current index in buf
+ CAvg float64 // cumulative average
+ prevSMA float64
+ filledBuf bool
+}
+
+func newSMA(ln int) *SMA {
+ if ln <= 0 {
+ ln = defaultWindowSize
+ }
+ return &SMA{
+ buf: make([]float64, ln),
+ window: ln,
+ idx: 0,
+ }
+}
+
+func (s *SMA) addSample(next float64) {
+ prev := s.buf[s.idx]
+ s.buf[s.idx] = next
+
+ if s.filledBuf {
+ s.prevSMA += (next - prev) / float64(s.window)
+ s.CAvg += (next - s.CAvg) / float64(s.window)
+ } else {
+ s.CAvg = s.simpleMovingAvg()
+ s.prevSMA = s.CAvg
+ }
+ if s.idx == s.window-1 {
+ s.filledBuf = true
+ }
+ s.idx = (s.idx + 1) % s.window
+}
+
+func (s *SMA) simpleMovingAvg() float64 {
+ if s.filledBuf {
+ return s.prevSMA
+ }
+ var tot float64
+ for _, r := range s.buf {
+ tot += r
+ }
+ return tot / float64(s.idx+1)
+}
+
+const (
+ defaultWindowSize = 10
+)
+
+type proxyStatsCache struct {
+ srProxyStats ProxyMetric
+ bucketStats map[string]ProxyMetric
+ sync.RWMutex // mutex for proxy stats
+}
+
+func newProxyStatsCache() proxyStatsCache {
+ return proxyStatsCache{
+ bucketStats: make(map[string]ProxyMetric),
+ }
+}
+
+func (p *proxyStatsCache) inc(bucket string, api replProxyAPI, isErr bool) {
+ p.Lock()
+ defer p.Unlock()
+ v, ok := p.bucketStats[bucket]
+ if !ok {
+ v = ProxyMetric{}
+ }
+ switch api {
+ case putObjectTaggingAPI:
+ if !isErr {
+ atomic.AddUint64(&v.PutTagTotal, 1)
+ atomic.AddUint64(&p.srProxyStats.PutTagTotal, 1)
+ } else {
+ atomic.AddUint64(&v.PutTagFailedTotal, 1)
+ atomic.AddUint64(&p.srProxyStats.PutTagFailedTotal, 1)
+ }
+ case getObjectTaggingAPI:
+ if !isErr {
+ atomic.AddUint64(&v.GetTagTotal, 1)
+ atomic.AddUint64(&p.srProxyStats.GetTagTotal, 1)
+ } else {
+ atomic.AddUint64(&v.GetTagFailedTotal, 1)
+ atomic.AddUint64(&p.srProxyStats.GetTagFailedTotal, 1)
+ }
+ case removeObjectTaggingAPI:
+ if !isErr {
+ atomic.AddUint64(&v.RmvTagTotal, 1)
+ atomic.AddUint64(&p.srProxyStats.RmvTagTotal, 1)
+ } else {
+ atomic.AddUint64(&v.RmvTagFailedTotal, 1)
+ atomic.AddUint64(&p.srProxyStats.RmvTagFailedTotal, 1)
+ }
+ case headObjectAPI:
+ if !isErr {
+ atomic.AddUint64(&v.HeadTotal, 1)
+ atomic.AddUint64(&p.srProxyStats.HeadTotal, 1)
+ } else {
+ atomic.AddUint64(&v.HeadFailedTotal, 1)
+ atomic.AddUint64(&p.srProxyStats.HeadFailedTotal, 1)
+ }
+ case getObjectAPI:
+ if !isErr {
+ atomic.AddUint64(&v.GetTotal, 1)
+ atomic.AddUint64(&p.srProxyStats.GetTotal, 1)
+ } else {
+ atomic.AddUint64(&v.GetFailedTotal, 1)
+ atomic.AddUint64(&p.srProxyStats.GetFailedTotal, 1)
+ }
+ default:
+ return
+ }
+ p.bucketStats[bucket] = v
+}
+
+func (p *proxyStatsCache) getBucketStats(bucket string) ProxyMetric {
+ p.RLock()
+ defer p.RUnlock()
+ v, ok := p.bucketStats[bucket]
+
+ if !ok {
+ return ProxyMetric{}
+ }
+ return ProxyMetric{
+ PutTagTotal: atomic.LoadUint64(&v.PutTagTotal),
+ GetTagTotal: atomic.LoadUint64(&v.GetTagTotal),
+ RmvTagTotal: atomic.LoadUint64(&v.RmvTagTotal),
+ HeadTotal: atomic.LoadUint64(&v.HeadTotal),
+ GetTotal: atomic.LoadUint64(&v.GetTotal),
+
+ PutTagFailedTotal: atomic.LoadUint64(&v.PutTagFailedTotal),
+ GetTagFailedTotal: atomic.LoadUint64(&v.GetTagFailedTotal),
+ RmvTagFailedTotal: atomic.LoadUint64(&v.RmvTagFailedTotal),
+ HeadFailedTotal: atomic.LoadUint64(&v.HeadFailedTotal),
+ GetFailedTotal: atomic.LoadUint64(&v.GetFailedTotal),
+ }
+}
+
+func (p *proxyStatsCache) getSiteStats() ProxyMetric {
+ v := p.srProxyStats
+ return ProxyMetric{
+ PutTagTotal: atomic.LoadUint64(&v.PutTagTotal),
+ GetTagTotal: atomic.LoadUint64(&v.GetTagTotal),
+ RmvTagTotal: atomic.LoadUint64(&v.RmvTagTotal),
+ HeadTotal: atomic.LoadUint64(&v.HeadTotal),
+ GetTotal: atomic.LoadUint64(&v.GetTotal),
+ PutTagFailedTotal: atomic.LoadUint64(&v.PutTagFailedTotal),
+ GetTagFailedTotal: atomic.LoadUint64(&v.GetTagFailedTotal),
+ RmvTagFailedTotal: atomic.LoadUint64(&v.RmvTagFailedTotal),
+ HeadFailedTotal: atomic.LoadUint64(&v.HeadFailedTotal),
+ GetFailedTotal: atomic.LoadUint64(&v.GetFailedTotal),
+ }
+}
+
+type replProxyAPI string
+
+const (
+ putObjectTaggingAPI replProxyAPI = "PutObjectTagging"
+ getObjectTaggingAPI replProxyAPI = "GetObjectTagging"
+ removeObjectTaggingAPI replProxyAPI = "RemoveObjectTagging"
+ headObjectAPI replProxyAPI = "HeadObject"
+ getObjectAPI replProxyAPI = "GetObject"
+)
+
+// ProxyMetric holds stats for replication proxying
+type ProxyMetric struct {
+ PutTagTotal uint64 `json:"putTaggingProxyTotal" msg:"ptc"`
+ GetTagTotal uint64 `json:"getTaggingProxyTotal" msg:"gtc"`
+ RmvTagTotal uint64 `json:"removeTaggingProxyTotal" msg:"rtc"`
+ GetTotal uint64 `json:"getProxyTotal" msg:"gc"`
+ HeadTotal uint64 `json:"headProxyTotal" msg:"hc"`
+ PutTagFailedTotal uint64 `json:"putTaggingProxyFailed" msg:"ptf"`
+ GetTagFailedTotal uint64 `json:"getTaggingProxyFailed" msg:"gtf"`
+ RmvTagFailedTotal uint64 `json:"removeTaggingProxyFailed" msg:"rtf"`
+ GetFailedTotal uint64 `json:"getProxyFailed" msg:"gf"`
+ HeadFailedTotal uint64 `json:"headProxyFailed" msg:"hf"`
+}
+
+func (p *ProxyMetric) add(p2 ProxyMetric) {
+ atomic.AddUint64(&p.GetTotal, p2.GetTotal)
+ atomic.AddUint64(&p.HeadTotal, p2.HeadTotal)
+ atomic.AddUint64(&p.GetTagTotal, p2.GetTagTotal)
+ atomic.AddUint64(&p.PutTagTotal, p2.PutTagTotal)
+ atomic.AddUint64(&p.RmvTagTotal, p2.RmvTagTotal)
+ atomic.AddUint64(&p.GetFailedTotal, p2.GetFailedTotal)
+ atomic.AddUint64(&p.HeadFailedTotal, p2.HeadFailedTotal)
+ atomic.AddUint64(&p.GetTagFailedTotal, p2.GetTagFailedTotal)
+ atomic.AddUint64(&p.PutTagFailedTotal, p2.PutTagFailedTotal)
+ atomic.AddUint64(&p.RmvTagFailedTotal, p2.RmvTagFailedTotal)
+}
diff --git a/cmd/bucket-replication-metrics_gen.go b/cmd/bucket-replication-metrics_gen.go
new file mode 100644
index 00000000..f8eba540
--- /dev/null
+++ b/cmd/bucket-replication-metrics_gen.go
@@ -0,0 +1,1528 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "github.com/tinylib/msgp/msgp"
+)
+
+// DecodeMsg implements msgp.Decodable
+func (z *ActiveWorkerStat) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Curr":
+ z.Curr, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "Curr")
+ return
+ }
+ case "Avg":
+ z.Avg, err = dc.ReadFloat32()
+ if err != nil {
+ err = msgp.WrapError(err, "Avg")
+ return
+ }
+ case "Max":
+ z.Max, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "Max")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z ActiveWorkerStat) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 3
+ // write "Curr"
+ err = en.Append(0x83, 0xa4, 0x43, 0x75, 0x72, 0x72)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.Curr)
+ if err != nil {
+ err = msgp.WrapError(err, "Curr")
+ return
+ }
+ // write "Avg"
+ err = en.Append(0xa3, 0x41, 0x76, 0x67)
+ if err != nil {
+ return
+ }
+ err = en.WriteFloat32(z.Avg)
+ if err != nil {
+ err = msgp.WrapError(err, "Avg")
+ return
+ }
+ // write "Max"
+ err = en.Append(0xa3, 0x4d, 0x61, 0x78)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.Max)
+ if err != nil {
+ err = msgp.WrapError(err, "Max")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z ActiveWorkerStat) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 3
+ // string "Curr"
+ o = append(o, 0x83, 0xa4, 0x43, 0x75, 0x72, 0x72)
+ o = msgp.AppendInt(o, z.Curr)
+ // string "Avg"
+ o = append(o, 0xa3, 0x41, 0x76, 0x67)
+ o = msgp.AppendFloat32(o, z.Avg)
+ // string "Max"
+ o = append(o, 0xa3, 0x4d, 0x61, 0x78)
+ o = msgp.AppendInt(o, z.Max)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ActiveWorkerStat) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Curr":
+ z.Curr, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Curr")
+ return
+ }
+ case "Avg":
+ z.Avg, bts, err = msgp.ReadFloat32Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Avg")
+ return
+ }
+ case "Max":
+ z.Max, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Max")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z ActiveWorkerStat) Msgsize() (s int) {
+ s = 1 + 5 + msgp.IntSize + 4 + msgp.Float32Size + 4 + msgp.IntSize
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *InQueueMetric) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "cq":
+ var zb0002 uint32
+ zb0002, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Curr")
+ return
+ }
+ for zb0002 > 0 {
+ zb0002--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err, "Curr")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Count":
+ z.Curr.Count, err = dc.ReadFloat64()
+ if err != nil {
+ err = msgp.WrapError(err, "Curr", "Count")
+ return
+ }
+ case "Bytes":
+ z.Curr.Bytes, err = dc.ReadFloat64()
+ if err != nil {
+ err = msgp.WrapError(err, "Curr", "Bytes")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err, "Curr")
+ return
+ }
+ }
+ }
+ case "aq":
+ var zb0003 uint32
+ zb0003, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Avg")
+ return
+ }
+ for zb0003 > 0 {
+ zb0003--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err, "Avg")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Count":
+ z.Avg.Count, err = dc.ReadFloat64()
+ if err != nil {
+ err = msgp.WrapError(err, "Avg", "Count")
+ return
+ }
+ case "Bytes":
+ z.Avg.Bytes, err = dc.ReadFloat64()
+ if err != nil {
+ err = msgp.WrapError(err, "Avg", "Bytes")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err, "Avg")
+ return
+ }
+ }
+ }
+ case "pq":
+ var zb0004 uint32
+ zb0004, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Max")
+ return
+ }
+ for zb0004 > 0 {
+ zb0004--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err, "Max")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Count":
+ z.Max.Count, err = dc.ReadFloat64()
+ if err != nil {
+ err = msgp.WrapError(err, "Max", "Count")
+ return
+ }
+ case "Bytes":
+ z.Max.Bytes, err = dc.ReadFloat64()
+ if err != nil {
+ err = msgp.WrapError(err, "Max", "Bytes")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err, "Max")
+ return
+ }
+ }
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *InQueueMetric) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 3
+ // write "cq"
+ err = en.Append(0x83, 0xa2, 0x63, 0x71)
+ if err != nil {
+ return
+ }
+ // map header, size 2
+ // write "Count"
+ err = en.Append(0x82, 0xa5, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteFloat64(z.Curr.Count)
+ if err != nil {
+ err = msgp.WrapError(err, "Curr", "Count")
+ return
+ }
+ // write "Bytes"
+ err = en.Append(0xa5, 0x42, 0x79, 0x74, 0x65, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteFloat64(z.Curr.Bytes)
+ if err != nil {
+ err = msgp.WrapError(err, "Curr", "Bytes")
+ return
+ }
+ // write "aq"
+ err = en.Append(0xa2, 0x61, 0x71)
+ if err != nil {
+ return
+ }
+ // map header, size 2
+ // write "Count"
+ err = en.Append(0x82, 0xa5, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteFloat64(z.Avg.Count)
+ if err != nil {
+ err = msgp.WrapError(err, "Avg", "Count")
+ return
+ }
+ // write "Bytes"
+ err = en.Append(0xa5, 0x42, 0x79, 0x74, 0x65, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteFloat64(z.Avg.Bytes)
+ if err != nil {
+ err = msgp.WrapError(err, "Avg", "Bytes")
+ return
+ }
+ // write "pq"
+ err = en.Append(0xa2, 0x70, 0x71)
+ if err != nil {
+ return
+ }
+ // map header, size 2
+ // write "Count"
+ err = en.Append(0x82, 0xa5, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteFloat64(z.Max.Count)
+ if err != nil {
+ err = msgp.WrapError(err, "Max", "Count")
+ return
+ }
+ // write "Bytes"
+ err = en.Append(0xa5, 0x42, 0x79, 0x74, 0x65, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteFloat64(z.Max.Bytes)
+ if err != nil {
+ err = msgp.WrapError(err, "Max", "Bytes")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *InQueueMetric) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 3
+ // string "cq"
+ o = append(o, 0x83, 0xa2, 0x63, 0x71)
+ // map header, size 2
+ // string "Count"
+ o = append(o, 0x82, 0xa5, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ o = msgp.AppendFloat64(o, z.Curr.Count)
+ // string "Bytes"
+ o = append(o, 0xa5, 0x42, 0x79, 0x74, 0x65, 0x73)
+ o = msgp.AppendFloat64(o, z.Curr.Bytes)
+ // string "aq"
+ o = append(o, 0xa2, 0x61, 0x71)
+ // map header, size 2
+ // string "Count"
+ o = append(o, 0x82, 0xa5, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ o = msgp.AppendFloat64(o, z.Avg.Count)
+ // string "Bytes"
+ o = append(o, 0xa5, 0x42, 0x79, 0x74, 0x65, 0x73)
+ o = msgp.AppendFloat64(o, z.Avg.Bytes)
+ // string "pq"
+ o = append(o, 0xa2, 0x70, 0x71)
+ // map header, size 2
+ // string "Count"
+ o = append(o, 0x82, 0xa5, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ o = msgp.AppendFloat64(o, z.Max.Count)
+ // string "Bytes"
+ o = append(o, 0xa5, 0x42, 0x79, 0x74, 0x65, 0x73)
+ o = msgp.AppendFloat64(o, z.Max.Bytes)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *InQueueMetric) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "cq":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Curr")
+ return
+ }
+ for zb0002 > 0 {
+ zb0002--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Curr")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Count":
+ z.Curr.Count, bts, err = msgp.ReadFloat64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Curr", "Count")
+ return
+ }
+ case "Bytes":
+ z.Curr.Bytes, bts, err = msgp.ReadFloat64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Curr", "Bytes")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Curr")
+ return
+ }
+ }
+ }
+ case "aq":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Avg")
+ return
+ }
+ for zb0003 > 0 {
+ zb0003--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Avg")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Count":
+ z.Avg.Count, bts, err = msgp.ReadFloat64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Avg", "Count")
+ return
+ }
+ case "Bytes":
+ z.Avg.Bytes, bts, err = msgp.ReadFloat64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Avg", "Bytes")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Avg")
+ return
+ }
+ }
+ }
+ case "pq":
+ var zb0004 uint32
+ zb0004, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Max")
+ return
+ }
+ for zb0004 > 0 {
+ zb0004--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Max")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Count":
+ z.Max.Count, bts, err = msgp.ReadFloat64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Max", "Count")
+ return
+ }
+ case "Bytes":
+ z.Max.Bytes, bts, err = msgp.ReadFloat64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Max", "Bytes")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Max")
+ return
+ }
+ }
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *InQueueMetric) Msgsize() (s int) {
+ s = 1 + 3 + 1 + 6 + msgp.Float64Size + 6 + msgp.Float64Size + 3 + 1 + 6 + msgp.Float64Size + 6 + msgp.Float64Size + 3 + 1 + 6 + msgp.Float64Size + 6 + msgp.Float64Size
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *InQueueStats) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z InQueueStats) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 0
+ _ = z
+ err = en.Append(0x80)
+ if err != nil {
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z InQueueStats) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 0
+ _ = z
+ o = append(o, 0x80)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *InQueueStats) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z InQueueStats) Msgsize() (s int) {
+ s = 1
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *ProxyMetric) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ptc":
+ z.PutTagTotal, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "PutTagTotal")
+ return
+ }
+ case "gtc":
+ z.GetTagTotal, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "GetTagTotal")
+ return
+ }
+ case "rtc":
+ z.RmvTagTotal, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "RmvTagTotal")
+ return
+ }
+ case "gc":
+ z.GetTotal, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "GetTotal")
+ return
+ }
+ case "hc":
+ z.HeadTotal, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "HeadTotal")
+ return
+ }
+ case "ptf":
+ z.PutTagFailedTotal, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "PutTagFailedTotal")
+ return
+ }
+ case "gtf":
+ z.GetTagFailedTotal, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "GetTagFailedTotal")
+ return
+ }
+ case "rtf":
+ z.RmvTagFailedTotal, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "RmvTagFailedTotal")
+ return
+ }
+ case "gf":
+ z.GetFailedTotal, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "GetFailedTotal")
+ return
+ }
+ case "hf":
+ z.HeadFailedTotal, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "HeadFailedTotal")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *ProxyMetric) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 10
+ // write "ptc"
+ err = en.Append(0x8a, 0xa3, 0x70, 0x74, 0x63)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.PutTagTotal)
+ if err != nil {
+ err = msgp.WrapError(err, "PutTagTotal")
+ return
+ }
+ // write "gtc"
+ err = en.Append(0xa3, 0x67, 0x74, 0x63)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.GetTagTotal)
+ if err != nil {
+ err = msgp.WrapError(err, "GetTagTotal")
+ return
+ }
+ // write "rtc"
+ err = en.Append(0xa3, 0x72, 0x74, 0x63)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.RmvTagTotal)
+ if err != nil {
+ err = msgp.WrapError(err, "RmvTagTotal")
+ return
+ }
+ // write "gc"
+ err = en.Append(0xa2, 0x67, 0x63)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.GetTotal)
+ if err != nil {
+ err = msgp.WrapError(err, "GetTotal")
+ return
+ }
+ // write "hc"
+ err = en.Append(0xa2, 0x68, 0x63)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.HeadTotal)
+ if err != nil {
+ err = msgp.WrapError(err, "HeadTotal")
+ return
+ }
+ // write "ptf"
+ err = en.Append(0xa3, 0x70, 0x74, 0x66)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.PutTagFailedTotal)
+ if err != nil {
+ err = msgp.WrapError(err, "PutTagFailedTotal")
+ return
+ }
+ // write "gtf"
+ err = en.Append(0xa3, 0x67, 0x74, 0x66)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.GetTagFailedTotal)
+ if err != nil {
+ err = msgp.WrapError(err, "GetTagFailedTotal")
+ return
+ }
+ // write "rtf"
+ err = en.Append(0xa3, 0x72, 0x74, 0x66)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.RmvTagFailedTotal)
+ if err != nil {
+ err = msgp.WrapError(err, "RmvTagFailedTotal")
+ return
+ }
+ // write "gf"
+ err = en.Append(0xa2, 0x67, 0x66)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.GetFailedTotal)
+ if err != nil {
+ err = msgp.WrapError(err, "GetFailedTotal")
+ return
+ }
+ // write "hf"
+ err = en.Append(0xa2, 0x68, 0x66)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.HeadFailedTotal)
+ if err != nil {
+ err = msgp.WrapError(err, "HeadFailedTotal")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *ProxyMetric) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 10
+ // string "ptc"
+ o = append(o, 0x8a, 0xa3, 0x70, 0x74, 0x63)
+ o = msgp.AppendUint64(o, z.PutTagTotal)
+ // string "gtc"
+ o = append(o, 0xa3, 0x67, 0x74, 0x63)
+ o = msgp.AppendUint64(o, z.GetTagTotal)
+ // string "rtc"
+ o = append(o, 0xa3, 0x72, 0x74, 0x63)
+ o = msgp.AppendUint64(o, z.RmvTagTotal)
+ // string "gc"
+ o = append(o, 0xa2, 0x67, 0x63)
+ o = msgp.AppendUint64(o, z.GetTotal)
+ // string "hc"
+ o = append(o, 0xa2, 0x68, 0x63)
+ o = msgp.AppendUint64(o, z.HeadTotal)
+ // string "ptf"
+ o = append(o, 0xa3, 0x70, 0x74, 0x66)
+ o = msgp.AppendUint64(o, z.PutTagFailedTotal)
+ // string "gtf"
+ o = append(o, 0xa3, 0x67, 0x74, 0x66)
+ o = msgp.AppendUint64(o, z.GetTagFailedTotal)
+ // string "rtf"
+ o = append(o, 0xa3, 0x72, 0x74, 0x66)
+ o = msgp.AppendUint64(o, z.RmvTagFailedTotal)
+ // string "gf"
+ o = append(o, 0xa2, 0x67, 0x66)
+ o = msgp.AppendUint64(o, z.GetFailedTotal)
+ // string "hf"
+ o = append(o, 0xa2, 0x68, 0x66)
+ o = msgp.AppendUint64(o, z.HeadFailedTotal)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ProxyMetric) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ptc":
+ z.PutTagTotal, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "PutTagTotal")
+ return
+ }
+ case "gtc":
+ z.GetTagTotal, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "GetTagTotal")
+ return
+ }
+ case "rtc":
+ z.RmvTagTotal, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "RmvTagTotal")
+ return
+ }
+ case "gc":
+ z.GetTotal, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "GetTotal")
+ return
+ }
+ case "hc":
+ z.HeadTotal, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "HeadTotal")
+ return
+ }
+ case "ptf":
+ z.PutTagFailedTotal, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "PutTagFailedTotal")
+ return
+ }
+ case "gtf":
+ z.GetTagFailedTotal, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "GetTagFailedTotal")
+ return
+ }
+ case "rtf":
+ z.RmvTagFailedTotal, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "RmvTagFailedTotal")
+ return
+ }
+ case "gf":
+ z.GetFailedTotal, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "GetFailedTotal")
+ return
+ }
+ case "hf":
+ z.HeadFailedTotal, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "HeadFailedTotal")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *ProxyMetric) Msgsize() (s int) {
+ s = 1 + 4 + msgp.Uint64Size + 4 + msgp.Uint64Size + 4 + msgp.Uint64Size + 3 + msgp.Uint64Size + 3 + msgp.Uint64Size + 4 + msgp.Uint64Size + 4 + msgp.Uint64Size + 4 + msgp.Uint64Size + 3 + msgp.Uint64Size + 3 + msgp.Uint64Size
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *QStat) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Count":
+ z.Count, err = dc.ReadFloat64()
+ if err != nil {
+ err = msgp.WrapError(err, "Count")
+ return
+ }
+ case "Bytes":
+ z.Bytes, err = dc.ReadFloat64()
+ if err != nil {
+ err = msgp.WrapError(err, "Bytes")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z QStat) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 2
+ // write "Count"
+ err = en.Append(0x82, 0xa5, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteFloat64(z.Count)
+ if err != nil {
+ err = msgp.WrapError(err, "Count")
+ return
+ }
+ // write "Bytes"
+ err = en.Append(0xa5, 0x42, 0x79, 0x74, 0x65, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteFloat64(z.Bytes)
+ if err != nil {
+ err = msgp.WrapError(err, "Bytes")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z QStat) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 2
+ // string "Count"
+ o = append(o, 0x82, 0xa5, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ o = msgp.AppendFloat64(o, z.Count)
+ // string "Bytes"
+ o = append(o, 0xa5, 0x42, 0x79, 0x74, 0x65, 0x73)
+ o = msgp.AppendFloat64(o, z.Bytes)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *QStat) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Count":
+ z.Count, bts, err = msgp.ReadFloat64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Count")
+ return
+ }
+ case "Bytes":
+ z.Bytes, bts, err = msgp.ReadFloat64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Bytes")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z QStat) Msgsize() (s int) {
+ s = 1 + 6 + msgp.Float64Size + 6 + msgp.Float64Size
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *ReplicationMRFStats) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "LastFailedCount":
+ z.LastFailedCount, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "LastFailedCount")
+ return
+ }
+ case "TotalDroppedCount":
+ z.TotalDroppedCount, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "TotalDroppedCount")
+ return
+ }
+ case "TotalDroppedBytes":
+ z.TotalDroppedBytes, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "TotalDroppedBytes")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z ReplicationMRFStats) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 3
+ // write "LastFailedCount"
+ err = en.Append(0x83, 0xaf, 0x4c, 0x61, 0x73, 0x74, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.LastFailedCount)
+ if err != nil {
+ err = msgp.WrapError(err, "LastFailedCount")
+ return
+ }
+ // write "TotalDroppedCount"
+ err = en.Append(0xb1, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x44, 0x72, 0x6f, 0x70, 0x70, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.TotalDroppedCount)
+ if err != nil {
+ err = msgp.WrapError(err, "TotalDroppedCount")
+ return
+ }
+ // write "TotalDroppedBytes"
+ err = en.Append(0xb1, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x44, 0x72, 0x6f, 0x70, 0x70, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.TotalDroppedBytes)
+ if err != nil {
+ err = msgp.WrapError(err, "TotalDroppedBytes")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z ReplicationMRFStats) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 3
+ // string "LastFailedCount"
+ o = append(o, 0x83, 0xaf, 0x4c, 0x61, 0x73, 0x74, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ o = msgp.AppendUint64(o, z.LastFailedCount)
+ // string "TotalDroppedCount"
+ o = append(o, 0xb1, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x44, 0x72, 0x6f, 0x70, 0x70, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ o = msgp.AppendUint64(o, z.TotalDroppedCount)
+ // string "TotalDroppedBytes"
+ o = append(o, 0xb1, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x44, 0x72, 0x6f, 0x70, 0x70, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73)
+ o = msgp.AppendUint64(o, z.TotalDroppedBytes)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ReplicationMRFStats) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "LastFailedCount":
+ z.LastFailedCount, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "LastFailedCount")
+ return
+ }
+ case "TotalDroppedCount":
+ z.TotalDroppedCount, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "TotalDroppedCount")
+ return
+ }
+ case "TotalDroppedBytes":
+ z.TotalDroppedBytes, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "TotalDroppedBytes")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z ReplicationMRFStats) Msgsize() (s int) {
+ s = 1 + 16 + msgp.Uint64Size + 18 + msgp.Uint64Size + 18 + msgp.Uint64Size
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *SMA) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "CAvg":
+ z.CAvg, err = dc.ReadFloat64()
+ if err != nil {
+ err = msgp.WrapError(err, "CAvg")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z SMA) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 1
+ // write "CAvg"
+ err = en.Append(0x81, 0xa4, 0x43, 0x41, 0x76, 0x67)
+ if err != nil {
+ return
+ }
+ err = en.WriteFloat64(z.CAvg)
+ if err != nil {
+ err = msgp.WrapError(err, "CAvg")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z SMA) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 1
+ // string "CAvg"
+ o = append(o, 0x81, 0xa4, 0x43, 0x41, 0x76, 0x67)
+ o = msgp.AppendFloat64(o, z.CAvg)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *SMA) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "CAvg":
+ z.CAvg, bts, err = msgp.ReadFloat64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "CAvg")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z SMA) Msgsize() (s int) {
+ s = 1 + 5 + msgp.Float64Size
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *XferStats) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "cr":
+ z.Curr, err = dc.ReadFloat64()
+ if err != nil {
+ err = msgp.WrapError(err, "Curr")
+ return
+ }
+ case "av":
+ z.Avg, err = dc.ReadFloat64()
+ if err != nil {
+ err = msgp.WrapError(err, "Avg")
+ return
+ }
+ case "p":
+ z.Peak, err = dc.ReadFloat64()
+ if err != nil {
+ err = msgp.WrapError(err, "Peak")
+ return
+ }
+ case "n":
+ z.N, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "N")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *XferStats) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 4
+ // write "cr"
+ err = en.Append(0x84, 0xa2, 0x63, 0x72)
+ if err != nil {
+ return
+ }
+ err = en.WriteFloat64(z.Curr)
+ if err != nil {
+ err = msgp.WrapError(err, "Curr")
+ return
+ }
+ // write "av"
+ err = en.Append(0xa2, 0x61, 0x76)
+ if err != nil {
+ return
+ }
+ err = en.WriteFloat64(z.Avg)
+ if err != nil {
+ err = msgp.WrapError(err, "Avg")
+ return
+ }
+ // write "p"
+ err = en.Append(0xa1, 0x70)
+ if err != nil {
+ return
+ }
+ err = en.WriteFloat64(z.Peak)
+ if err != nil {
+ err = msgp.WrapError(err, "Peak")
+ return
+ }
+ // write "n"
+ err = en.Append(0xa1, 0x6e)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.N)
+ if err != nil {
+ err = msgp.WrapError(err, "N")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *XferStats) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 4
+ // string "cr"
+ o = append(o, 0x84, 0xa2, 0x63, 0x72)
+ o = msgp.AppendFloat64(o, z.Curr)
+ // string "av"
+ o = append(o, 0xa2, 0x61, 0x76)
+ o = msgp.AppendFloat64(o, z.Avg)
+ // string "p"
+ o = append(o, 0xa1, 0x70)
+ o = msgp.AppendFloat64(o, z.Peak)
+ // string "n"
+ o = append(o, 0xa1, 0x6e)
+ o = msgp.AppendInt64(o, z.N)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *XferStats) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "cr":
+ z.Curr, bts, err = msgp.ReadFloat64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Curr")
+ return
+ }
+ case "av":
+ z.Avg, bts, err = msgp.ReadFloat64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Avg")
+ return
+ }
+ case "p":
+ z.Peak, bts, err = msgp.ReadFloat64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Peak")
+ return
+ }
+ case "n":
+ z.N, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "N")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *XferStats) Msgsize() (s int) {
+ s = 1 + 3 + msgp.Float64Size + 3 + msgp.Float64Size + 2 + msgp.Float64Size + 2 + msgp.Int64Size
+ return
+}
diff --git a/cmd/bucket-replication-metrics_gen_test.go b/cmd/bucket-replication-metrics_gen_test.go
new file mode 100644
index 00000000..7342472f
--- /dev/null
+++ b/cmd/bucket-replication-metrics_gen_test.go
@@ -0,0 +1,914 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+func TestMarshalUnmarshalActiveWorkerStat(t *testing.T) {
+ v := ActiveWorkerStat{}
+ 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 BenchmarkMarshalMsgActiveWorkerStat(b *testing.B) {
+ v := ActiveWorkerStat{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgActiveWorkerStat(b *testing.B) {
+ v := ActiveWorkerStat{}
+ 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 BenchmarkUnmarshalActiveWorkerStat(b *testing.B) {
+ v := ActiveWorkerStat{}
+ 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 TestEncodeDecodeActiveWorkerStat(t *testing.T) {
+ v := ActiveWorkerStat{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeActiveWorkerStat Msgsize() is inaccurate")
+ }
+
+ vn := ActiveWorkerStat{}
+ 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 BenchmarkEncodeActiveWorkerStat(b *testing.B) {
+ v := ActiveWorkerStat{}
+ 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 BenchmarkDecodeActiveWorkerStat(b *testing.B) {
+ v := ActiveWorkerStat{}
+ 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 TestMarshalUnmarshalInQueueMetric(t *testing.T) {
+ v := InQueueMetric{}
+ 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 BenchmarkMarshalMsgInQueueMetric(b *testing.B) {
+ v := InQueueMetric{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgInQueueMetric(b *testing.B) {
+ v := InQueueMetric{}
+ 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 BenchmarkUnmarshalInQueueMetric(b *testing.B) {
+ v := InQueueMetric{}
+ 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 TestEncodeDecodeInQueueMetric(t *testing.T) {
+ v := InQueueMetric{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeInQueueMetric Msgsize() is inaccurate")
+ }
+
+ vn := InQueueMetric{}
+ 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 BenchmarkEncodeInQueueMetric(b *testing.B) {
+ v := InQueueMetric{}
+ 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 BenchmarkDecodeInQueueMetric(b *testing.B) {
+ v := InQueueMetric{}
+ 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 TestMarshalUnmarshalInQueueStats(t *testing.T) {
+ v := InQueueStats{}
+ 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 BenchmarkMarshalMsgInQueueStats(b *testing.B) {
+ v := InQueueStats{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgInQueueStats(b *testing.B) {
+ v := InQueueStats{}
+ 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 BenchmarkUnmarshalInQueueStats(b *testing.B) {
+ v := InQueueStats{}
+ 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 TestEncodeDecodeInQueueStats(t *testing.T) {
+ v := InQueueStats{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeInQueueStats Msgsize() is inaccurate")
+ }
+
+ vn := InQueueStats{}
+ 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 BenchmarkEncodeInQueueStats(b *testing.B) {
+ v := InQueueStats{}
+ 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 BenchmarkDecodeInQueueStats(b *testing.B) {
+ v := InQueueStats{}
+ 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 TestMarshalUnmarshalProxyMetric(t *testing.T) {
+ v := ProxyMetric{}
+ 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 BenchmarkMarshalMsgProxyMetric(b *testing.B) {
+ v := ProxyMetric{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgProxyMetric(b *testing.B) {
+ v := ProxyMetric{}
+ 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 BenchmarkUnmarshalProxyMetric(b *testing.B) {
+ v := ProxyMetric{}
+ 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 TestEncodeDecodeProxyMetric(t *testing.T) {
+ v := ProxyMetric{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeProxyMetric Msgsize() is inaccurate")
+ }
+
+ vn := ProxyMetric{}
+ 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 BenchmarkEncodeProxyMetric(b *testing.B) {
+ v := ProxyMetric{}
+ 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 BenchmarkDecodeProxyMetric(b *testing.B) {
+ v := ProxyMetric{}
+ 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 TestMarshalUnmarshalQStat(t *testing.T) {
+ v := QStat{}
+ 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 BenchmarkMarshalMsgQStat(b *testing.B) {
+ v := QStat{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgQStat(b *testing.B) {
+ v := QStat{}
+ 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 BenchmarkUnmarshalQStat(b *testing.B) {
+ v := QStat{}
+ 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 TestEncodeDecodeQStat(t *testing.T) {
+ v := QStat{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeQStat Msgsize() is inaccurate")
+ }
+
+ vn := QStat{}
+ 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 BenchmarkEncodeQStat(b *testing.B) {
+ v := QStat{}
+ 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 BenchmarkDecodeQStat(b *testing.B) {
+ v := QStat{}
+ 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 TestMarshalUnmarshalReplicationMRFStats(t *testing.T) {
+ v := ReplicationMRFStats{}
+ 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 BenchmarkMarshalMsgReplicationMRFStats(b *testing.B) {
+ v := ReplicationMRFStats{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgReplicationMRFStats(b *testing.B) {
+ v := ReplicationMRFStats{}
+ 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 BenchmarkUnmarshalReplicationMRFStats(b *testing.B) {
+ v := ReplicationMRFStats{}
+ 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 TestEncodeDecodeReplicationMRFStats(t *testing.T) {
+ v := ReplicationMRFStats{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeReplicationMRFStats Msgsize() is inaccurate")
+ }
+
+ vn := ReplicationMRFStats{}
+ 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 BenchmarkEncodeReplicationMRFStats(b *testing.B) {
+ v := ReplicationMRFStats{}
+ 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 BenchmarkDecodeReplicationMRFStats(b *testing.B) {
+ v := ReplicationMRFStats{}
+ 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 TestMarshalUnmarshalSMA(t *testing.T) {
+ v := SMA{}
+ 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 BenchmarkMarshalMsgSMA(b *testing.B) {
+ v := SMA{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgSMA(b *testing.B) {
+ v := SMA{}
+ 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 BenchmarkUnmarshalSMA(b *testing.B) {
+ v := SMA{}
+ 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 TestEncodeDecodeSMA(t *testing.T) {
+ v := SMA{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeSMA Msgsize() is inaccurate")
+ }
+
+ vn := SMA{}
+ 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 BenchmarkEncodeSMA(b *testing.B) {
+ v := SMA{}
+ 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 BenchmarkDecodeSMA(b *testing.B) {
+ v := SMA{}
+ 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 TestMarshalUnmarshalXferStats(t *testing.T) {
+ v := XferStats{}
+ 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 BenchmarkMarshalMsgXferStats(b *testing.B) {
+ v := XferStats{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgXferStats(b *testing.B) {
+ v := XferStats{}
+ 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 BenchmarkUnmarshalXferStats(b *testing.B) {
+ v := XferStats{}
+ 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 TestEncodeDecodeXferStats(t *testing.T) {
+ v := XferStats{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeXferStats Msgsize() is inaccurate")
+ }
+
+ vn := XferStats{}
+ 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 BenchmarkEncodeXferStats(b *testing.B) {
+ v := XferStats{}
+ 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 BenchmarkDecodeXferStats(b *testing.B) {
+ v := XferStats{}
+ 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)
+ }
+ }
+}
diff --git a/cmd/bucket-replication-stats.go b/cmd/bucket-replication-stats.go
new file mode 100644
index 00000000..29717928
--- /dev/null
+++ b/cmd/bucket-replication-stats.go
@@ -0,0 +1,516 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/minio/minio/internal/bucket/replication"
+ "github.com/rcrowley/go-metrics"
+)
+
+func (b *BucketReplicationStats) hasReplicationUsage() bool {
+ for _, s := range b.Stats {
+ if s.hasReplicationUsage() {
+ return true
+ }
+ }
+ return false
+}
+
+// ReplicationStats holds the global in-memory replication stats
+type ReplicationStats struct {
+ // map of site deployment ID to site replication status
+ // for site replication - maintain stats at global level
+ srStats *SRStats
+ // active worker stats
+ workers *ActiveWorkerStat
+ // queue stats cache
+ qCache queueCache
+
+ pCache proxyStatsCache
+ // mrf backlog stats
+ mrfStats ReplicationMRFStats
+ // for bucket replication, continue to use existing cache
+ Cache map[string]*BucketReplicationStats
+ mostRecentStats BucketStatsMap
+ registry metrics.Registry
+ sync.RWMutex // mutex for Cache
+ mostRecentStatsMu sync.Mutex // mutex for mostRecentStats
+
+ wlock sync.RWMutex // mutex for active workers
+
+ movingAvgTicker *time.Ticker // Ticker for calculating moving averages
+ wTimer *time.Ticker // ticker for calculating active workers
+ qTimer *time.Ticker // ticker for calculating queue stats
+}
+
+func (r *ReplicationStats) trackEWMA() {
+ for {
+ select {
+ case <-r.movingAvgTicker.C:
+ r.updateMovingAvg()
+ case <-GlobalContext.Done():
+ return
+ }
+ }
+}
+
+func (r *ReplicationStats) updateMovingAvg() {
+ r.RLock()
+ for _, s := range r.Cache {
+ for _, st := range s.Stats {
+ st.XferRateLrg.measure.updateExponentialMovingAverage(time.Now())
+ st.XferRateSml.measure.updateExponentialMovingAverage(time.Now())
+ }
+ }
+ r.RUnlock()
+}
+
+// ActiveWorkers returns worker stats
+func (r *ReplicationStats) ActiveWorkers() ActiveWorkerStat {
+ if r == nil {
+ return ActiveWorkerStat{}
+ }
+ r.wlock.RLock()
+ defer r.wlock.RUnlock()
+ w := r.workers.get()
+ return ActiveWorkerStat{
+ Curr: w.Curr,
+ Max: w.Max,
+ Avg: w.Avg,
+ }
+}
+
+func (r *ReplicationStats) collectWorkerMetrics(ctx context.Context) {
+ if r == nil {
+ return
+ }
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-r.wTimer.C:
+ r.wlock.Lock()
+ r.workers.update()
+ r.wlock.Unlock()
+ }
+ }
+}
+
+func (r *ReplicationStats) collectQueueMetrics(ctx context.Context) {
+ if r == nil {
+ return
+ }
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-r.qTimer.C:
+ r.qCache.update()
+ }
+ }
+}
+
+// Delete deletes in-memory replication statistics for a bucket.
+func (r *ReplicationStats) Delete(bucket string) {
+ if r == nil {
+ return
+ }
+
+ r.Lock()
+ defer r.Unlock()
+ delete(r.Cache, bucket)
+}
+
+// UpdateReplicaStat updates in-memory replica statistics with new values.
+func (r *ReplicationStats) UpdateReplicaStat(bucket string, n int64) {
+ if r == nil {
+ return
+ }
+
+ r.Lock()
+ defer r.Unlock()
+ bs, ok := r.Cache[bucket]
+ if !ok {
+ bs = newBucketReplicationStats()
+ }
+ bs.ReplicaSize += n
+ bs.ReplicaCount++
+ r.Cache[bucket] = bs
+ r.srUpdateReplicaStat(n)
+}
+
+func (r *ReplicationStats) srUpdateReplicaStat(sz int64) {
+ if r == nil {
+ return
+ }
+ atomic.AddInt64(&r.srStats.ReplicaSize, sz)
+ atomic.AddInt64(&r.srStats.ReplicaCount, 1)
+}
+
+func (r *ReplicationStats) srUpdate(sr replStat) {
+ dID, err := globalSiteReplicationSys.getDeplIDForEndpoint(sr.endpoint())
+ if err == nil {
+ r.srStats.update(sr, dID)
+ }
+}
+
+// Update updates in-memory replication statistics with new values.
+func (r *ReplicationStats) Update(bucket string, ri replicatedTargetInfo, status, prevStatus replication.StatusType) {
+ if r == nil {
+ return
+ }
+ var rs replStat
+ switch status {
+ case replication.Pending:
+ if ri.OpType.IsDataReplication() && prevStatus != status {
+ rs.set(ri.Arn, ri.Size, 0, status, ri.OpType, ri.endpoint, ri.secure, ri.Err)
+ }
+ case replication.Completed:
+ if ri.OpType.IsDataReplication() {
+ rs.set(ri.Arn, ri.Size, ri.Duration, status, ri.OpType, ri.endpoint, ri.secure, ri.Err)
+ }
+ case replication.Failed:
+ if ri.OpType.IsDataReplication() && prevStatus == replication.Pending {
+ rs.set(ri.Arn, ri.Size, ri.Duration, status, ri.OpType, ri.endpoint, ri.secure, ri.Err)
+ }
+ case replication.Replica:
+ if ri.OpType == replication.ObjectReplicationType {
+ rs.set(ri.Arn, ri.Size, 0, status, ri.OpType, "", false, ri.Err)
+ }
+ }
+
+ // update site-replication in-memory stats
+ if rs.Completed || rs.Failed {
+ r.srUpdate(rs)
+ }
+
+ r.Lock()
+ defer r.Unlock()
+
+ // update bucket replication in-memory stats
+ bs, ok := r.Cache[bucket]
+ if !ok {
+ bs = newBucketReplicationStats()
+ r.Cache[bucket] = bs
+ }
+ b, ok := bs.Stats[ri.Arn]
+ if !ok {
+ b = &BucketReplicationStat{
+ XferRateLrg: newXferStats(),
+ XferRateSml: newXferStats(),
+ }
+ bs.Stats[ri.Arn] = b
+ }
+
+ switch {
+ case rs.Completed:
+ b.ReplicatedSize += rs.TransferSize
+ b.ReplicatedCount++
+ if rs.TransferDuration > 0 {
+ b.Latency.update(rs.TransferSize, rs.TransferDuration)
+ b.updateXferRate(rs.TransferSize, rs.TransferDuration)
+ }
+ case rs.Failed:
+ b.FailStats.addsize(rs.TransferSize, rs.Err)
+ case rs.Pending:
+ }
+}
+
+type replStat struct {
+ Arn string
+ Completed bool
+ Pending bool
+ Failed bool
+ opType replication.Type
+ // transfer size
+ TransferSize int64
+ // transfer duration
+ TransferDuration time.Duration
+ Endpoint string
+ Secure bool
+ Err error
+}
+
+func (rs *replStat) endpoint() string {
+ scheme := "http"
+ if rs.Secure {
+ scheme = "https"
+ }
+ return scheme + "://" + rs.Endpoint
+}
+
+func (rs *replStat) set(arn string, n int64, duration time.Duration, status replication.StatusType, opType replication.Type, endpoint string, secure bool, err error) {
+ rs.Endpoint = endpoint
+ rs.Secure = secure
+ rs.TransferSize = n
+ rs.Arn = arn
+ rs.TransferDuration = duration
+ rs.opType = opType
+ switch status {
+ case replication.Completed:
+ rs.Completed = true
+ case replication.Pending:
+ rs.Pending = true
+ case replication.Failed:
+ rs.Failed = true
+ rs.Err = err
+ }
+}
+
+// GetAll returns replication metrics for all buckets at once.
+func (r *ReplicationStats) GetAll() map[string]BucketReplicationStats {
+ if r == nil {
+ return map[string]BucketReplicationStats{}
+ }
+
+ r.RLock()
+
+ bucketReplicationStats := make(map[string]BucketReplicationStats, len(r.Cache))
+ for k, v := range r.Cache {
+ bucketReplicationStats[k] = v.Clone()
+ }
+ r.RUnlock()
+ for k, v := range bucketReplicationStats {
+ v.QStat = r.qCache.getBucketStats(k)
+ bucketReplicationStats[k] = v
+ }
+
+ return bucketReplicationStats
+}
+
+func (r *ReplicationStats) getSRMetricsForNode() SRMetricsSummary {
+ if r == nil {
+ return SRMetricsSummary{}
+ }
+
+ m := SRMetricsSummary{
+ Uptime: UTCNow().Unix() - globalBootTime.Unix(),
+ Queued: r.qCache.getSiteStats(),
+ ActiveWorkers: r.ActiveWorkers(),
+ Metrics: r.srStats.get(),
+ Proxied: r.pCache.getSiteStats(),
+ ReplicaSize: atomic.LoadInt64(&r.srStats.ReplicaSize),
+ ReplicaCount: atomic.LoadInt64(&r.srStats.ReplicaCount),
+ }
+ return m
+}
+
+// Get replication metrics for a bucket from this node since this node came up.
+func (r *ReplicationStats) Get(bucket string) BucketReplicationStats {
+ if r == nil {
+ return BucketReplicationStats{Stats: make(map[string]*BucketReplicationStat)}
+ }
+
+ r.RLock()
+ defer r.RUnlock()
+
+ st, ok := r.Cache[bucket]
+ if !ok {
+ return BucketReplicationStats{Stats: make(map[string]*BucketReplicationStat)}
+ }
+ return st.Clone()
+}
+
+// NewReplicationStats initialize in-memory replication statistics
+func NewReplicationStats(ctx context.Context, objectAPI ObjectLayer) *ReplicationStats {
+ r := metrics.NewRegistry()
+ rs := ReplicationStats{
+ Cache: make(map[string]*BucketReplicationStats),
+ qCache: newQueueCache(r),
+ pCache: newProxyStatsCache(),
+ srStats: newSRStats(),
+ movingAvgTicker: time.NewTicker(2 * time.Second),
+ wTimer: time.NewTicker(2 * time.Second),
+ qTimer: time.NewTicker(2 * time.Second),
+
+ workers: newActiveWorkerStat(r),
+ registry: r,
+ }
+ go rs.collectWorkerMetrics(ctx)
+ go rs.collectQueueMetrics(ctx)
+ return &rs
+}
+
+func (r *ReplicationStats) getAllLatest(bucketsUsage map[string]BucketUsageInfo) (bucketsReplicationStats map[string]BucketStats) {
+ if r == nil {
+ return nil
+ }
+ peerBucketStatsList := globalNotificationSys.GetClusterAllBucketStats(GlobalContext)
+ bucketsReplicationStats = make(map[string]BucketStats, len(bucketsUsage))
+
+ for bucket := range bucketsUsage {
+ bucketStats := make([]BucketStats, len(peerBucketStatsList))
+ for i, peerBucketStats := range peerBucketStatsList {
+ bucketStat, ok := peerBucketStats.Stats[bucket]
+ if !ok {
+ continue
+ }
+ bucketStats[i] = bucketStat
+ }
+ bucketsReplicationStats[bucket] = r.calculateBucketReplicationStats(bucket, bucketStats)
+ }
+ return bucketsReplicationStats
+}
+
+func (r *ReplicationStats) calculateBucketReplicationStats(bucket string, bucketStats []BucketStats) (bs BucketStats) {
+ if r == nil {
+ bs = BucketStats{
+ ReplicationStats: BucketReplicationStats{
+ Stats: make(map[string]*BucketReplicationStat),
+ },
+ QueueStats: ReplicationQueueStats{},
+ ProxyStats: ProxyMetric{},
+ }
+ return bs
+ }
+ var s BucketReplicationStats
+ // accumulate cluster bucket stats
+ stats := make(map[string]*BucketReplicationStat)
+ var (
+ totReplicaSize, totReplicatedSize int64
+ totReplicaCount, totReplicatedCount int64
+ totFailed RTimedMetrics
+ tq InQueueMetric
+ )
+ for _, bucketStat := range bucketStats {
+ totReplicaSize += bucketStat.ReplicationStats.ReplicaSize
+ totReplicaCount += bucketStat.ReplicationStats.ReplicaCount
+ for _, q := range bucketStat.QueueStats.Nodes {
+ tq = tq.merge(q.QStats)
+ }
+
+ for arn, stat := range bucketStat.ReplicationStats.Stats {
+ oldst := stats[arn]
+ if oldst == nil {
+ oldst = &BucketReplicationStat{
+ XferRateLrg: newXferStats(),
+ XferRateSml: newXferStats(),
+ }
+ }
+ fstats := stat.FailStats.merge(oldst.FailStats)
+ lrg := oldst.XferRateLrg.merge(*stat.XferRateLrg)
+ sml := oldst.XferRateSml.merge(*stat.XferRateSml)
+ stats[arn] = &BucketReplicationStat{
+ Failed: fstats.toMetric(),
+ FailStats: fstats,
+ ReplicatedSize: stat.ReplicatedSize + oldst.ReplicatedSize,
+ ReplicatedCount: stat.ReplicatedCount + oldst.ReplicatedCount,
+ Latency: stat.Latency.merge(oldst.Latency),
+ XferRateLrg: &lrg,
+ XferRateSml: &sml,
+ }
+ totReplicatedSize += stat.ReplicatedSize
+ totReplicatedCount += stat.ReplicatedCount
+ totFailed = totFailed.merge(stat.FailStats)
+ }
+ }
+
+ s = BucketReplicationStats{
+ Stats: stats,
+ QStat: tq,
+ ReplicaSize: totReplicaSize,
+ ReplicaCount: totReplicaCount,
+ ReplicatedSize: totReplicatedSize,
+ ReplicatedCount: totReplicatedCount,
+ Failed: totFailed.toMetric(),
+ }
+
+ var qs ReplicationQueueStats
+ for _, bs := range bucketStats {
+ qs.Nodes = append(qs.Nodes, bs.QueueStats.Nodes...)
+ }
+ qs.Uptime = UTCNow().Unix() - globalBootTime.Unix()
+
+ var ps ProxyMetric
+ for _, bs := range bucketStats {
+ ps.add(bs.ProxyStats)
+ }
+ bs = BucketStats{
+ ReplicationStats: s,
+ QueueStats: qs,
+ ProxyStats: ps,
+ }
+ r.mostRecentStatsMu.Lock()
+ if len(r.mostRecentStats.Stats) == 0 {
+ r.mostRecentStats = BucketStatsMap{Stats: make(map[string]BucketStats, 1), Timestamp: UTCNow()}
+ }
+ if len(bs.ReplicationStats.Stats) > 0 {
+ r.mostRecentStats.Stats[bucket] = bs
+ }
+ r.mostRecentStats.Timestamp = UTCNow()
+ r.mostRecentStatsMu.Unlock()
+ return bs
+}
+
+// get the most current of in-memory replication stats and data usage info from crawler.
+func (r *ReplicationStats) getLatestReplicationStats(bucket string) (s BucketStats) {
+ if r == nil {
+ return s
+ }
+ bucketStats := globalNotificationSys.GetClusterBucketStats(GlobalContext, bucket)
+ return r.calculateBucketReplicationStats(bucket, bucketStats)
+}
+
+func (r *ReplicationStats) incQ(bucket string, sz int64, isDeleteRepl bool, opType replication.Type) {
+ r.qCache.Lock()
+ defer r.qCache.Unlock()
+ v, ok := r.qCache.bucketStats[bucket]
+ if !ok {
+ v = newInQueueStats(r.registry, bucket)
+ }
+ atomic.AddInt64(&v.nowBytes, sz)
+ atomic.AddInt64(&v.nowCount, 1)
+ r.qCache.bucketStats[bucket] = v
+ atomic.AddInt64(&r.qCache.srQueueStats.nowBytes, sz)
+ atomic.AddInt64(&r.qCache.srQueueStats.nowCount, 1)
+}
+
+func (r *ReplicationStats) decQ(bucket string, sz int64, isDelMarker bool, opType replication.Type) {
+ r.qCache.Lock()
+ defer r.qCache.Unlock()
+ v, ok := r.qCache.bucketStats[bucket]
+ if !ok {
+ v = newInQueueStats(r.registry, bucket)
+ }
+ atomic.AddInt64(&v.nowBytes, -1*sz)
+ atomic.AddInt64(&v.nowCount, -1)
+ r.qCache.bucketStats[bucket] = v
+
+ atomic.AddInt64(&r.qCache.srQueueStats.nowBytes, -1*sz)
+ atomic.AddInt64(&r.qCache.srQueueStats.nowCount, -1)
+}
+
+// incProxy increments proxy metrics for proxied calls
+func (r *ReplicationStats) incProxy(bucket string, api replProxyAPI, isErr bool) {
+ if r != nil {
+ r.pCache.inc(bucket, api, isErr)
+ }
+}
+
+func (r *ReplicationStats) getProxyStats(bucket string) ProxyMetric {
+ if r == nil {
+ return ProxyMetric{}
+ }
+ return r.pCache.getBucketStats(bucket)
+}
diff --git a/cmd/bucket-replication-utils.go b/cmd/bucket-replication-utils.go
new file mode 100644
index 00000000..41b5dbc6
--- /dev/null
+++ b/cmd/bucket-replication-utils.go
@@ -0,0 +1,811 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "maps"
+ "net/http"
+ "net/url"
+ "regexp"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/bucket/replication"
+ "github.com/minio/minio/internal/crypto"
+ xhttp "github.com/minio/minio/internal/http"
+)
+
+//go:generate msgp -file=$GOFILE
+
+// replicatedTargetInfo struct represents replication info on a target
+type replicatedTargetInfo struct {
+ Arn string
+ Size int64
+ Duration time.Duration
+ ReplicationAction replicationAction // full or metadata only
+ OpType replication.Type // whether incoming replication, existing object, healing etc..
+ ReplicationStatus replication.StatusType
+ PrevReplicationStatus replication.StatusType
+ VersionPurgeStatus VersionPurgeStatusType
+ ResyncTimestamp string
+ ReplicationResynced bool // true only if resync attempted for this target
+ endpoint string
+ secure bool
+ Err error // replication error if any
+}
+
+// Empty returns true for a target if arn is empty
+func (rt replicatedTargetInfo) Empty() bool {
+ return rt.Arn == ""
+}
+
+type replicatedInfos struct {
+ ReplicationTimeStamp time.Time
+ Targets []replicatedTargetInfo
+}
+
+func (ri replicatedInfos) CompletedSize() (sz int64) {
+ for _, t := range ri.Targets {
+ if t.Empty() {
+ continue
+ }
+ if t.ReplicationStatus == replication.Completed && t.PrevReplicationStatus != replication.Completed {
+ sz += t.Size
+ }
+ }
+ return sz
+}
+
+// ReplicationAttempted returns true if replication was attempted on any of the targets for the object version
+// queued
+func (ri replicatedInfos) ReplicationResynced() bool {
+ for _, t := range ri.Targets {
+ if t.Empty() || !t.ReplicationResynced {
+ continue
+ }
+ return true
+ }
+ return false
+}
+
+func (ri replicatedInfos) ReplicationStatusInternal() string {
+ b := new(bytes.Buffer)
+ for _, t := range ri.Targets {
+ if t.Empty() {
+ continue
+ }
+ fmt.Fprintf(b, "%s=%s;", t.Arn, t.ReplicationStatus.String())
+ }
+ return b.String()
+}
+
+func (ri replicatedInfos) ReplicationStatus() replication.StatusType {
+ if len(ri.Targets) == 0 {
+ return replication.StatusType("")
+ }
+ completed := 0
+ for _, v := range ri.Targets {
+ switch v.ReplicationStatus {
+ case replication.Failed:
+ return replication.Failed
+ case replication.Completed:
+ completed++
+ }
+ }
+ if completed == len(ri.Targets) {
+ return replication.Completed
+ }
+ return replication.Pending
+}
+
+func (ri replicatedInfos) VersionPurgeStatus() VersionPurgeStatusType {
+ if len(ri.Targets) == 0 {
+ return VersionPurgeStatusType("")
+ }
+ completed := 0
+ for _, v := range ri.Targets {
+ switch v.VersionPurgeStatus {
+ case replication.VersionPurgeFailed:
+ return replication.VersionPurgeFailed
+ case replication.VersionPurgeComplete:
+ completed++
+ }
+ }
+ if completed == len(ri.Targets) {
+ return replication.VersionPurgeComplete
+ }
+ return replication.VersionPurgePending
+}
+
+func (ri replicatedInfos) VersionPurgeStatusInternal() string {
+ b := new(bytes.Buffer)
+ for _, t := range ri.Targets {
+ if t.Empty() {
+ continue
+ }
+ if t.VersionPurgeStatus.Empty() {
+ continue
+ }
+ fmt.Fprintf(b, "%s=%s;", t.Arn, t.VersionPurgeStatus)
+ }
+ return b.String()
+}
+
+func (ri replicatedInfos) Action() replicationAction {
+ for _, t := range ri.Targets {
+ if t.Empty() {
+ continue
+ }
+ // rely on replication action from target that actually performed replication now.
+ if t.PrevReplicationStatus != replication.Completed {
+ return t.ReplicationAction
+ }
+ }
+ return replicateNone
+}
+
+var replStatusRegex = regexp.MustCompile(`([^=].*?)=([^,].*?);`)
+
+// TargetReplicationStatus - returns replication status of a target
+func (ri ReplicateObjectInfo) TargetReplicationStatus(arn string) (status replication.StatusType) {
+ repStatMatches := replStatusRegex.FindAllStringSubmatch(ri.ReplicationStatusInternal, -1)
+ for _, repStatMatch := range repStatMatches {
+ if len(repStatMatch) != 3 {
+ return status
+ }
+ if repStatMatch[1] == arn {
+ return replication.StatusType(repStatMatch[2])
+ }
+ }
+ return status
+}
+
+// TargetReplicationStatus - returns replication status of a target
+func (o ObjectInfo) TargetReplicationStatus(arn string) (status replication.StatusType) {
+ repStatMatches := replStatusRegex.FindAllStringSubmatch(o.ReplicationStatusInternal, -1)
+ for _, repStatMatch := range repStatMatches {
+ if len(repStatMatch) != 3 {
+ return status
+ }
+ if repStatMatch[1] == arn {
+ return replication.StatusType(repStatMatch[2])
+ }
+ }
+ return status
+}
+
+type replicateTargetDecision struct {
+ Replicate bool // Replicate to this target
+ Synchronous bool // Synchronous replication configured.
+ Arn string // ARN of replication target
+ ID string
+}
+
+func (t *replicateTargetDecision) String() string {
+ return fmt.Sprintf("%t;%t;%s;%s", t.Replicate, t.Synchronous, t.Arn, t.ID)
+}
+
+func newReplicateTargetDecision(arn string, replicate bool, sync bool) replicateTargetDecision {
+ d := replicateTargetDecision{
+ Replicate: replicate,
+ Synchronous: sync,
+ Arn: arn,
+ }
+ return d
+}
+
+// ReplicateDecision represents replication decision for each target
+type ReplicateDecision struct {
+ targetsMap map[string]replicateTargetDecision
+}
+
+// ReplicateAny returns true if at least one target qualifies for replication
+func (d ReplicateDecision) ReplicateAny() bool {
+ for _, t := range d.targetsMap {
+ if t.Replicate {
+ return true
+ }
+ }
+ return false
+}
+
+// Synchronous returns true if at least one target qualifies for synchronous replication
+func (d ReplicateDecision) Synchronous() bool {
+ for _, t := range d.targetsMap {
+ if t.Synchronous {
+ return true
+ }
+ }
+ return false
+}
+
+func (d ReplicateDecision) String() string {
+ b := new(bytes.Buffer)
+ for key, value := range d.targetsMap {
+ fmt.Fprintf(b, "%s=%s,", key, value.String())
+ }
+ return strings.TrimSuffix(b.String(), ",")
+}
+
+// Set updates ReplicateDecision with target's replication decision
+func (d *ReplicateDecision) Set(t replicateTargetDecision) {
+ if d.targetsMap == nil {
+ d.targetsMap = make(map[string]replicateTargetDecision)
+ }
+ d.targetsMap[t.Arn] = t
+}
+
+// PendingStatus returns a stringified representation of internal replication status with all targets marked as `PENDING`
+func (d ReplicateDecision) PendingStatus() string {
+ b := new(bytes.Buffer)
+ for _, k := range d.targetsMap {
+ if k.Replicate {
+ fmt.Fprintf(b, "%s=%s;", k.Arn, replication.Pending.String())
+ }
+ }
+ return b.String()
+}
+
+// ResyncDecision is a struct representing a map with target's individual resync decisions
+type ResyncDecision struct {
+ targets map[string]ResyncTargetDecision
+}
+
+// Empty returns true if no targets with resync decision present
+func (r ResyncDecision) Empty() bool {
+ return r.targets == nil
+}
+
+func (r ResyncDecision) mustResync() bool {
+ for _, v := range r.targets {
+ if v.Replicate {
+ return true
+ }
+ }
+ return false
+}
+
+func (r ResyncDecision) mustResyncTarget(tgtArn string) bool {
+ if r.targets == nil {
+ return false
+ }
+ v, ok := r.targets[tgtArn]
+ return ok && v.Replicate
+}
+
+// ResyncTargetDecision is struct that represents resync decision for this target
+type ResyncTargetDecision struct {
+ Replicate bool
+ ResetID string
+ ResetBeforeDate time.Time
+}
+
+var errInvalidReplicateDecisionFormat = fmt.Errorf("ReplicateDecision has invalid format")
+
+// parse k-v pairs of target ARN to stringified ReplicateTargetDecision delimited by ',' into a
+// ReplicateDecision struct
+func parseReplicateDecision(ctx context.Context, bucket, s string) (r ReplicateDecision, err error) {
+ r = ReplicateDecision{
+ targetsMap: make(map[string]replicateTargetDecision),
+ }
+ if len(s) == 0 {
+ return r, err
+ }
+ for p := range strings.SplitSeq(s, ",") {
+ if p == "" {
+ continue
+ }
+ slc := strings.Split(p, "=")
+ if len(slc) != 2 {
+ return r, errInvalidReplicateDecisionFormat
+ }
+ tgtStr := strings.TrimSuffix(strings.TrimPrefix(slc[1], `"`), `"`)
+ tgt := strings.Split(tgtStr, ";")
+ if len(tgt) != 4 {
+ return r, errInvalidReplicateDecisionFormat
+ }
+ r.targetsMap[slc[0]] = replicateTargetDecision{Replicate: tgt[0] == "true", Synchronous: tgt[1] == "true", Arn: tgt[2], ID: tgt[3]}
+ }
+ return r, err
+}
+
+// ReplicationState represents internal replication state
+type ReplicationState struct {
+ ReplicaTimeStamp time.Time // timestamp when last replica update was received
+ ReplicaStatus replication.StatusType // replica statusstringis
+ DeleteMarker bool // represents DeleteMarker replication state
+ ReplicationTimeStamp time.Time // timestamp when last replication activity happened
+ ReplicationStatusInternal string // stringified representation of all replication activity
+ // VersionPurgeStatusInternal is internally in the format "arn1=PENDING;arn2=COMPLETED;"
+ VersionPurgeStatusInternal string // stringified representation of all version purge statuses
+ ReplicateDecisionStr string // stringified representation of replication decision for each target
+ Targets map[string]replication.StatusType // map of ARN->replication status for ongoing replication activity
+ PurgeTargets map[string]VersionPurgeStatusType // map of ARN->VersionPurgeStatus for all the targets
+ ResetStatusesMap map[string]string // map of ARN-> stringified reset id and timestamp for all the targets
+}
+
+// Equal returns true if replication state is identical for version purge statuses and (replica)tion statuses.
+func (rs *ReplicationState) Equal(o ReplicationState) bool {
+ return rs.ReplicaStatus == o.ReplicaStatus &&
+ rs.ReplicationStatusInternal == o.ReplicationStatusInternal &&
+ rs.VersionPurgeStatusInternal == o.VersionPurgeStatusInternal
+}
+
+// CompositeReplicationStatus returns overall replication status for the object version being replicated.
+func (rs *ReplicationState) CompositeReplicationStatus() (st replication.StatusType) {
+ switch {
+ case rs.ReplicationStatusInternal != "":
+ switch replication.StatusType(rs.ReplicationStatusInternal) {
+ case replication.Pending, replication.Completed, replication.Failed, replication.Replica: // for backward compatibility
+ return replication.StatusType(rs.ReplicationStatusInternal)
+ default:
+ replStatus := getCompositeReplicationStatus(rs.Targets)
+ // return REPLICA status if replica received timestamp is later than replication timestamp
+ // provided object replication completed for all targets.
+ if rs.ReplicaTimeStamp.Equal(timeSentinel) || rs.ReplicaTimeStamp.IsZero() {
+ return replStatus
+ }
+ if replStatus == replication.Completed && rs.ReplicaTimeStamp.After(rs.ReplicationTimeStamp) {
+ return rs.ReplicaStatus
+ }
+ return replStatus
+ }
+ case !rs.ReplicaStatus.Empty():
+ return rs.ReplicaStatus
+ default:
+ return st
+ }
+}
+
+// CompositeVersionPurgeStatus returns overall replication purge status for the permanent delete being replicated.
+func (rs *ReplicationState) CompositeVersionPurgeStatus() VersionPurgeStatusType {
+ switch VersionPurgeStatusType(rs.VersionPurgeStatusInternal) {
+ case replication.VersionPurgePending, replication.VersionPurgeComplete, replication.VersionPurgeFailed: // for backward compatibility
+ return VersionPurgeStatusType(rs.VersionPurgeStatusInternal)
+ default:
+ return getCompositeVersionPurgeStatus(rs.PurgeTargets)
+ }
+}
+
+// TargetState returns replicatedInfos struct initialized with the previous state of replication
+func (rs *ReplicationState) targetState(arn string) (r replicatedTargetInfo) {
+ return replicatedTargetInfo{
+ Arn: arn,
+ PrevReplicationStatus: rs.Targets[arn],
+ VersionPurgeStatus: rs.PurgeTargets[arn],
+ ResyncTimestamp: rs.ResetStatusesMap[arn],
+ }
+}
+
+// getReplicationState returns replication state using target replicated info for the targets
+func getReplicationState(rinfos replicatedInfos, prevState ReplicationState, vID string) ReplicationState {
+ rs := ReplicationState{
+ ReplicateDecisionStr: prevState.ReplicateDecisionStr,
+ ResetStatusesMap: prevState.ResetStatusesMap,
+ ReplicaTimeStamp: prevState.ReplicaTimeStamp,
+ ReplicaStatus: prevState.ReplicaStatus,
+ }
+ var replStatuses, vpurgeStatuses string
+ replStatuses = rinfos.ReplicationStatusInternal()
+ rs.Targets = replicationStatusesMap(replStatuses)
+ rs.ReplicationStatusInternal = replStatuses
+ rs.ReplicationTimeStamp = rinfos.ReplicationTimeStamp
+
+ vpurgeStatuses = rinfos.VersionPurgeStatusInternal()
+ rs.VersionPurgeStatusInternal = vpurgeStatuses
+ rs.PurgeTargets = versionPurgeStatusesMap(vpurgeStatuses)
+
+ for _, rinfo := range rinfos.Targets {
+ if rinfo.ResyncTimestamp != "" {
+ rs.ResetStatusesMap[targetResetHeader(rinfo.Arn)] = rinfo.ResyncTimestamp
+ }
+ }
+ return rs
+}
+
+// constructs a replication status map from string representation
+func replicationStatusesMap(s string) map[string]replication.StatusType {
+ targets := make(map[string]replication.StatusType)
+ repStatMatches := replStatusRegex.FindAllStringSubmatch(s, -1)
+ for _, repStatMatch := range repStatMatches {
+ if len(repStatMatch) != 3 {
+ continue
+ }
+ status := replication.StatusType(repStatMatch[2])
+ targets[repStatMatch[1]] = status
+ }
+ return targets
+}
+
+// constructs a version purge status map from string representation
+func versionPurgeStatusesMap(s string) map[string]VersionPurgeStatusType {
+ targets := make(map[string]VersionPurgeStatusType)
+ purgeStatusMatches := replStatusRegex.FindAllStringSubmatch(s, -1)
+ for _, purgeStatusMatch := range purgeStatusMatches {
+ if len(purgeStatusMatch) != 3 {
+ continue
+ }
+ targets[purgeStatusMatch[1]] = VersionPurgeStatusType(purgeStatusMatch[2])
+ }
+ return targets
+}
+
+// return the overall replication status for all the targets
+func getCompositeReplicationStatus(m map[string]replication.StatusType) replication.StatusType {
+ if len(m) == 0 {
+ return replication.StatusType("")
+ }
+ completed := 0
+ for _, v := range m {
+ switch v {
+ case replication.Failed:
+ return replication.Failed
+ case replication.Completed:
+ completed++
+ }
+ }
+ if completed == len(m) {
+ return replication.Completed
+ }
+ return replication.Pending
+}
+
+// return the overall version purge status for all the targets
+func getCompositeVersionPurgeStatus(m map[string]VersionPurgeStatusType) VersionPurgeStatusType {
+ if len(m) == 0 {
+ return VersionPurgeStatusType("")
+ }
+ completed := 0
+ for _, v := range m {
+ switch v {
+ case replication.VersionPurgeFailed:
+ return replication.VersionPurgeFailed
+ case replication.VersionPurgeComplete:
+ completed++
+ }
+ }
+ if completed == len(m) {
+ return replication.VersionPurgeComplete
+ }
+ return replication.VersionPurgePending
+}
+
+// getHealReplicateObjectInfo returns info needed by heal replication in ReplicateObjectInfo
+func getHealReplicateObjectInfo(oi ObjectInfo, rcfg replicationConfig) ReplicateObjectInfo {
+ userDefined := cloneMSS(oi.UserDefined)
+ if rcfg.Config != nil && rcfg.Config.RoleArn != "" {
+ // For backward compatibility of objects pending/failed replication.
+ // Save replication related statuses in the new internal representation for
+ // compatible behavior.
+ if !oi.ReplicationStatus.Empty() {
+ oi.ReplicationStatusInternal = fmt.Sprintf("%s=%s;", rcfg.Config.RoleArn, oi.ReplicationStatus)
+ }
+ if !oi.VersionPurgeStatus.Empty() {
+ oi.VersionPurgeStatusInternal = fmt.Sprintf("%s=%s;", rcfg.Config.RoleArn, oi.VersionPurgeStatus)
+ }
+ for k, v := range userDefined {
+ if strings.EqualFold(k, ReservedMetadataPrefixLower+ReplicationReset) {
+ delete(userDefined, k)
+ userDefined[targetResetHeader(rcfg.Config.RoleArn)] = v
+ }
+ }
+ }
+
+ var dsc ReplicateDecision
+ if oi.DeleteMarker || !oi.VersionPurgeStatus.Empty() {
+ dsc = checkReplicateDelete(GlobalContext, oi.Bucket, ObjectToDelete{
+ ObjectV: ObjectV{
+ ObjectName: oi.Name,
+ VersionID: oi.VersionID,
+ },
+ }, oi, ObjectOptions{
+ Versioned: globalBucketVersioningSys.PrefixEnabled(oi.Bucket, oi.Name),
+ VersionSuspended: globalBucketVersioningSys.PrefixSuspended(oi.Bucket, oi.Name),
+ }, nil)
+ } else {
+ dsc = mustReplicate(GlobalContext, oi.Bucket, oi.Name, getMustReplicateOptions(userDefined, oi.UserTags, "", replication.HealReplicationType, ObjectOptions{}))
+ }
+
+ tgtStatuses := replicationStatusesMap(oi.ReplicationStatusInternal)
+ purgeStatuses := versionPurgeStatusesMap(oi.VersionPurgeStatusInternal)
+ existingObjResync := rcfg.Resync(GlobalContext, oi, dsc, tgtStatuses)
+ tm, _ := time.Parse(time.RFC3339Nano, userDefined[ReservedMetadataPrefixLower+ReplicationTimestamp])
+ rstate := oi.ReplicationState()
+ rstate.ReplicateDecisionStr = dsc.String()
+ asz, _ := oi.GetActualSize()
+
+ r := ReplicateObjectInfo{
+ Name: oi.Name,
+ Size: oi.Size,
+ ActualSize: asz,
+ Bucket: oi.Bucket,
+ VersionID: oi.VersionID,
+ ETag: oi.ETag,
+ ModTime: oi.ModTime,
+ ReplicationStatus: oi.ReplicationStatus,
+ ReplicationStatusInternal: oi.ReplicationStatusInternal,
+ DeleteMarker: oi.DeleteMarker,
+ VersionPurgeStatusInternal: oi.VersionPurgeStatusInternal,
+ VersionPurgeStatus: oi.VersionPurgeStatus,
+
+ ReplicationState: rstate,
+ OpType: replication.HealReplicationType,
+ Dsc: dsc,
+ ExistingObjResync: existingObjResync,
+ TargetStatuses: tgtStatuses,
+ TargetPurgeStatuses: purgeStatuses,
+ ReplicationTimestamp: tm,
+ SSEC: crypto.SSEC.IsEncrypted(oi.UserDefined),
+ UserTags: oi.UserTags,
+ }
+ if r.SSEC {
+ r.Checksum = oi.Checksum
+ }
+ return r
+}
+
+// ReplicationState - returns replication state using other internal replication metadata in ObjectInfo
+func (o ObjectInfo) ReplicationState() ReplicationState {
+ rs := ReplicationState{
+ ReplicationStatusInternal: o.ReplicationStatusInternal,
+ VersionPurgeStatusInternal: o.VersionPurgeStatusInternal,
+ ReplicateDecisionStr: o.replicationDecision,
+ Targets: make(map[string]replication.StatusType),
+ PurgeTargets: make(map[string]VersionPurgeStatusType),
+ ResetStatusesMap: make(map[string]string),
+ }
+ rs.Targets = replicationStatusesMap(o.ReplicationStatusInternal)
+ rs.PurgeTargets = versionPurgeStatusesMap(o.VersionPurgeStatusInternal)
+ for k, v := range o.UserDefined {
+ if strings.HasPrefix(k, ReservedMetadataPrefixLower+ReplicationReset) {
+ arn := strings.TrimPrefix(k, fmt.Sprintf("%s-", ReservedMetadataPrefixLower+ReplicationReset))
+ rs.ResetStatusesMap[arn] = v
+ }
+ }
+ return rs
+}
+
+// ReplicationState returns replication state using other internal replication metadata in ObjectToDelete
+func (o ObjectToDelete) ReplicationState() ReplicationState {
+ r := ReplicationState{
+ ReplicationStatusInternal: o.DeleteMarkerReplicationStatus,
+ VersionPurgeStatusInternal: o.VersionPurgeStatuses,
+ ReplicateDecisionStr: o.ReplicateDecisionStr,
+ }
+
+ r.Targets = replicationStatusesMap(o.DeleteMarkerReplicationStatus)
+ r.PurgeTargets = versionPurgeStatusesMap(o.VersionPurgeStatuses)
+ return r
+}
+
+// VersionPurgeStatus returns a composite version purge status across targets
+func (d *DeletedObject) VersionPurgeStatus() VersionPurgeStatusType {
+ return d.ReplicationState.CompositeVersionPurgeStatus()
+}
+
+// DeleteMarkerReplicationStatus return composite replication status of delete marker across targets
+func (d *DeletedObject) DeleteMarkerReplicationStatus() replication.StatusType {
+ return d.ReplicationState.CompositeReplicationStatus()
+}
+
+// ResyncTargetsInfo holds a slice of targets with resync info per target
+type ResyncTargetsInfo struct {
+ Targets []ResyncTarget `json:"target,omitempty"`
+}
+
+// ResyncTarget is a struct representing the Target reset ID where target is identified by its Arn
+type ResyncTarget struct {
+ Arn string `json:"arn"`
+ ResetID string `json:"resetid"`
+ StartTime time.Time `json:"startTime"`
+ EndTime time.Time `json:"endTime"`
+ // Status of resync operation
+ ResyncStatus string `json:"resyncStatus,omitempty"`
+ // Completed size in bytes
+ ReplicatedSize int64 `json:"completedReplicationSize"`
+ // Failed size in bytes
+ FailedSize int64 `json:"failedReplicationSize"`
+ // Total number of failed operations
+ FailedCount int64 `json:"failedReplicationCount"`
+ // Total number of failed operations
+ ReplicatedCount int64 `json:"replicationCount"`
+ // Last bucket/object replicated.
+ Bucket string `json:"bucket,omitempty"`
+ Object string `json:"object,omitempty"`
+}
+
+// VersionPurgeStatusType represents status of a versioned delete or permanent delete w.r.t bucket replication
+type VersionPurgeStatusType = replication.VersionPurgeStatusType
+
+type replicationResyncer struct {
+ // map of bucket to their resync status
+ statusMap map[string]BucketReplicationResyncStatus
+ workerSize int
+ resyncCancelCh chan struct{}
+ workerCh chan struct{}
+ sync.RWMutex
+}
+
+const (
+ replicationDir = ".replication"
+ resyncFileName = "resync.bin"
+ resyncMetaFormat = 1
+ resyncMetaVersionV1 = 1
+ resyncMetaVersion = resyncMetaVersionV1
+)
+
+type resyncOpts struct {
+ bucket string
+ arn string
+ resyncID string
+ resyncBefore time.Time
+}
+
+// ResyncStatusType status of resync operation
+type ResyncStatusType int
+
+const (
+ // NoResync - no resync in progress
+ NoResync ResyncStatusType = iota
+ // ResyncPending - resync pending
+ ResyncPending
+ // ResyncCanceled - resync canceled
+ ResyncCanceled
+ // ResyncStarted - resync in progress
+ ResyncStarted
+ // ResyncCompleted - resync finished
+ ResyncCompleted
+ // ResyncFailed - resync failed
+ ResyncFailed
+)
+
+func (rt ResyncStatusType) isValid() bool {
+ return rt != NoResync
+}
+
+func (rt ResyncStatusType) String() string {
+ switch rt {
+ case ResyncStarted:
+ return "Ongoing"
+ case ResyncCompleted:
+ return "Completed"
+ case ResyncFailed:
+ return "Failed"
+ case ResyncPending:
+ return "Pending"
+ case ResyncCanceled:
+ return "Canceled"
+ default:
+ return ""
+ }
+}
+
+// TargetReplicationResyncStatus status of resync of bucket for a specific target
+type TargetReplicationResyncStatus struct {
+ StartTime time.Time `json:"startTime" msg:"st"`
+ LastUpdate time.Time `json:"lastUpdated" msg:"lst"`
+ // Resync ID assigned to this reset
+ ResyncID string `json:"resyncID" msg:"id"`
+ // ResyncBeforeDate - resync all objects created prior to this date
+ ResyncBeforeDate time.Time `json:"resyncBeforeDate" msg:"rdt"`
+ // Status of resync operation
+ ResyncStatus ResyncStatusType `json:"resyncStatus" msg:"rst"`
+ // Failed size in bytes
+ FailedSize int64 `json:"failedReplicationSize" msg:"fs"`
+ // Total number of failed operations
+ FailedCount int64 `json:"failedReplicationCount" msg:"frc"`
+ // Completed size in bytes
+ ReplicatedSize int64 `json:"completedReplicationSize" msg:"rs"`
+ // Total number of failed operations
+ ReplicatedCount int64 `json:"replicationCount" msg:"rrc"`
+ // Last bucket/object replicated.
+ Bucket string `json:"-" msg:"bkt"`
+ Object string `json:"-" msg:"obj"`
+ Error error `json:"-" msg:"-"`
+}
+
+// BucketReplicationResyncStatus captures current replication resync status
+type BucketReplicationResyncStatus struct {
+ Version int `json:"version" msg:"v"`
+ // map of remote arn to their resync status for a bucket
+ TargetsMap map[string]TargetReplicationResyncStatus `json:"resyncMap,omitempty" msg:"brs"`
+ ID int `json:"id" msg:"id"`
+ LastUpdate time.Time `json:"lastUpdate" msg:"lu"`
+}
+
+func (rs *BucketReplicationResyncStatus) cloneTgtStats() (m map[string]TargetReplicationResyncStatus) {
+ m = make(map[string]TargetReplicationResyncStatus)
+ maps.Copy(m, rs.TargetsMap)
+ return m
+}
+
+func newBucketResyncStatus(bucket string) BucketReplicationResyncStatus {
+ return BucketReplicationResyncStatus{
+ TargetsMap: make(map[string]TargetReplicationResyncStatus),
+ Version: resyncMetaVersion,
+ }
+}
+
+var contentRangeRegexp = regexp.MustCompile(`bytes ([0-9]+)-([0-9]+)/([0-9]+|\\*)`)
+
+// parse size from content-range header
+func parseSizeFromContentRange(h http.Header) (sz int64, err error) {
+ cr := h.Get(xhttp.ContentRange)
+ if cr == "" {
+ return sz, fmt.Errorf("Content-Range not set")
+ }
+ parts := contentRangeRegexp.FindStringSubmatch(cr)
+ if len(parts) != 4 {
+ return sz, fmt.Errorf("invalid Content-Range header %s", cr)
+ }
+ if parts[3] == "*" {
+ return -1, nil
+ }
+ var usz uint64
+ usz, err = strconv.ParseUint(parts[3], 10, 64)
+ if err != nil {
+ return sz, err
+ }
+ return int64(usz), nil
+}
+
+func extractReplicateDiffOpts(q url.Values) (opts madmin.ReplDiffOpts) {
+ opts.Verbose = q.Get("verbose") == "true"
+ opts.ARN = q.Get("arn")
+ opts.Prefix = q.Get("prefix")
+ return opts
+}
+
+const (
+ replicationMRFDir = bucketMetaPrefix + SlashSeparator + replicationDir + SlashSeparator + "mrf"
+ mrfMetaFormat = 1
+ mrfMetaVersionV1 = 1
+ mrfMetaVersion = mrfMetaVersionV1
+)
+
+// MRFReplicateEntry mrf entry to save to disk
+type MRFReplicateEntry struct {
+ Bucket string `json:"bucket" msg:"b"`
+ Object string `json:"object" msg:"o"`
+ versionID string `json:"-"`
+ RetryCount int `json:"retryCount" msg:"rc"`
+ sz int64 `json:"-"`
+}
+
+// MRFReplicateEntries has the map of MRF entries to save to disk
+type MRFReplicateEntries struct {
+ Entries map[string]MRFReplicateEntry `json:"entries" msg:"e"`
+ Version int `json:"version" msg:"v"`
+}
+
+// ToMRFEntry returns the relevant info needed by MRF
+func (ri ReplicateObjectInfo) ToMRFEntry() MRFReplicateEntry {
+ return MRFReplicateEntry{
+ Bucket: ri.Bucket,
+ Object: ri.Name,
+ versionID: ri.VersionID,
+ sz: ri.Size,
+ RetryCount: int(ri.RetryCount),
+ }
+}
diff --git a/cmd/bucket-replication-utils_gen.go b/cmd/bucket-replication-utils_gen.go
new file mode 100644
index 00000000..93386921
--- /dev/null
+++ b/cmd/bucket-replication-utils_gen.go
@@ -0,0 +1,2485 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "github.com/minio/minio/internal/bucket/replication"
+ "github.com/tinylib/msgp/msgp"
+)
+
+// DecodeMsg implements msgp.Decodable
+func (z *BucketReplicationResyncStatus) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "v":
+ z.Version, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "Version")
+ return
+ }
+ case "brs":
+ var zb0002 uint32
+ zb0002, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "TargetsMap")
+ return
+ }
+ if z.TargetsMap == nil {
+ z.TargetsMap = make(map[string]TargetReplicationResyncStatus, zb0002)
+ } else if len(z.TargetsMap) > 0 {
+ clear(z.TargetsMap)
+ }
+ for zb0002 > 0 {
+ zb0002--
+ var za0001 string
+ za0001, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "TargetsMap")
+ return
+ }
+ var za0002 TargetReplicationResyncStatus
+ err = za0002.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "TargetsMap", za0001)
+ return
+ }
+ z.TargetsMap[za0001] = za0002
+ }
+ case "id":
+ z.ID, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "ID")
+ return
+ }
+ case "lu":
+ z.LastUpdate, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "LastUpdate")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *BucketReplicationResyncStatus) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 4
+ // write "v"
+ err = en.Append(0x84, 0xa1, 0x76)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.Version)
+ if err != nil {
+ err = msgp.WrapError(err, "Version")
+ return
+ }
+ // write "brs"
+ err = en.Append(0xa3, 0x62, 0x72, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteMapHeader(uint32(len(z.TargetsMap)))
+ if err != nil {
+ err = msgp.WrapError(err, "TargetsMap")
+ return
+ }
+ for za0001, za0002 := range z.TargetsMap {
+ err = en.WriteString(za0001)
+ if err != nil {
+ err = msgp.WrapError(err, "TargetsMap")
+ return
+ }
+ err = za0002.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "TargetsMap", za0001)
+ return
+ }
+ }
+ // write "id"
+ err = en.Append(0xa2, 0x69, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.ID)
+ if err != nil {
+ err = msgp.WrapError(err, "ID")
+ return
+ }
+ // write "lu"
+ err = en.Append(0xa2, 0x6c, 0x75)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.LastUpdate)
+ if err != nil {
+ err = msgp.WrapError(err, "LastUpdate")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *BucketReplicationResyncStatus) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 4
+ // string "v"
+ o = append(o, 0x84, 0xa1, 0x76)
+ o = msgp.AppendInt(o, z.Version)
+ // string "brs"
+ o = append(o, 0xa3, 0x62, 0x72, 0x73)
+ o = msgp.AppendMapHeader(o, uint32(len(z.TargetsMap)))
+ for za0001, za0002 := range z.TargetsMap {
+ o = msgp.AppendString(o, za0001)
+ o, err = za0002.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "TargetsMap", za0001)
+ return
+ }
+ }
+ // string "id"
+ o = append(o, 0xa2, 0x69, 0x64)
+ o = msgp.AppendInt(o, z.ID)
+ // string "lu"
+ o = append(o, 0xa2, 0x6c, 0x75)
+ o = msgp.AppendTime(o, z.LastUpdate)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BucketReplicationResyncStatus) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "v":
+ z.Version, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Version")
+ return
+ }
+ case "brs":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "TargetsMap")
+ return
+ }
+ if z.TargetsMap == nil {
+ z.TargetsMap = make(map[string]TargetReplicationResyncStatus, zb0002)
+ } else if len(z.TargetsMap) > 0 {
+ clear(z.TargetsMap)
+ }
+ for zb0002 > 0 {
+ var za0002 TargetReplicationResyncStatus
+ zb0002--
+ var za0001 string
+ za0001, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "TargetsMap")
+ return
+ }
+ bts, err = za0002.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "TargetsMap", za0001)
+ return
+ }
+ z.TargetsMap[za0001] = za0002
+ }
+ case "id":
+ z.ID, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ID")
+ return
+ }
+ case "lu":
+ z.LastUpdate, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "LastUpdate")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *BucketReplicationResyncStatus) Msgsize() (s int) {
+ s = 1 + 2 + msgp.IntSize + 4 + msgp.MapHeaderSize
+ if z.TargetsMap != nil {
+ for za0001, za0002 := range z.TargetsMap {
+ _ = za0002
+ s += msgp.StringPrefixSize + len(za0001) + za0002.Msgsize()
+ }
+ }
+ s += 3 + msgp.IntSize + 3 + msgp.TimeSize
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *MRFReplicateEntries) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "e":
+ var zb0002 uint32
+ zb0002, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Entries")
+ return
+ }
+ if z.Entries == nil {
+ z.Entries = make(map[string]MRFReplicateEntry, zb0002)
+ } else if len(z.Entries) > 0 {
+ clear(z.Entries)
+ }
+ for zb0002 > 0 {
+ zb0002--
+ var za0001 string
+ za0001, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Entries")
+ return
+ }
+ var za0002 MRFReplicateEntry
+ var zb0003 uint32
+ zb0003, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Entries", za0001)
+ return
+ }
+ for zb0003 > 0 {
+ zb0003--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err, "Entries", za0001)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "b":
+ za0002.Bucket, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Entries", za0001, "Bucket")
+ return
+ }
+ case "o":
+ za0002.Object, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Entries", za0001, "Object")
+ return
+ }
+ case "rc":
+ za0002.RetryCount, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "Entries", za0001, "RetryCount")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err, "Entries", za0001)
+ return
+ }
+ }
+ }
+ z.Entries[za0001] = za0002
+ }
+ case "v":
+ z.Version, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "Version")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *MRFReplicateEntries) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 2
+ // write "e"
+ err = en.Append(0x82, 0xa1, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteMapHeader(uint32(len(z.Entries)))
+ if err != nil {
+ err = msgp.WrapError(err, "Entries")
+ return
+ }
+ for za0001, za0002 := range z.Entries {
+ err = en.WriteString(za0001)
+ if err != nil {
+ err = msgp.WrapError(err, "Entries")
+ return
+ }
+ // map header, size 3
+ // write "b"
+ err = en.Append(0x83, 0xa1, 0x62)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(za0002.Bucket)
+ if err != nil {
+ err = msgp.WrapError(err, "Entries", za0001, "Bucket")
+ return
+ }
+ // write "o"
+ err = en.Append(0xa1, 0x6f)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(za0002.Object)
+ if err != nil {
+ err = msgp.WrapError(err, "Entries", za0001, "Object")
+ return
+ }
+ // write "rc"
+ err = en.Append(0xa2, 0x72, 0x63)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(za0002.RetryCount)
+ if err != nil {
+ err = msgp.WrapError(err, "Entries", za0001, "RetryCount")
+ return
+ }
+ }
+ // write "v"
+ err = en.Append(0xa1, 0x76)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.Version)
+ if err != nil {
+ err = msgp.WrapError(err, "Version")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *MRFReplicateEntries) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 2
+ // string "e"
+ o = append(o, 0x82, 0xa1, 0x65)
+ o = msgp.AppendMapHeader(o, uint32(len(z.Entries)))
+ for za0001, za0002 := range z.Entries {
+ o = msgp.AppendString(o, za0001)
+ // map header, size 3
+ // string "b"
+ o = append(o, 0x83, 0xa1, 0x62)
+ o = msgp.AppendString(o, za0002.Bucket)
+ // string "o"
+ o = append(o, 0xa1, 0x6f)
+ o = msgp.AppendString(o, za0002.Object)
+ // string "rc"
+ o = append(o, 0xa2, 0x72, 0x63)
+ o = msgp.AppendInt(o, za0002.RetryCount)
+ }
+ // string "v"
+ o = append(o, 0xa1, 0x76)
+ o = msgp.AppendInt(o, z.Version)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *MRFReplicateEntries) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "e":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Entries")
+ return
+ }
+ if z.Entries == nil {
+ z.Entries = make(map[string]MRFReplicateEntry, zb0002)
+ } else if len(z.Entries) > 0 {
+ clear(z.Entries)
+ }
+ for zb0002 > 0 {
+ var za0002 MRFReplicateEntry
+ zb0002--
+ var za0001 string
+ za0001, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Entries")
+ return
+ }
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Entries", za0001)
+ return
+ }
+ for zb0003 > 0 {
+ zb0003--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Entries", za0001)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "b":
+ za0002.Bucket, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Entries", za0001, "Bucket")
+ return
+ }
+ case "o":
+ za0002.Object, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Entries", za0001, "Object")
+ return
+ }
+ case "rc":
+ za0002.RetryCount, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Entries", za0001, "RetryCount")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Entries", za0001)
+ return
+ }
+ }
+ }
+ z.Entries[za0001] = za0002
+ }
+ case "v":
+ z.Version, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Version")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *MRFReplicateEntries) Msgsize() (s int) {
+ s = 1 + 2 + msgp.MapHeaderSize
+ if z.Entries != nil {
+ for za0001, za0002 := range z.Entries {
+ _ = za0002
+ s += msgp.StringPrefixSize + len(za0001) + 1 + 2 + msgp.StringPrefixSize + len(za0002.Bucket) + 2 + msgp.StringPrefixSize + len(za0002.Object) + 3 + msgp.IntSize
+ }
+ }
+ s += 2 + msgp.IntSize
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *MRFReplicateEntry) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "b":
+ z.Bucket, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "o":
+ z.Object, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ case "rc":
+ z.RetryCount, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "RetryCount")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z MRFReplicateEntry) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 3
+ // write "b"
+ err = en.Append(0x83, 0xa1, 0x62)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Bucket)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ // write "o"
+ err = en.Append(0xa1, 0x6f)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Object)
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ // write "rc"
+ err = en.Append(0xa2, 0x72, 0x63)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.RetryCount)
+ if err != nil {
+ err = msgp.WrapError(err, "RetryCount")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z MRFReplicateEntry) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 3
+ // string "b"
+ o = append(o, 0x83, 0xa1, 0x62)
+ o = msgp.AppendString(o, z.Bucket)
+ // string "o"
+ o = append(o, 0xa1, 0x6f)
+ o = msgp.AppendString(o, z.Object)
+ // string "rc"
+ o = append(o, 0xa2, 0x72, 0x63)
+ o = msgp.AppendInt(o, z.RetryCount)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *MRFReplicateEntry) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "b":
+ z.Bucket, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "o":
+ z.Object, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ case "rc":
+ z.RetryCount, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "RetryCount")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z MRFReplicateEntry) Msgsize() (s int) {
+ s = 1 + 2 + msgp.StringPrefixSize + len(z.Bucket) + 2 + msgp.StringPrefixSize + len(z.Object) + 3 + msgp.IntSize
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *ReplicateDecision) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z ReplicateDecision) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 0
+ _ = z
+ err = en.Append(0x80)
+ if err != nil {
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z ReplicateDecision) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 0
+ _ = z
+ o = append(o, 0x80)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ReplicateDecision) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z ReplicateDecision) Msgsize() (s int) {
+ s = 1
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *ReplicationState) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ReplicaTimeStamp":
+ z.ReplicaTimeStamp, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicaTimeStamp")
+ return
+ }
+ case "ReplicaStatus":
+ err = z.ReplicaStatus.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicaStatus")
+ return
+ }
+ case "DeleteMarker":
+ z.DeleteMarker, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "DeleteMarker")
+ return
+ }
+ case "ReplicationTimeStamp":
+ z.ReplicationTimeStamp, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationTimeStamp")
+ return
+ }
+ case "ReplicationStatusInternal":
+ z.ReplicationStatusInternal, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationStatusInternal")
+ return
+ }
+ case "VersionPurgeStatusInternal":
+ z.VersionPurgeStatusInternal, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "VersionPurgeStatusInternal")
+ return
+ }
+ case "ReplicateDecisionStr":
+ z.ReplicateDecisionStr, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicateDecisionStr")
+ return
+ }
+ case "Targets":
+ var zb0002 uint32
+ zb0002, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Targets")
+ return
+ }
+ if z.Targets == nil {
+ z.Targets = make(map[string]replication.StatusType, zb0002)
+ } else if len(z.Targets) > 0 {
+ clear(z.Targets)
+ }
+ for zb0002 > 0 {
+ zb0002--
+ var za0001 string
+ za0001, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Targets")
+ return
+ }
+ var za0002 replication.StatusType
+ err = za0002.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Targets", za0001)
+ return
+ }
+ z.Targets[za0001] = za0002
+ }
+ case "PurgeTargets":
+ var zb0003 uint32
+ zb0003, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "PurgeTargets")
+ return
+ }
+ if z.PurgeTargets == nil {
+ z.PurgeTargets = make(map[string]VersionPurgeStatusType, zb0003)
+ } else if len(z.PurgeTargets) > 0 {
+ clear(z.PurgeTargets)
+ }
+ for zb0003 > 0 {
+ zb0003--
+ var za0003 string
+ za0003, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "PurgeTargets")
+ return
+ }
+ var za0004 VersionPurgeStatusType
+ err = za0004.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "PurgeTargets", za0003)
+ return
+ }
+ z.PurgeTargets[za0003] = za0004
+ }
+ case "ResetStatusesMap":
+ var zb0004 uint32
+ zb0004, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "ResetStatusesMap")
+ return
+ }
+ if z.ResetStatusesMap == nil {
+ z.ResetStatusesMap = make(map[string]string, zb0004)
+ } else if len(z.ResetStatusesMap) > 0 {
+ clear(z.ResetStatusesMap)
+ }
+ for zb0004 > 0 {
+ zb0004--
+ var za0005 string
+ za0005, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "ResetStatusesMap")
+ return
+ }
+ var za0006 string
+ za0006, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "ResetStatusesMap", za0005)
+ return
+ }
+ z.ResetStatusesMap[za0005] = za0006
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *ReplicationState) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 10
+ // write "ReplicaTimeStamp"
+ err = en.Append(0x8a, 0xb0, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x74, 0x61, 0x6d, 0x70)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.ReplicaTimeStamp)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicaTimeStamp")
+ return
+ }
+ // write "ReplicaStatus"
+ err = en.Append(0xad, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73)
+ if err != nil {
+ return
+ }
+ err = z.ReplicaStatus.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicaStatus")
+ return
+ }
+ // write "DeleteMarker"
+ err = en.Append(0xac, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.DeleteMarker)
+ if err != nil {
+ err = msgp.WrapError(err, "DeleteMarker")
+ return
+ }
+ // write "ReplicationTimeStamp"
+ err = en.Append(0xb4, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x74, 0x61, 0x6d, 0x70)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.ReplicationTimeStamp)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationTimeStamp")
+ return
+ }
+ // write "ReplicationStatusInternal"
+ err = en.Append(0xb9, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.ReplicationStatusInternal)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationStatusInternal")
+ return
+ }
+ // write "VersionPurgeStatusInternal"
+ err = en.Append(0xba, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x75, 0x72, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.VersionPurgeStatusInternal)
+ if err != nil {
+ err = msgp.WrapError(err, "VersionPurgeStatusInternal")
+ return
+ }
+ // write "ReplicateDecisionStr"
+ err = en.Append(0xb4, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x72)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.ReplicateDecisionStr)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicateDecisionStr")
+ return
+ }
+ // write "Targets"
+ err = en.Append(0xa7, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteMapHeader(uint32(len(z.Targets)))
+ if err != nil {
+ err = msgp.WrapError(err, "Targets")
+ return
+ }
+ for za0001, za0002 := range z.Targets {
+ err = en.WriteString(za0001)
+ if err != nil {
+ err = msgp.WrapError(err, "Targets")
+ return
+ }
+ err = za0002.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Targets", za0001)
+ return
+ }
+ }
+ // write "PurgeTargets"
+ err = en.Append(0xac, 0x50, 0x75, 0x72, 0x67, 0x65, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteMapHeader(uint32(len(z.PurgeTargets)))
+ if err != nil {
+ err = msgp.WrapError(err, "PurgeTargets")
+ return
+ }
+ for za0003, za0004 := range z.PurgeTargets {
+ err = en.WriteString(za0003)
+ if err != nil {
+ err = msgp.WrapError(err, "PurgeTargets")
+ return
+ }
+ err = za0004.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "PurgeTargets", za0003)
+ return
+ }
+ }
+ // write "ResetStatusesMap"
+ err = en.Append(0xb0, 0x52, 0x65, 0x73, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x4d, 0x61, 0x70)
+ if err != nil {
+ return
+ }
+ err = en.WriteMapHeader(uint32(len(z.ResetStatusesMap)))
+ if err != nil {
+ err = msgp.WrapError(err, "ResetStatusesMap")
+ return
+ }
+ for za0005, za0006 := range z.ResetStatusesMap {
+ err = en.WriteString(za0005)
+ if err != nil {
+ err = msgp.WrapError(err, "ResetStatusesMap")
+ return
+ }
+ err = en.WriteString(za0006)
+ if err != nil {
+ err = msgp.WrapError(err, "ResetStatusesMap", za0005)
+ return
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *ReplicationState) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 10
+ // string "ReplicaTimeStamp"
+ o = append(o, 0x8a, 0xb0, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x74, 0x61, 0x6d, 0x70)
+ o = msgp.AppendTime(o, z.ReplicaTimeStamp)
+ // string "ReplicaStatus"
+ o = append(o, 0xad, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73)
+ o, err = z.ReplicaStatus.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicaStatus")
+ return
+ }
+ // string "DeleteMarker"
+ o = append(o, 0xac, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72)
+ o = msgp.AppendBool(o, z.DeleteMarker)
+ // string "ReplicationTimeStamp"
+ o = append(o, 0xb4, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x74, 0x61, 0x6d, 0x70)
+ o = msgp.AppendTime(o, z.ReplicationTimeStamp)
+ // string "ReplicationStatusInternal"
+ o = append(o, 0xb9, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c)
+ o = msgp.AppendString(o, z.ReplicationStatusInternal)
+ // string "VersionPurgeStatusInternal"
+ o = append(o, 0xba, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x75, 0x72, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c)
+ o = msgp.AppendString(o, z.VersionPurgeStatusInternal)
+ // string "ReplicateDecisionStr"
+ o = append(o, 0xb4, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x44, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x72)
+ o = msgp.AppendString(o, z.ReplicateDecisionStr)
+ // string "Targets"
+ o = append(o, 0xa7, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73)
+ o = msgp.AppendMapHeader(o, uint32(len(z.Targets)))
+ for za0001, za0002 := range z.Targets {
+ o = msgp.AppendString(o, za0001)
+ o, err = za0002.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Targets", za0001)
+ return
+ }
+ }
+ // string "PurgeTargets"
+ o = append(o, 0xac, 0x50, 0x75, 0x72, 0x67, 0x65, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73)
+ o = msgp.AppendMapHeader(o, uint32(len(z.PurgeTargets)))
+ for za0003, za0004 := range z.PurgeTargets {
+ o = msgp.AppendString(o, za0003)
+ o, err = za0004.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "PurgeTargets", za0003)
+ return
+ }
+ }
+ // string "ResetStatusesMap"
+ o = append(o, 0xb0, 0x52, 0x65, 0x73, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x4d, 0x61, 0x70)
+ o = msgp.AppendMapHeader(o, uint32(len(z.ResetStatusesMap)))
+ for za0005, za0006 := range z.ResetStatusesMap {
+ o = msgp.AppendString(o, za0005)
+ o = msgp.AppendString(o, za0006)
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ReplicationState) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ReplicaTimeStamp":
+ z.ReplicaTimeStamp, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicaTimeStamp")
+ return
+ }
+ case "ReplicaStatus":
+ bts, err = z.ReplicaStatus.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicaStatus")
+ return
+ }
+ case "DeleteMarker":
+ z.DeleteMarker, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "DeleteMarker")
+ return
+ }
+ case "ReplicationTimeStamp":
+ z.ReplicationTimeStamp, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationTimeStamp")
+ return
+ }
+ case "ReplicationStatusInternal":
+ z.ReplicationStatusInternal, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationStatusInternal")
+ return
+ }
+ case "VersionPurgeStatusInternal":
+ z.VersionPurgeStatusInternal, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "VersionPurgeStatusInternal")
+ return
+ }
+ case "ReplicateDecisionStr":
+ z.ReplicateDecisionStr, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicateDecisionStr")
+ return
+ }
+ case "Targets":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Targets")
+ return
+ }
+ if z.Targets == nil {
+ z.Targets = make(map[string]replication.StatusType, zb0002)
+ } else if len(z.Targets) > 0 {
+ clear(z.Targets)
+ }
+ for zb0002 > 0 {
+ var za0002 replication.StatusType
+ zb0002--
+ var za0001 string
+ za0001, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Targets")
+ return
+ }
+ bts, err = za0002.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Targets", za0001)
+ return
+ }
+ z.Targets[za0001] = za0002
+ }
+ case "PurgeTargets":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "PurgeTargets")
+ return
+ }
+ if z.PurgeTargets == nil {
+ z.PurgeTargets = make(map[string]VersionPurgeStatusType, zb0003)
+ } else if len(z.PurgeTargets) > 0 {
+ clear(z.PurgeTargets)
+ }
+ for zb0003 > 0 {
+ var za0004 VersionPurgeStatusType
+ zb0003--
+ var za0003 string
+ za0003, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "PurgeTargets")
+ return
+ }
+ bts, err = za0004.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "PurgeTargets", za0003)
+ return
+ }
+ z.PurgeTargets[za0003] = za0004
+ }
+ case "ResetStatusesMap":
+ var zb0004 uint32
+ zb0004, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ResetStatusesMap")
+ return
+ }
+ if z.ResetStatusesMap == nil {
+ z.ResetStatusesMap = make(map[string]string, zb0004)
+ } else if len(z.ResetStatusesMap) > 0 {
+ clear(z.ResetStatusesMap)
+ }
+ for zb0004 > 0 {
+ var za0006 string
+ zb0004--
+ var za0005 string
+ za0005, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ResetStatusesMap")
+ return
+ }
+ za0006, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ResetStatusesMap", za0005)
+ return
+ }
+ z.ResetStatusesMap[za0005] = za0006
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *ReplicationState) Msgsize() (s int) {
+ s = 1 + 17 + msgp.TimeSize + 14 + z.ReplicaStatus.Msgsize() + 13 + msgp.BoolSize + 21 + msgp.TimeSize + 26 + msgp.StringPrefixSize + len(z.ReplicationStatusInternal) + 27 + msgp.StringPrefixSize + len(z.VersionPurgeStatusInternal) + 21 + msgp.StringPrefixSize + len(z.ReplicateDecisionStr) + 8 + msgp.MapHeaderSize
+ if z.Targets != nil {
+ for za0001, za0002 := range z.Targets {
+ _ = za0002
+ s += msgp.StringPrefixSize + len(za0001) + za0002.Msgsize()
+ }
+ }
+ s += 13 + msgp.MapHeaderSize
+ if z.PurgeTargets != nil {
+ for za0003, za0004 := range z.PurgeTargets {
+ _ = za0004
+ s += msgp.StringPrefixSize + len(za0003) + za0004.Msgsize()
+ }
+ }
+ s += 17 + msgp.MapHeaderSize
+ if z.ResetStatusesMap != nil {
+ for za0005, za0006 := range z.ResetStatusesMap {
+ _ = za0006
+ s += msgp.StringPrefixSize + len(za0005) + msgp.StringPrefixSize + len(za0006)
+ }
+ }
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *ResyncDecision) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z ResyncDecision) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 0
+ _ = z
+ err = en.Append(0x80)
+ if err != nil {
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z ResyncDecision) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 0
+ _ = z
+ o = append(o, 0x80)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ResyncDecision) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z ResyncDecision) Msgsize() (s int) {
+ s = 1
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *ResyncStatusType) DecodeMsg(dc *msgp.Reader) (err error) {
+ {
+ var zb0001 int
+ zb0001, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ (*z) = ResyncStatusType(zb0001)
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z ResyncStatusType) EncodeMsg(en *msgp.Writer) (err error) {
+ err = en.WriteInt(int(z))
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z ResyncStatusType) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ o = msgp.AppendInt(o, int(z))
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ResyncStatusType) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ {
+ var zb0001 int
+ zb0001, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ (*z) = ResyncStatusType(zb0001)
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z ResyncStatusType) Msgsize() (s int) {
+ s = msgp.IntSize
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *ResyncTarget) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Arn":
+ z.Arn, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Arn")
+ return
+ }
+ case "ResetID":
+ z.ResetID, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "ResetID")
+ return
+ }
+ case "StartTime":
+ z.StartTime, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "StartTime")
+ return
+ }
+ case "EndTime":
+ z.EndTime, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "EndTime")
+ return
+ }
+ case "ResyncStatus":
+ z.ResyncStatus, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "ResyncStatus")
+ return
+ }
+ case "ReplicatedSize":
+ z.ReplicatedSize, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedSize")
+ return
+ }
+ case "FailedSize":
+ z.FailedSize, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "FailedSize")
+ return
+ }
+ case "FailedCount":
+ z.FailedCount, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "FailedCount")
+ return
+ }
+ case "ReplicatedCount":
+ z.ReplicatedCount, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedCount")
+ return
+ }
+ case "Bucket":
+ z.Bucket, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "Object":
+ z.Object, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *ResyncTarget) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 11
+ // write "Arn"
+ err = en.Append(0x8b, 0xa3, 0x41, 0x72, 0x6e)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Arn)
+ if err != nil {
+ err = msgp.WrapError(err, "Arn")
+ return
+ }
+ // write "ResetID"
+ err = en.Append(0xa7, 0x52, 0x65, 0x73, 0x65, 0x74, 0x49, 0x44)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.ResetID)
+ if err != nil {
+ err = msgp.WrapError(err, "ResetID")
+ return
+ }
+ // write "StartTime"
+ err = en.Append(0xa9, 0x53, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.StartTime)
+ if err != nil {
+ err = msgp.WrapError(err, "StartTime")
+ return
+ }
+ // write "EndTime"
+ err = en.Append(0xa7, 0x45, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.EndTime)
+ if err != nil {
+ err = msgp.WrapError(err, "EndTime")
+ return
+ }
+ // write "ResyncStatus"
+ err = en.Append(0xac, 0x52, 0x65, 0x73, 0x79, 0x6e, 0x63, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.ResyncStatus)
+ if err != nil {
+ err = msgp.WrapError(err, "ResyncStatus")
+ return
+ }
+ // write "ReplicatedSize"
+ err = en.Append(0xae, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.ReplicatedSize)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedSize")
+ return
+ }
+ // write "FailedSize"
+ err = en.Append(0xaa, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.FailedSize)
+ if err != nil {
+ err = msgp.WrapError(err, "FailedSize")
+ return
+ }
+ // write "FailedCount"
+ err = en.Append(0xab, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.FailedCount)
+ if err != nil {
+ err = msgp.WrapError(err, "FailedCount")
+ return
+ }
+ // write "ReplicatedCount"
+ err = en.Append(0xaf, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.ReplicatedCount)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedCount")
+ return
+ }
+ // write "Bucket"
+ err = en.Append(0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Bucket)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ // write "Object"
+ err = en.Append(0xa6, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Object)
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *ResyncTarget) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 11
+ // string "Arn"
+ o = append(o, 0x8b, 0xa3, 0x41, 0x72, 0x6e)
+ o = msgp.AppendString(o, z.Arn)
+ // string "ResetID"
+ o = append(o, 0xa7, 0x52, 0x65, 0x73, 0x65, 0x74, 0x49, 0x44)
+ o = msgp.AppendString(o, z.ResetID)
+ // string "StartTime"
+ o = append(o, 0xa9, 0x53, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65)
+ o = msgp.AppendTime(o, z.StartTime)
+ // string "EndTime"
+ o = append(o, 0xa7, 0x45, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65)
+ o = msgp.AppendTime(o, z.EndTime)
+ // string "ResyncStatus"
+ o = append(o, 0xac, 0x52, 0x65, 0x73, 0x79, 0x6e, 0x63, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73)
+ o = msgp.AppendString(o, z.ResyncStatus)
+ // string "ReplicatedSize"
+ o = append(o, 0xae, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65)
+ o = msgp.AppendInt64(o, z.ReplicatedSize)
+ // string "FailedSize"
+ o = append(o, 0xaa, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65)
+ o = msgp.AppendInt64(o, z.FailedSize)
+ // string "FailedCount"
+ o = append(o, 0xab, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ o = msgp.AppendInt64(o, z.FailedCount)
+ // string "ReplicatedCount"
+ o = append(o, 0xaf, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ o = msgp.AppendInt64(o, z.ReplicatedCount)
+ // string "Bucket"
+ o = append(o, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ o = msgp.AppendString(o, z.Bucket)
+ // string "Object"
+ o = append(o, 0xa6, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74)
+ o = msgp.AppendString(o, z.Object)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ResyncTarget) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Arn":
+ z.Arn, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Arn")
+ return
+ }
+ case "ResetID":
+ z.ResetID, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ResetID")
+ return
+ }
+ case "StartTime":
+ z.StartTime, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "StartTime")
+ return
+ }
+ case "EndTime":
+ z.EndTime, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "EndTime")
+ return
+ }
+ case "ResyncStatus":
+ z.ResyncStatus, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ResyncStatus")
+ return
+ }
+ case "ReplicatedSize":
+ z.ReplicatedSize, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedSize")
+ return
+ }
+ case "FailedSize":
+ z.FailedSize, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "FailedSize")
+ return
+ }
+ case "FailedCount":
+ z.FailedCount, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "FailedCount")
+ return
+ }
+ case "ReplicatedCount":
+ z.ReplicatedCount, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedCount")
+ return
+ }
+ case "Bucket":
+ z.Bucket, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "Object":
+ z.Object, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *ResyncTarget) Msgsize() (s int) {
+ s = 1 + 4 + msgp.StringPrefixSize + len(z.Arn) + 8 + msgp.StringPrefixSize + len(z.ResetID) + 10 + msgp.TimeSize + 8 + msgp.TimeSize + 13 + msgp.StringPrefixSize + len(z.ResyncStatus) + 15 + msgp.Int64Size + 11 + msgp.Int64Size + 12 + msgp.Int64Size + 16 + msgp.Int64Size + 7 + msgp.StringPrefixSize + len(z.Bucket) + 7 + msgp.StringPrefixSize + len(z.Object)
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *ResyncTargetDecision) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Replicate":
+ z.Replicate, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "Replicate")
+ return
+ }
+ case "ResetID":
+ z.ResetID, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "ResetID")
+ return
+ }
+ case "ResetBeforeDate":
+ z.ResetBeforeDate, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "ResetBeforeDate")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z ResyncTargetDecision) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 3
+ // write "Replicate"
+ err = en.Append(0x83, 0xa9, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.Replicate)
+ if err != nil {
+ err = msgp.WrapError(err, "Replicate")
+ return
+ }
+ // write "ResetID"
+ err = en.Append(0xa7, 0x52, 0x65, 0x73, 0x65, 0x74, 0x49, 0x44)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.ResetID)
+ if err != nil {
+ err = msgp.WrapError(err, "ResetID")
+ return
+ }
+ // write "ResetBeforeDate"
+ err = en.Append(0xaf, 0x52, 0x65, 0x73, 0x65, 0x74, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x44, 0x61, 0x74, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.ResetBeforeDate)
+ if err != nil {
+ err = msgp.WrapError(err, "ResetBeforeDate")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z ResyncTargetDecision) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 3
+ // string "Replicate"
+ o = append(o, 0x83, 0xa9, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65)
+ o = msgp.AppendBool(o, z.Replicate)
+ // string "ResetID"
+ o = append(o, 0xa7, 0x52, 0x65, 0x73, 0x65, 0x74, 0x49, 0x44)
+ o = msgp.AppendString(o, z.ResetID)
+ // string "ResetBeforeDate"
+ o = append(o, 0xaf, 0x52, 0x65, 0x73, 0x65, 0x74, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x44, 0x61, 0x74, 0x65)
+ o = msgp.AppendTime(o, z.ResetBeforeDate)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ResyncTargetDecision) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Replicate":
+ z.Replicate, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Replicate")
+ return
+ }
+ case "ResetID":
+ z.ResetID, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ResetID")
+ return
+ }
+ case "ResetBeforeDate":
+ z.ResetBeforeDate, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ResetBeforeDate")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z ResyncTargetDecision) Msgsize() (s int) {
+ s = 1 + 10 + msgp.BoolSize + 8 + msgp.StringPrefixSize + len(z.ResetID) + 16 + msgp.TimeSize
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *ResyncTargetsInfo) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Targets":
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Targets")
+ return
+ }
+ if cap(z.Targets) >= int(zb0002) {
+ z.Targets = (z.Targets)[:zb0002]
+ } else {
+ z.Targets = make([]ResyncTarget, zb0002)
+ }
+ for za0001 := range z.Targets {
+ err = z.Targets[za0001].DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Targets", za0001)
+ return
+ }
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *ResyncTargetsInfo) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 1
+ // write "Targets"
+ err = en.Append(0x81, 0xa7, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(len(z.Targets)))
+ if err != nil {
+ err = msgp.WrapError(err, "Targets")
+ return
+ }
+ for za0001 := range z.Targets {
+ err = z.Targets[za0001].EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Targets", za0001)
+ return
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *ResyncTargetsInfo) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 1
+ // string "Targets"
+ o = append(o, 0x81, 0xa7, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.Targets)))
+ for za0001 := range z.Targets {
+ o, err = z.Targets[za0001].MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Targets", za0001)
+ return
+ }
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ResyncTargetsInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Targets":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Targets")
+ return
+ }
+ if cap(z.Targets) >= int(zb0002) {
+ z.Targets = (z.Targets)[:zb0002]
+ } else {
+ z.Targets = make([]ResyncTarget, zb0002)
+ }
+ for za0001 := range z.Targets {
+ bts, err = z.Targets[za0001].UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Targets", za0001)
+ return
+ }
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *ResyncTargetsInfo) Msgsize() (s int) {
+ s = 1 + 8 + msgp.ArrayHeaderSize
+ for za0001 := range z.Targets {
+ s += z.Targets[za0001].Msgsize()
+ }
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *TargetReplicationResyncStatus) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "st":
+ z.StartTime, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "StartTime")
+ return
+ }
+ case "lst":
+ z.LastUpdate, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "LastUpdate")
+ return
+ }
+ case "id":
+ z.ResyncID, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "ResyncID")
+ return
+ }
+ case "rdt":
+ z.ResyncBeforeDate, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "ResyncBeforeDate")
+ return
+ }
+ case "rst":
+ {
+ var zb0002 int
+ zb0002, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "ResyncStatus")
+ return
+ }
+ z.ResyncStatus = ResyncStatusType(zb0002)
+ }
+ case "fs":
+ z.FailedSize, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "FailedSize")
+ return
+ }
+ case "frc":
+ z.FailedCount, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "FailedCount")
+ return
+ }
+ case "rs":
+ z.ReplicatedSize, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedSize")
+ return
+ }
+ case "rrc":
+ z.ReplicatedCount, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedCount")
+ return
+ }
+ case "bkt":
+ z.Bucket, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "obj":
+ z.Object, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *TargetReplicationResyncStatus) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 11
+ // write "st"
+ err = en.Append(0x8b, 0xa2, 0x73, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.StartTime)
+ if err != nil {
+ err = msgp.WrapError(err, "StartTime")
+ return
+ }
+ // write "lst"
+ err = en.Append(0xa3, 0x6c, 0x73, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.LastUpdate)
+ if err != nil {
+ err = msgp.WrapError(err, "LastUpdate")
+ return
+ }
+ // write "id"
+ err = en.Append(0xa2, 0x69, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.ResyncID)
+ if err != nil {
+ err = msgp.WrapError(err, "ResyncID")
+ return
+ }
+ // write "rdt"
+ err = en.Append(0xa3, 0x72, 0x64, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.ResyncBeforeDate)
+ if err != nil {
+ err = msgp.WrapError(err, "ResyncBeforeDate")
+ return
+ }
+ // write "rst"
+ err = en.Append(0xa3, 0x72, 0x73, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(int(z.ResyncStatus))
+ if err != nil {
+ err = msgp.WrapError(err, "ResyncStatus")
+ return
+ }
+ // write "fs"
+ err = en.Append(0xa2, 0x66, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.FailedSize)
+ if err != nil {
+ err = msgp.WrapError(err, "FailedSize")
+ return
+ }
+ // write "frc"
+ err = en.Append(0xa3, 0x66, 0x72, 0x63)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.FailedCount)
+ if err != nil {
+ err = msgp.WrapError(err, "FailedCount")
+ return
+ }
+ // write "rs"
+ err = en.Append(0xa2, 0x72, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.ReplicatedSize)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedSize")
+ return
+ }
+ // write "rrc"
+ err = en.Append(0xa3, 0x72, 0x72, 0x63)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.ReplicatedCount)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedCount")
+ return
+ }
+ // write "bkt"
+ err = en.Append(0xa3, 0x62, 0x6b, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Bucket)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ // write "obj"
+ err = en.Append(0xa3, 0x6f, 0x62, 0x6a)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Object)
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *TargetReplicationResyncStatus) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 11
+ // string "st"
+ o = append(o, 0x8b, 0xa2, 0x73, 0x74)
+ o = msgp.AppendTime(o, z.StartTime)
+ // string "lst"
+ o = append(o, 0xa3, 0x6c, 0x73, 0x74)
+ o = msgp.AppendTime(o, z.LastUpdate)
+ // string "id"
+ o = append(o, 0xa2, 0x69, 0x64)
+ o = msgp.AppendString(o, z.ResyncID)
+ // string "rdt"
+ o = append(o, 0xa3, 0x72, 0x64, 0x74)
+ o = msgp.AppendTime(o, z.ResyncBeforeDate)
+ // string "rst"
+ o = append(o, 0xa3, 0x72, 0x73, 0x74)
+ o = msgp.AppendInt(o, int(z.ResyncStatus))
+ // string "fs"
+ o = append(o, 0xa2, 0x66, 0x73)
+ o = msgp.AppendInt64(o, z.FailedSize)
+ // string "frc"
+ o = append(o, 0xa3, 0x66, 0x72, 0x63)
+ o = msgp.AppendInt64(o, z.FailedCount)
+ // string "rs"
+ o = append(o, 0xa2, 0x72, 0x73)
+ o = msgp.AppendInt64(o, z.ReplicatedSize)
+ // string "rrc"
+ o = append(o, 0xa3, 0x72, 0x72, 0x63)
+ o = msgp.AppendInt64(o, z.ReplicatedCount)
+ // string "bkt"
+ o = append(o, 0xa3, 0x62, 0x6b, 0x74)
+ o = msgp.AppendString(o, z.Bucket)
+ // string "obj"
+ o = append(o, 0xa3, 0x6f, 0x62, 0x6a)
+ o = msgp.AppendString(o, z.Object)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *TargetReplicationResyncStatus) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "st":
+ z.StartTime, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "StartTime")
+ return
+ }
+ case "lst":
+ z.LastUpdate, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "LastUpdate")
+ return
+ }
+ case "id":
+ z.ResyncID, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ResyncID")
+ return
+ }
+ case "rdt":
+ z.ResyncBeforeDate, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ResyncBeforeDate")
+ return
+ }
+ case "rst":
+ {
+ var zb0002 int
+ zb0002, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ResyncStatus")
+ return
+ }
+ z.ResyncStatus = ResyncStatusType(zb0002)
+ }
+ case "fs":
+ z.FailedSize, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "FailedSize")
+ return
+ }
+ case "frc":
+ z.FailedCount, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "FailedCount")
+ return
+ }
+ case "rs":
+ z.ReplicatedSize, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedSize")
+ return
+ }
+ case "rrc":
+ z.ReplicatedCount, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedCount")
+ return
+ }
+ case "bkt":
+ z.Bucket, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "obj":
+ z.Object, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *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)
+ return
+}
diff --git a/cmd/bucket-replication-utils_gen_test.go b/cmd/bucket-replication-utils_gen_test.go
new file mode 100644
index 00000000..ec7dd41c
--- /dev/null
+++ b/cmd/bucket-replication-utils_gen_test.go
@@ -0,0 +1,1140 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+func TestMarshalUnmarshalBucketReplicationResyncStatus(t *testing.T) {
+ v := BucketReplicationResyncStatus{}
+ 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 BenchmarkMarshalMsgBucketReplicationResyncStatus(b *testing.B) {
+ v := BucketReplicationResyncStatus{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBucketReplicationResyncStatus(b *testing.B) {
+ v := BucketReplicationResyncStatus{}
+ 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 BenchmarkUnmarshalBucketReplicationResyncStatus(b *testing.B) {
+ v := BucketReplicationResyncStatus{}
+ 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 TestEncodeDecodeBucketReplicationResyncStatus(t *testing.T) {
+ v := BucketReplicationResyncStatus{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBucketReplicationResyncStatus Msgsize() is inaccurate")
+ }
+
+ vn := BucketReplicationResyncStatus{}
+ 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 BenchmarkEncodeBucketReplicationResyncStatus(b *testing.B) {
+ v := BucketReplicationResyncStatus{}
+ 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 BenchmarkDecodeBucketReplicationResyncStatus(b *testing.B) {
+ v := BucketReplicationResyncStatus{}
+ 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 TestMarshalUnmarshalMRFReplicateEntries(t *testing.T) {
+ v := MRFReplicateEntries{}
+ 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 BenchmarkMarshalMsgMRFReplicateEntries(b *testing.B) {
+ v := MRFReplicateEntries{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgMRFReplicateEntries(b *testing.B) {
+ v := MRFReplicateEntries{}
+ 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 BenchmarkUnmarshalMRFReplicateEntries(b *testing.B) {
+ v := MRFReplicateEntries{}
+ 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 TestEncodeDecodeMRFReplicateEntries(t *testing.T) {
+ v := MRFReplicateEntries{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeMRFReplicateEntries Msgsize() is inaccurate")
+ }
+
+ vn := MRFReplicateEntries{}
+ 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 BenchmarkEncodeMRFReplicateEntries(b *testing.B) {
+ v := MRFReplicateEntries{}
+ 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 BenchmarkDecodeMRFReplicateEntries(b *testing.B) {
+ v := MRFReplicateEntries{}
+ 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 TestMarshalUnmarshalMRFReplicateEntry(t *testing.T) {
+ v := MRFReplicateEntry{}
+ 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 BenchmarkMarshalMsgMRFReplicateEntry(b *testing.B) {
+ v := MRFReplicateEntry{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgMRFReplicateEntry(b *testing.B) {
+ v := MRFReplicateEntry{}
+ 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 BenchmarkUnmarshalMRFReplicateEntry(b *testing.B) {
+ v := MRFReplicateEntry{}
+ 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 TestEncodeDecodeMRFReplicateEntry(t *testing.T) {
+ v := MRFReplicateEntry{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeMRFReplicateEntry Msgsize() is inaccurate")
+ }
+
+ vn := MRFReplicateEntry{}
+ 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 BenchmarkEncodeMRFReplicateEntry(b *testing.B) {
+ v := MRFReplicateEntry{}
+ 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 BenchmarkDecodeMRFReplicateEntry(b *testing.B) {
+ v := MRFReplicateEntry{}
+ 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 TestMarshalUnmarshalReplicateDecision(t *testing.T) {
+ v := ReplicateDecision{}
+ 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 BenchmarkMarshalMsgReplicateDecision(b *testing.B) {
+ v := ReplicateDecision{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgReplicateDecision(b *testing.B) {
+ v := ReplicateDecision{}
+ 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 BenchmarkUnmarshalReplicateDecision(b *testing.B) {
+ v := ReplicateDecision{}
+ 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 TestEncodeDecodeReplicateDecision(t *testing.T) {
+ v := ReplicateDecision{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeReplicateDecision Msgsize() is inaccurate")
+ }
+
+ vn := ReplicateDecision{}
+ 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 BenchmarkEncodeReplicateDecision(b *testing.B) {
+ v := ReplicateDecision{}
+ 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 BenchmarkDecodeReplicateDecision(b *testing.B) {
+ v := ReplicateDecision{}
+ 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 TestMarshalUnmarshalReplicationState(t *testing.T) {
+ v := ReplicationState{}
+ 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 BenchmarkMarshalMsgReplicationState(b *testing.B) {
+ v := ReplicationState{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgReplicationState(b *testing.B) {
+ v := ReplicationState{}
+ 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 BenchmarkUnmarshalReplicationState(b *testing.B) {
+ v := ReplicationState{}
+ 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 TestEncodeDecodeReplicationState(t *testing.T) {
+ v := ReplicationState{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeReplicationState Msgsize() is inaccurate")
+ }
+
+ vn := ReplicationState{}
+ 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 BenchmarkEncodeReplicationState(b *testing.B) {
+ v := ReplicationState{}
+ 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 BenchmarkDecodeReplicationState(b *testing.B) {
+ v := ReplicationState{}
+ 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 TestMarshalUnmarshalResyncDecision(t *testing.T) {
+ v := ResyncDecision{}
+ 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 BenchmarkMarshalMsgResyncDecision(b *testing.B) {
+ v := ResyncDecision{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgResyncDecision(b *testing.B) {
+ v := ResyncDecision{}
+ 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 BenchmarkUnmarshalResyncDecision(b *testing.B) {
+ v := ResyncDecision{}
+ 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 TestEncodeDecodeResyncDecision(t *testing.T) {
+ v := ResyncDecision{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeResyncDecision Msgsize() is inaccurate")
+ }
+
+ vn := ResyncDecision{}
+ 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 BenchmarkEncodeResyncDecision(b *testing.B) {
+ v := ResyncDecision{}
+ 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 BenchmarkDecodeResyncDecision(b *testing.B) {
+ v := ResyncDecision{}
+ 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 TestMarshalUnmarshalResyncTarget(t *testing.T) {
+ v := ResyncTarget{}
+ 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 BenchmarkMarshalMsgResyncTarget(b *testing.B) {
+ v := ResyncTarget{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgResyncTarget(b *testing.B) {
+ v := ResyncTarget{}
+ 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 BenchmarkUnmarshalResyncTarget(b *testing.B) {
+ v := ResyncTarget{}
+ 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 TestEncodeDecodeResyncTarget(t *testing.T) {
+ v := ResyncTarget{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeResyncTarget Msgsize() is inaccurate")
+ }
+
+ vn := ResyncTarget{}
+ 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 BenchmarkEncodeResyncTarget(b *testing.B) {
+ v := ResyncTarget{}
+ 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 BenchmarkDecodeResyncTarget(b *testing.B) {
+ v := ResyncTarget{}
+ 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 TestMarshalUnmarshalResyncTargetDecision(t *testing.T) {
+ v := ResyncTargetDecision{}
+ 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 BenchmarkMarshalMsgResyncTargetDecision(b *testing.B) {
+ v := ResyncTargetDecision{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgResyncTargetDecision(b *testing.B) {
+ v := ResyncTargetDecision{}
+ 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 BenchmarkUnmarshalResyncTargetDecision(b *testing.B) {
+ v := ResyncTargetDecision{}
+ 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 TestEncodeDecodeResyncTargetDecision(t *testing.T) {
+ v := ResyncTargetDecision{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeResyncTargetDecision Msgsize() is inaccurate")
+ }
+
+ vn := ResyncTargetDecision{}
+ 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 BenchmarkEncodeResyncTargetDecision(b *testing.B) {
+ v := ResyncTargetDecision{}
+ 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 BenchmarkDecodeResyncTargetDecision(b *testing.B) {
+ v := ResyncTargetDecision{}
+ 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 TestMarshalUnmarshalResyncTargetsInfo(t *testing.T) {
+ v := ResyncTargetsInfo{}
+ 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 BenchmarkMarshalMsgResyncTargetsInfo(b *testing.B) {
+ v := ResyncTargetsInfo{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgResyncTargetsInfo(b *testing.B) {
+ v := ResyncTargetsInfo{}
+ 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 BenchmarkUnmarshalResyncTargetsInfo(b *testing.B) {
+ v := ResyncTargetsInfo{}
+ 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 TestEncodeDecodeResyncTargetsInfo(t *testing.T) {
+ v := ResyncTargetsInfo{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeResyncTargetsInfo Msgsize() is inaccurate")
+ }
+
+ vn := ResyncTargetsInfo{}
+ 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 BenchmarkEncodeResyncTargetsInfo(b *testing.B) {
+ v := ResyncTargetsInfo{}
+ 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 BenchmarkDecodeResyncTargetsInfo(b *testing.B) {
+ v := ResyncTargetsInfo{}
+ 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 TestMarshalUnmarshalTargetReplicationResyncStatus(t *testing.T) {
+ v := TargetReplicationResyncStatus{}
+ 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 BenchmarkMarshalMsgTargetReplicationResyncStatus(b *testing.B) {
+ v := TargetReplicationResyncStatus{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgTargetReplicationResyncStatus(b *testing.B) {
+ v := TargetReplicationResyncStatus{}
+ 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 BenchmarkUnmarshalTargetReplicationResyncStatus(b *testing.B) {
+ v := TargetReplicationResyncStatus{}
+ 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 TestEncodeDecodeTargetReplicationResyncStatus(t *testing.T) {
+ v := TargetReplicationResyncStatus{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeTargetReplicationResyncStatus Msgsize() is inaccurate")
+ }
+
+ vn := TargetReplicationResyncStatus{}
+ 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 BenchmarkEncodeTargetReplicationResyncStatus(b *testing.B) {
+ v := TargetReplicationResyncStatus{}
+ 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 BenchmarkDecodeTargetReplicationResyncStatus(b *testing.B) {
+ v := TargetReplicationResyncStatus{}
+ 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)
+ }
+ }
+}
diff --git a/cmd/bucket-replication-utils_test.go b/cmd/bucket-replication-utils_test.go
new file mode 100644
index 00000000..19c1b4b1
--- /dev/null
+++ b/cmd/bucket-replication-utils_test.go
@@ -0,0 +1,247 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "testing"
+
+ "github.com/minio/minio/internal/bucket/replication"
+)
+
+var replicatedInfosTests = []struct {
+ name string
+ tgtInfos []replicatedTargetInfo
+ expectedCompletedSize int64
+ expectedReplicationStatusInternal string
+ expectedReplicationStatus replication.StatusType
+ expectedOpType replication.Type
+ expectedAction replicationAction
+}{
+ { // 1. empty tgtInfos slice
+ name: "no replicated targets",
+ tgtInfos: []replicatedTargetInfo{},
+ expectedCompletedSize: 0,
+ expectedReplicationStatusInternal: "",
+ expectedReplicationStatus: replication.StatusType(""),
+ expectedOpType: replication.UnsetReplicationType,
+ expectedAction: replicateNone,
+ },
+ { // 2. replication completed to single target
+ name: "replication completed to single target",
+ tgtInfos: []replicatedTargetInfo{
+ {
+ Arn: "arn1",
+ Size: 249,
+ PrevReplicationStatus: replication.Pending,
+ ReplicationStatus: replication.Completed,
+ OpType: replication.ObjectReplicationType,
+ ReplicationAction: replicateAll,
+ },
+ },
+ expectedCompletedSize: 249,
+ expectedReplicationStatusInternal: "arn1=COMPLETED;",
+ expectedReplicationStatus: replication.Completed,
+ expectedOpType: replication.ObjectReplicationType,
+ expectedAction: replicateAll,
+ },
+ { // 3. replication completed to single target; failed to another
+ name: "replication completed to single target",
+ tgtInfos: []replicatedTargetInfo{
+ {
+ Arn: "arn1",
+ Size: 249,
+ PrevReplicationStatus: replication.Pending,
+ ReplicationStatus: replication.Completed,
+ OpType: replication.ObjectReplicationType,
+ ReplicationAction: replicateAll,
+ },
+ {
+ Arn: "arn2",
+ Size: 249,
+ PrevReplicationStatus: replication.Pending,
+ ReplicationStatus: replication.Failed,
+ OpType: replication.ObjectReplicationType,
+ ReplicationAction: replicateAll,
+ },
+ },
+ expectedCompletedSize: 249,
+ expectedReplicationStatusInternal: "arn1=COMPLETED;arn2=FAILED;",
+ expectedReplicationStatus: replication.Failed,
+ expectedOpType: replication.ObjectReplicationType,
+ expectedAction: replicateAll,
+ },
+ { // 4. replication pending on one target; failed to another
+ name: "replication completed to single target",
+ tgtInfos: []replicatedTargetInfo{
+ {
+ Arn: "arn1",
+ Size: 249,
+ PrevReplicationStatus: replication.Pending,
+ ReplicationStatus: replication.Pending,
+ OpType: replication.ObjectReplicationType,
+ ReplicationAction: replicateAll,
+ },
+ {
+ Arn: "arn2",
+ Size: 249,
+ PrevReplicationStatus: replication.Pending,
+ ReplicationStatus: replication.Failed,
+ OpType: replication.ObjectReplicationType,
+ ReplicationAction: replicateAll,
+ },
+ },
+ expectedCompletedSize: 0,
+ expectedReplicationStatusInternal: "arn1=PENDING;arn2=FAILED;",
+ expectedReplicationStatus: replication.Failed,
+ expectedOpType: replication.ObjectReplicationType,
+ expectedAction: replicateAll,
+ },
+}
+
+func TestReplicatedInfos(t *testing.T) {
+ for i, test := range replicatedInfosTests {
+ rinfos := replicatedInfos{
+ Targets: test.tgtInfos,
+ }
+ if actualSize := rinfos.CompletedSize(); actualSize != test.expectedCompletedSize {
+ t.Errorf("Test%d (%s): Size got %d , want %d", i+1, test.name, actualSize, test.expectedCompletedSize)
+ }
+ if repStatusStr := rinfos.ReplicationStatusInternal(); repStatusStr != test.expectedReplicationStatusInternal {
+ t.Errorf("Test%d (%s): Internal replication status got %s , want %s", i+1, test.name, repStatusStr, test.expectedReplicationStatusInternal)
+ }
+ if repStatus := rinfos.ReplicationStatus(); repStatus != test.expectedReplicationStatus {
+ t.Errorf("Test%d (%s): ReplicationStatus got %s , want %s", i+1, test.name, repStatus, test.expectedReplicationStatus)
+ }
+ if action := rinfos.Action(); action != test.expectedAction {
+ t.Errorf("Test%d (%s): Action got %s , want %s", i+1, test.name, action, test.expectedAction)
+ }
+ }
+}
+
+var parseReplicationDecisionTest = []struct {
+ name string
+ dsc string
+ expDsc ReplicateDecision
+ expErr error
+}{
+ { // 1.
+ name: "empty string",
+ dsc: "",
+ expDsc: ReplicateDecision{
+ targetsMap: map[string]replicateTargetDecision{},
+ },
+ expErr: nil,
+ },
+
+ { // 2.
+ name: "replicate decision for one target",
+ dsc: "arn:minio:replication::id:bucket=true;false;arn:minio:replication::id:bucket;id",
+ expErr: nil,
+ expDsc: ReplicateDecision{
+ targetsMap: map[string]replicateTargetDecision{
+ "arn:minio:replication::id:bucket": newReplicateTargetDecision("arn:minio:replication::id:bucket", true, false),
+ },
+ },
+ },
+ { // 3.
+ name: "replicate decision for multiple targets",
+ dsc: "arn:minio:replication::id:bucket=true;false;arn:minio:replication::id:bucket;id,arn:minio:replication::id2:bucket=false;true;arn:minio:replication::id2:bucket;id2",
+ expErr: nil,
+ expDsc: ReplicateDecision{
+ targetsMap: map[string]replicateTargetDecision{
+ "arn:minio:replication::id:bucket": newReplicateTargetDecision("arn:minio:replication::id:bucket", true, false),
+ "arn:minio:replication::id2:bucket": newReplicateTargetDecision("arn:minio:replication::id2:bucket", false, true),
+ },
+ },
+ },
+ { // 4.
+ name: "invalid format replicate decision for one target",
+ dsc: "arn:minio:replication::id:bucket:true;false;arn:minio:replication::id:bucket;id",
+ expErr: errInvalidReplicateDecisionFormat,
+ expDsc: ReplicateDecision{
+ targetsMap: map[string]replicateTargetDecision{
+ "arn:minio:replication::id:bucket": newReplicateTargetDecision("arn:minio:replication::id:bucket", true, false),
+ },
+ },
+ },
+}
+
+func TestParseReplicateDecision(t *testing.T) {
+ for i, test := range parseReplicationDecisionTest {
+ dsc, err := parseReplicateDecision(t.Context(), "bucket", test.expDsc.String())
+ if err != nil {
+ if test.expErr != err {
+ t.Errorf("Test%d (%s): Expected parse error got %t , want %t", i+1, test.name, err, test.expErr)
+ }
+ continue
+ }
+ if len(dsc.targetsMap) != len(test.expDsc.targetsMap) {
+ t.Errorf("Test%d (%s): Invalid number of entries in targetsMap got %d , want %d", i+1, test.name, len(dsc.targetsMap), len(test.expDsc.targetsMap))
+ }
+ for arn, tdsc := range dsc.targetsMap {
+ expDsc, ok := test.expDsc.targetsMap[arn]
+ if !ok || expDsc != tdsc {
+ t.Errorf("Test%d (%s): Invalid target replicate decision: got %+v, want %+v", i+1, test.name, tdsc, expDsc)
+ }
+ }
+ }
+}
+
+var replicationStateTest = []struct {
+ name string
+ rs ReplicationState
+ arn string
+ expStatus replication.StatusType
+}{
+ { // 1. no replication status header
+ name: "no replicated targets",
+ rs: ReplicationState{},
+ expStatus: replication.StatusType(""),
+ },
+ { // 2. replication status for one target
+ name: "replication status for one target",
+ rs: ReplicationState{ReplicationStatusInternal: "arn1=PENDING;", Targets: map[string]replication.StatusType{"arn1": "PENDING"}},
+ expStatus: replication.Pending,
+ },
+ { // 3. replication status for one target - incorrect format
+ name: "replication status for one target",
+ rs: ReplicationState{ReplicationStatusInternal: "arn1=PENDING"},
+ expStatus: replication.StatusType(""),
+ },
+ { // 4. replication status for 3 targets, one of them failed
+ name: "replication status for 3 targets - one failed",
+ rs: ReplicationState{
+ ReplicationStatusInternal: "arn1=COMPLETED;arn2=COMPLETED;arn3=FAILED;",
+ Targets: map[string]replication.StatusType{"arn1": "COMPLETED", "arn2": "COMPLETED", "arn3": "FAILED"},
+ },
+ expStatus: replication.Failed,
+ },
+ { // 5. replication status for replica version
+ name: "replication status for replica version",
+ rs: ReplicationState{ReplicationStatusInternal: string(replication.Replica)},
+ expStatus: replication.Replica,
+ },
+}
+
+func TestCompositeReplicationStatus(t *testing.T) {
+ for i, test := range replicationStateTest {
+ if rstatus := test.rs.CompositeReplicationStatus(); rstatus != test.expStatus {
+ t.Errorf("Test%d (%s): Overall replication status got %s , want %s", i+1, test.name, rstatus, test.expStatus)
+ }
+ }
+}
diff --git a/cmd/bucket-replication.go b/cmd/bucket-replication.go
new file mode 100644
index 00000000..5f6268cb
--- /dev/null
+++ b/cmd/bucket-replication.go
@@ -0,0 +1,3803 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/base64"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "maps"
+ "math/rand"
+ "net/http"
+ "net/url"
+ "path"
+ "reflect"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/dustin/go-humanize"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio-go/v7"
+ "github.com/minio/minio-go/v7/pkg/encrypt"
+ "github.com/minio/minio-go/v7/pkg/tags"
+ "github.com/minio/minio/internal/amztime"
+ "github.com/minio/minio/internal/bucket/bandwidth"
+ objectlock "github.com/minio/minio/internal/bucket/object/lock"
+ "github.com/minio/minio/internal/bucket/replication"
+ "github.com/minio/minio/internal/config/storageclass"
+ "github.com/minio/minio/internal/crypto"
+ "github.com/minio/minio/internal/event"
+ "github.com/minio/minio/internal/hash"
+ xhttp "github.com/minio/minio/internal/http"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/minio/internal/kms"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/minio/internal/once"
+ "github.com/tinylib/msgp/msgp"
+ "github.com/zeebo/xxh3"
+)
+
+const (
+ throttleDeadline = 1 * time.Hour
+ // ReplicationReset has reset id and timestamp of last reset operation
+ ReplicationReset = "replication-reset"
+ // ReplicationStatus has internal replication status - stringified representation of target's replication status for all replication
+ // activity initiated from this cluster
+ ReplicationStatus = "replication-status"
+ // ReplicationTimestamp - the last time replication was initiated on this cluster for this object version
+ ReplicationTimestamp = "replication-timestamp"
+ // ReplicaStatus - this header is present if a replica was received by this cluster for this object version
+ ReplicaStatus = "replica-status"
+ // ReplicaTimestamp - the last time a replica was received by this cluster for this object version
+ ReplicaTimestamp = "replica-timestamp"
+ // TaggingTimestamp - the last time a tag metadata modification happened on this cluster for this object version
+ TaggingTimestamp = "tagging-timestamp"
+ // ObjectLockRetentionTimestamp - the last time a object lock metadata modification happened on this cluster for this object version
+ ObjectLockRetentionTimestamp = "objectlock-retention-timestamp"
+ // ObjectLockLegalHoldTimestamp - the last time a legal hold metadata modification happened on this cluster for this object version
+ ObjectLockLegalHoldTimestamp = "objectlock-legalhold-timestamp"
+
+ // ReplicationSsecChecksumHeader - the encrypted checksum of the SSE-C encrypted object.
+ ReplicationSsecChecksumHeader = "X-Minio-Replication-Ssec-Crc"
+)
+
+// gets replication config associated to a given bucket name.
+func getReplicationConfig(ctx context.Context, bucketName string) (rc *replication.Config, err error) {
+ rCfg, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucketName)
+ if err != nil && !errors.Is(err, BucketReplicationConfigNotFound{Bucket: bucketName}) {
+ return rCfg, err
+ }
+ return rCfg, nil
+}
+
+// validateReplicationDestination returns error if replication destination bucket missing or not configured
+// It also returns true if replication destination is same as this server.
+func validateReplicationDestination(ctx context.Context, bucket string, rCfg *replication.Config, opts *validateReplicationDestinationOptions) (bool, APIError) {
+ if opts == nil {
+ opts = &validateReplicationDestinationOptions{}
+ }
+ var arns []string
+ if rCfg.RoleArn != "" {
+ arns = append(arns, rCfg.RoleArn)
+ } else {
+ for _, rule := range rCfg.Rules {
+ arns = append(arns, rule.Destination.String())
+ }
+ }
+ var sameTarget bool
+ for _, arnStr := range arns {
+ arn, err := madmin.ParseARN(arnStr)
+ if err != nil {
+ return sameTarget, errorCodes.ToAPIErrWithErr(ErrBucketRemoteArnInvalid, err)
+ }
+ if arn.Type != madmin.ReplicationService {
+ return sameTarget, toAPIError(ctx, BucketRemoteArnTypeInvalid{Bucket: bucket})
+ }
+ clnt := globalBucketTargetSys.GetRemoteTargetClient(bucket, arnStr)
+ if clnt == nil {
+ return sameTarget, toAPIError(ctx, BucketRemoteTargetNotFound{Bucket: bucket})
+ }
+ if opts.CheckRemoteBucket { // validate remote bucket
+ found, err := clnt.BucketExists(ctx, arn.Bucket)
+ if err != nil {
+ return sameTarget, errorCodes.ToAPIErrWithErr(ErrRemoteDestinationNotFoundError, err)
+ }
+ if !found {
+ return sameTarget, errorCodes.ToAPIErrWithErr(ErrRemoteDestinationNotFoundError, BucketRemoteTargetNotFound{Bucket: arn.Bucket})
+ }
+ if ret, err := globalBucketObjectLockSys.Get(bucket); err == nil {
+ if ret.LockEnabled {
+ lock, _, _, _, err := clnt.GetObjectLockConfig(ctx, arn.Bucket)
+ if err != nil {
+ return sameTarget, errorCodes.ToAPIErrWithErr(ErrReplicationDestinationMissingLock, err)
+ }
+ if lock != objectlock.Enabled {
+ return sameTarget, errorCodes.ToAPIErrWithErr(ErrReplicationDestinationMissingLock, nil)
+ }
+ }
+ }
+ }
+ // if checked bucket, then check the ready is unnecessary
+ if !opts.CheckRemoteBucket && opts.CheckReady {
+ endpoint := clnt.EndpointURL().String()
+ if errInt, ok := opts.checkReadyErr.Load(endpoint); !ok {
+ err = checkRemoteEndpoint(ctx, clnt.EndpointURL())
+ opts.checkReadyErr.Store(endpoint, err)
+ } else {
+ if errInt == nil {
+ err = nil
+ } else {
+ err, _ = errInt.(error)
+ }
+ }
+ switch err.(type) {
+ case BucketRemoteIdenticalToSource:
+ return true, errorCodes.ToAPIErrWithErr(ErrBucketRemoteIdenticalToSource, fmt.Errorf("remote target endpoint %s is self referential", clnt.EndpointURL().String()))
+ default:
+ }
+ }
+ // validate replication ARN against target endpoint
+ selfTarget, _ := isLocalHost(clnt.EndpointURL().Hostname(), clnt.EndpointURL().Port(), globalMinioPort)
+ if !sameTarget {
+ sameTarget = selfTarget
+ }
+ }
+
+ if len(arns) == 0 {
+ return false, toAPIError(ctx, BucketRemoteTargetNotFound{Bucket: bucket})
+ }
+ return sameTarget, toAPIError(ctx, nil)
+}
+
+// performs a http request to remote endpoint to check if deployment id of remote endpoint is same as
+// local cluster deployment id. This is to prevent replication to self, especially in case of a loadbalancer
+// in front of MinIO.
+func checkRemoteEndpoint(ctx context.Context, epURL *url.URL) error {
+ reqURL := &url.URL{
+ Scheme: epURL.Scheme,
+ Host: epURL.Host,
+ Path: healthCheckPathPrefix + healthCheckReadinessPath,
+ }
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL.String(), nil)
+ if err != nil {
+ return err
+ }
+
+ client := &http.Client{
+ Transport: globalRemoteTargetTransport,
+ Timeout: 10 * time.Second,
+ }
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+ if err == nil {
+ // Drain the connection.
+ xhttp.DrainBody(resp.Body)
+ }
+ if resp != nil {
+ amzid := resp.Header.Get(xhttp.AmzRequestHostID)
+ if _, ok := globalNodeNamesHex[amzid]; ok {
+ return BucketRemoteIdenticalToSource{
+ Endpoint: epURL.String(),
+ }
+ }
+ }
+ return nil
+}
+
+type mustReplicateOptions struct {
+ meta map[string]string
+ status replication.StatusType
+ opType replication.Type
+ replicationRequest bool // incoming request is a replication request
+}
+
+func (o mustReplicateOptions) ReplicationStatus() (s replication.StatusType) {
+ if rs, ok := o.meta[xhttp.AmzBucketReplicationStatus]; ok {
+ return replication.StatusType(rs)
+ }
+ return s
+}
+
+func (o mustReplicateOptions) isExistingObjectReplication() bool {
+ return o.opType == replication.ExistingObjectReplicationType
+}
+
+func (o mustReplicateOptions) isMetadataReplication() bool {
+ return o.opType == replication.MetadataReplicationType
+}
+
+func (o ObjectInfo) getMustReplicateOptions(op replication.Type, opts ObjectOptions) mustReplicateOptions {
+ return getMustReplicateOptions(o.UserDefined, o.UserTags, o.ReplicationStatus, op, opts)
+}
+
+func getMustReplicateOptions(userDefined map[string]string, userTags string, status replication.StatusType, op replication.Type, opts ObjectOptions) mustReplicateOptions {
+ meta := cloneMSS(userDefined)
+ if userTags != "" {
+ meta[xhttp.AmzObjectTagging] = userTags
+ }
+
+ return mustReplicateOptions{
+ meta: meta,
+ status: status,
+ opType: op,
+ replicationRequest: opts.ReplicationRequest,
+ }
+}
+
+// mustReplicate returns 2 booleans - true if object meets replication criteria and true if replication is to be done in
+// a synchronous manner.
+func mustReplicate(ctx context.Context, bucket, object string, mopts mustReplicateOptions) (dsc ReplicateDecision) {
+ // object layer not initialized we return with no decision.
+ if newObjectLayerFn() == nil {
+ return dsc
+ }
+
+ // Disable server-side replication on object prefixes which are excluded
+ // from versioning via the MinIO bucket versioning extension.
+ if !globalBucketVersioningSys.PrefixEnabled(bucket, object) {
+ return dsc
+ }
+
+ replStatus := mopts.ReplicationStatus()
+ if replStatus == replication.Replica && !mopts.isMetadataReplication() {
+ return dsc
+ }
+
+ if mopts.replicationRequest { // incoming replication request on target cluster
+ return dsc
+ }
+
+ cfg, err := getReplicationConfig(ctx, bucket)
+ if err != nil {
+ replLogOnceIf(ctx, err, bucket)
+ return dsc
+ }
+ if cfg == nil {
+ return dsc
+ }
+
+ opts := replication.ObjectOpts{
+ Name: object,
+ SSEC: crypto.SSEC.IsEncrypted(mopts.meta),
+ Replica: replStatus == replication.Replica,
+ ExistingObject: mopts.isExistingObjectReplication(),
+ }
+ tagStr, ok := mopts.meta[xhttp.AmzObjectTagging]
+ if ok {
+ opts.UserTags = tagStr
+ }
+ tgtArns := cfg.FilterTargetArns(opts)
+ for _, tgtArn := range tgtArns {
+ tgt := globalBucketTargetSys.GetRemoteTargetClient(bucket, tgtArn)
+ // the target online status should not be used here while deciding
+ // whether to replicate as the target could be temporarily down
+ opts.TargetArn = tgtArn
+ replicate := cfg.Replicate(opts)
+ var synchronous bool
+ if tgt != nil {
+ synchronous = tgt.replicateSync
+ }
+ dsc.Set(newReplicateTargetDecision(tgtArn, replicate, synchronous))
+ }
+ return dsc
+}
+
+// Standard headers that needs to be extracted from User metadata.
+var standardHeaders = []string{
+ xhttp.ContentType,
+ xhttp.CacheControl,
+ xhttp.ContentEncoding,
+ xhttp.ContentLanguage,
+ xhttp.ContentDisposition,
+ xhttp.AmzStorageClass,
+ xhttp.AmzObjectTagging,
+ xhttp.AmzBucketReplicationStatus,
+ xhttp.AmzObjectLockMode,
+ xhttp.AmzObjectLockRetainUntilDate,
+ xhttp.AmzObjectLockLegalHold,
+ xhttp.AmzTagCount,
+ xhttp.AmzServerSideEncryption,
+}
+
+// returns true if any of the objects being deleted qualifies for replication.
+func hasReplicationRules(ctx context.Context, bucket string, objects []ObjectToDelete) bool {
+ c, err := getReplicationConfig(ctx, bucket)
+ if err != nil || c == nil {
+ replLogOnceIf(ctx, err, bucket)
+ return false
+ }
+ for _, obj := range objects {
+ if c.HasActiveRules(obj.ObjectName, true) {
+ return true
+ }
+ }
+ return false
+}
+
+// isStandardHeader returns true if header is a supported header and not a custom header
+func isStandardHeader(matchHeaderKey string) bool {
+ return equals(matchHeaderKey, standardHeaders...)
+}
+
+// returns whether object version is a deletemarker and if object qualifies for replication
+func checkReplicateDelete(ctx context.Context, bucket string, dobj ObjectToDelete, oi ObjectInfo, delOpts ObjectOptions, gerr error) (dsc ReplicateDecision) {
+ rcfg, err := getReplicationConfig(ctx, bucket)
+ if err != nil || rcfg == nil {
+ replLogOnceIf(ctx, err, bucket)
+ return dsc
+ }
+ // If incoming request is a replication request, it does not need to be re-replicated.
+ if delOpts.ReplicationRequest {
+ return dsc
+ }
+ // Skip replication if this object's prefix is excluded from being
+ // versioned.
+ if !delOpts.Versioned {
+ return dsc
+ }
+ opts := replication.ObjectOpts{
+ Name: dobj.ObjectName,
+ SSEC: crypto.SSEC.IsEncrypted(oi.UserDefined),
+ UserTags: oi.UserTags,
+ DeleteMarker: oi.DeleteMarker,
+ VersionID: dobj.VersionID,
+ OpType: replication.DeleteReplicationType,
+ }
+ tgtArns := rcfg.FilterTargetArns(opts)
+ dsc.targetsMap = make(map[string]replicateTargetDecision, len(tgtArns))
+ if len(tgtArns) == 0 {
+ return dsc
+ }
+ var sync, replicate bool
+ for _, tgtArn := range tgtArns {
+ opts.TargetArn = tgtArn
+ replicate = rcfg.Replicate(opts)
+ // when incoming delete is removal of a delete marker(a.k.a versioned delete),
+ // GetObjectInfo returns extra information even though it returns errFileNotFound
+ if gerr != nil {
+ validReplStatus := false
+ switch oi.TargetReplicationStatus(tgtArn) {
+ case replication.Pending, replication.Completed, replication.Failed:
+ validReplStatus = true
+ }
+ if oi.DeleteMarker && (validReplStatus || replicate) {
+ dsc.Set(newReplicateTargetDecision(tgtArn, replicate, sync))
+ continue
+ }
+ // 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
+ if !oi.VersionPurgeStatus.Empty() {
+ replicate = oi.VersionPurgeStatus == replication.VersionPurgePending || oi.VersionPurgeStatus == replication.VersionPurgeFailed
+ dsc.Set(newReplicateTargetDecision(tgtArn, replicate, sync))
+ }
+ continue
+ }
+ tgt := globalBucketTargetSys.GetRemoteTargetClient(bucket, tgtArn)
+ // the target online status should not be used here while deciding
+ // whether to replicate deletes as the target could be temporarily down
+ tgtDsc := newReplicateTargetDecision(tgtArn, false, false)
+ if tgt != nil {
+ tgtDsc = newReplicateTargetDecision(tgtArn, replicate, tgt.replicateSync)
+ }
+ dsc.Set(tgtDsc)
+ }
+ return dsc
+}
+
+// replicate deletes to the designated replication target if replication configuration
+// has delete marker replication or delete replication (MinIO extension to allow deletes where version id
+// is specified) enabled.
+// Similar to bucket replication for PUT operation, soft delete (a.k.a setting delete marker) and
+// permanent deletes (by specifying a version ID in the delete operation) have three states "Pending", "Complete"
+// and "Failed" to mark the status of the replication of "DELETE" operation. All failed operations can
+// then be retried by healing. In the case of permanent deletes, until the replication is completed on the
+// target cluster, the object version is marked deleted on the source and hidden from listing. It is permanently
+// deleted from the source when the VersionPurgeStatus changes to "Complete", i.e after replication succeeds
+// on target.
+func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, objectAPI ObjectLayer) {
+ var replicationStatus replication.StatusType
+ bucket := dobj.Bucket
+ versionID := dobj.DeleteMarkerVersionID
+ if versionID == "" {
+ versionID = dobj.VersionID
+ }
+
+ defer func() {
+ replStatus := string(replicationStatus)
+ auditLogInternal(context.Background(), AuditLogOptions{
+ Event: dobj.EventType,
+ APIName: ReplicateDeleteAPI,
+ Bucket: bucket,
+ Object: dobj.ObjectName,
+ VersionID: versionID,
+ Status: replStatus,
+ })
+ }()
+
+ rcfg, err := getReplicationConfig(ctx, bucket)
+ if err != nil || rcfg == nil {
+ replLogOnceIf(ctx, fmt.Errorf("unable to obtain replication config for bucket: %s: err: %s", bucket, err), bucket)
+ sendEvent(eventArgs{
+ BucketName: bucket,
+ Object: ObjectInfo{
+ Bucket: bucket,
+ Name: dobj.ObjectName,
+ VersionID: versionID,
+ DeleteMarker: dobj.DeleteMarker,
+ },
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ EventName: event.ObjectReplicationNotTracked,
+ })
+ return
+ }
+ dsc, err := parseReplicateDecision(ctx, bucket, dobj.ReplicationState.ReplicateDecisionStr)
+ if err != nil {
+ replLogOnceIf(ctx, fmt.Errorf("unable to parse replication decision parameters for bucket: %s, err: %s, decision: %s",
+ bucket, err, dobj.ReplicationState.ReplicateDecisionStr), dobj.ReplicationState.ReplicateDecisionStr)
+ sendEvent(eventArgs{
+ BucketName: bucket,
+ Object: ObjectInfo{
+ Bucket: bucket,
+ Name: dobj.ObjectName,
+ VersionID: versionID,
+ DeleteMarker: dobj.DeleteMarker,
+ },
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ EventName: event.ObjectReplicationNotTracked,
+ })
+ return
+ }
+
+ // Lock the object name before starting replication operation.
+ // Use separate lock that doesn't collide with regular objects.
+ lk := objectAPI.NewNSLock(bucket, "/[replicate]/"+dobj.ObjectName)
+ lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ globalReplicationPool.Get().queueMRFSave(dobj.ToMRFEntry())
+ sendEvent(eventArgs{
+ BucketName: bucket,
+ Object: ObjectInfo{
+ Bucket: bucket,
+ Name: dobj.ObjectName,
+ VersionID: versionID,
+ DeleteMarker: dobj.DeleteMarker,
+ },
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ EventName: event.ObjectReplicationNotTracked,
+ })
+ return
+ }
+ ctx = lkctx.Context()
+ defer lk.Unlock(lkctx)
+
+ rinfos := replicatedInfos{Targets: make([]replicatedTargetInfo, 0, len(dsc.targetsMap))}
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+ for _, tgtEntry := range dsc.targetsMap {
+ if !tgtEntry.Replicate {
+ continue
+ }
+ // if dobj.TargetArn is not empty string, this is a case of specific target being re-synced.
+ if dobj.TargetArn != "" && dobj.TargetArn != tgtEntry.Arn {
+ continue
+ }
+ tgtClnt := globalBucketTargetSys.GetRemoteTargetClient(bucket, tgtEntry.Arn)
+ if tgtClnt == nil {
+ // Skip stale targets if any and log them to be missing at least once.
+ replLogOnceIf(ctx, fmt.Errorf("failed to get target for bucket:%s arn:%s", bucket, tgtEntry.Arn), tgtEntry.Arn)
+ sendEvent(eventArgs{
+ EventName: event.ObjectReplicationNotTracked,
+ BucketName: bucket,
+ Object: ObjectInfo{
+ Bucket: bucket,
+ Name: dobj.ObjectName,
+ VersionID: versionID,
+ DeleteMarker: dobj.DeleteMarker,
+ },
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ })
+ continue
+ }
+ wg.Add(1)
+ go func(tgt *TargetClient) {
+ defer wg.Done()
+ tgtInfo := replicateDeleteToTarget(ctx, dobj, tgt)
+
+ mu.Lock()
+ rinfos.Targets = append(rinfos.Targets, tgtInfo)
+ mu.Unlock()
+ }(tgtClnt)
+ }
+ wg.Wait()
+
+ replicationStatus = rinfos.ReplicationStatus()
+ prevStatus := dobj.DeleteMarkerReplicationStatus()
+
+ if dobj.VersionID != "" {
+ prevStatus = replication.StatusType(dobj.VersionPurgeStatus())
+ replicationStatus = replication.StatusType(rinfos.VersionPurgeStatus())
+ }
+
+ // to decrement pending count later.
+ for _, rinfo := range rinfos.Targets {
+ if rinfo.ReplicationStatus != rinfo.PrevReplicationStatus {
+ globalReplicationStats.Load().Update(dobj.Bucket, rinfo, replicationStatus,
+ prevStatus)
+ }
+ }
+
+ eventName := event.ObjectReplicationComplete
+ if replicationStatus == replication.Failed {
+ eventName = event.ObjectReplicationFailed
+ globalReplicationPool.Get().queueMRFSave(dobj.ToMRFEntry())
+ }
+ drs := getReplicationState(rinfos, dobj.ReplicationState, dobj.VersionID)
+ if replicationStatus != prevStatus {
+ drs.ReplicationTimeStamp = UTCNow()
+ }
+
+ dobjInfo, err := objectAPI.DeleteObject(ctx, bucket, dobj.ObjectName, ObjectOptions{
+ VersionID: versionID,
+ MTime: dobj.DeleteMarkerMTime.Time,
+ DeleteReplication: drs,
+ Versioned: globalBucketVersioningSys.PrefixEnabled(bucket, dobj.ObjectName),
+ // Objects matching prefixes should not leave delete markers,
+ // dramatically reduces namespace pollution while keeping the
+ // benefits of replication, make sure to apply version suspension
+ // only at bucket level instead.
+ VersionSuspended: globalBucketVersioningSys.Suspended(bucket),
+ })
+ if err != nil && !isErrVersionNotFound(err) { // VersionNotFound would be reported by pool that object version is missing on.
+ sendEvent(eventArgs{
+ BucketName: bucket,
+ Object: ObjectInfo{
+ Bucket: bucket,
+ Name: dobj.ObjectName,
+ VersionID: versionID,
+ DeleteMarker: dobj.DeleteMarker,
+ },
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ EventName: eventName,
+ })
+ } else {
+ sendEvent(eventArgs{
+ BucketName: bucket,
+ Object: dobjInfo,
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ EventName: eventName,
+ })
+ }
+}
+
+func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationInfo, tgt *TargetClient) (rinfo replicatedTargetInfo) {
+ versionID := dobj.DeleteMarkerVersionID
+ if versionID == "" {
+ versionID = dobj.VersionID
+ }
+
+ rinfo = dobj.ReplicationState.targetState(tgt.ARN)
+ rinfo.OpType = dobj.OpType
+ rinfo.endpoint = tgt.EndpointURL().Host
+ rinfo.secure = tgt.EndpointURL().Scheme == "https"
+ defer func() {
+ if rinfo.ReplicationStatus == replication.Completed && tgt.ResetID != "" && dobj.OpType == replication.ExistingObjectReplicationType {
+ rinfo.ResyncTimestamp = fmt.Sprintf("%s;%s", UTCNow().Format(http.TimeFormat), tgt.ResetID)
+ }
+ }()
+
+ if dobj.VersionID == "" && rinfo.PrevReplicationStatus == replication.Completed && dobj.OpType != replication.ExistingObjectReplicationType {
+ rinfo.ReplicationStatus = rinfo.PrevReplicationStatus
+ return rinfo
+ }
+ if dobj.VersionID != "" && rinfo.VersionPurgeStatus == replication.VersionPurgeComplete {
+ return rinfo
+ }
+ if globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
+ replLogOnceIf(ctx, fmt.Errorf("remote target is offline for bucket:%s arn:%s", dobj.Bucket, tgt.ARN), "replication-target-offline-delete-"+tgt.ARN)
+ sendEvent(eventArgs{
+ BucketName: dobj.Bucket,
+ Object: ObjectInfo{
+ Bucket: dobj.Bucket,
+ Name: dobj.ObjectName,
+ VersionID: dobj.VersionID,
+ DeleteMarker: dobj.DeleteMarker,
+ },
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ EventName: event.ObjectReplicationNotTracked,
+ })
+ if dobj.VersionID == "" {
+ rinfo.ReplicationStatus = replication.Failed
+ } else {
+ rinfo.VersionPurgeStatus = replication.VersionPurgeFailed
+ }
+ return rinfo
+ }
+ // early return if already replicated delete marker for existing object replication/ healing delete markers
+ if dobj.DeleteMarkerVersionID != "" {
+ toi, err := tgt.StatObject(ctx, tgt.Bucket, dobj.ObjectName, minio.StatObjectOptions{
+ VersionID: versionID,
+ Internal: minio.AdvancedGetOptions{
+ ReplicationProxyRequest: "false",
+ IsReplicationReadyForDeleteMarker: true,
+ },
+ })
+ serr := ErrorRespToObjectError(err, dobj.Bucket, dobj.ObjectName, dobj.VersionID)
+ switch {
+ case isErrMethodNotAllowed(serr):
+ // delete marker already replicated
+ if dobj.VersionID == "" && rinfo.VersionPurgeStatus.Empty() {
+ rinfo.ReplicationStatus = replication.Completed
+ return rinfo
+ }
+ case isErrObjectNotFound(serr), isErrVersionNotFound(serr):
+ // version being purged is already not found on target.
+ if !rinfo.VersionPurgeStatus.Empty() {
+ rinfo.VersionPurgeStatus = replication.VersionPurgeComplete
+ return rinfo
+ }
+ case isErrReadQuorum(serr), isErrWriteQuorum(serr):
+ // destination has some quorum issues, perform removeObject() anyways
+ // to complete the operation.
+ default:
+ if err != nil && minio.IsNetworkOrHostDown(err, true) && !globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
+ globalBucketTargetSys.markOffline(tgt.EndpointURL())
+ }
+ // mark delete marker replication as failed if target cluster not ready to receive
+ // this request yet (object version not replicated yet)
+ if err != nil && !toi.ReplicationReady {
+ rinfo.ReplicationStatus = replication.Failed
+ rinfo.Err = err
+ return rinfo
+ }
+ }
+ }
+ rmErr := tgt.RemoveObject(ctx, tgt.Bucket, dobj.ObjectName, minio.RemoveObjectOptions{
+ VersionID: versionID,
+ Internal: minio.AdvancedRemoveOptions{
+ ReplicationDeleteMarker: dobj.DeleteMarkerVersionID != "",
+ ReplicationMTime: dobj.DeleteMarkerMTime.Time,
+ ReplicationStatus: minio.ReplicationStatusReplica,
+ ReplicationRequest: true, // always set this to distinguish between `mc mirror` replication and serverside
+ },
+ })
+ if rmErr != nil {
+ rinfo.Err = rmErr
+ if dobj.VersionID == "" {
+ rinfo.ReplicationStatus = replication.Failed
+ } else {
+ 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))
+ if rmErr != nil && minio.IsNetworkOrHostDown(rmErr, true) && !globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
+ globalBucketTargetSys.markOffline(tgt.EndpointURL())
+ }
+ } else {
+ if dobj.VersionID == "" {
+ rinfo.ReplicationStatus = replication.Completed
+ } else {
+ rinfo.VersionPurgeStatus = replication.VersionPurgeComplete
+ }
+ }
+ return rinfo
+}
+
+func getCopyObjMetadata(oi ObjectInfo, sc string) map[string]string {
+ meta := make(map[string]string, len(oi.UserDefined))
+ for k, v := range oi.UserDefined {
+ if stringsHasPrefixFold(k, ReservedMetadataPrefixLower) {
+ continue
+ }
+
+ if equals(k, xhttp.AmzBucketReplicationStatus) {
+ continue
+ }
+
+ // https://github.com/google/security-research/security/advisories/GHSA-76wf-9vgp-pj7w
+ if equals(k, xhttp.AmzMetaUnencryptedContentLength, xhttp.AmzMetaUnencryptedContentMD5) {
+ continue
+ }
+ meta[k] = v
+ }
+
+ if oi.ContentEncoding != "" {
+ meta[xhttp.ContentEncoding] = oi.ContentEncoding
+ }
+
+ if oi.ContentType != "" {
+ meta[xhttp.ContentType] = oi.ContentType
+ }
+
+ meta[xhttp.AmzObjectTagging] = oi.UserTags
+ meta[xhttp.AmzTagDirective] = "REPLACE"
+
+ if sc == "" {
+ sc = oi.StorageClass
+ }
+ // drop non standard storage classes for tiering from replication
+ if sc != "" && (sc == storageclass.RRS || sc == storageclass.STANDARD) {
+ meta[xhttp.AmzStorageClass] = sc
+ }
+
+ meta[xhttp.MinIOSourceETag] = oi.ETag
+ meta[xhttp.MinIOSourceMTime] = oi.ModTime.UTC().Format(time.RFC3339Nano)
+ meta[xhttp.AmzBucketReplicationStatus] = replication.Replica.String()
+ return meta
+}
+
+type caseInsensitiveMap map[string]string
+
+// Lookup map entry case insensitively.
+func (m caseInsensitiveMap) Lookup(key string) (string, bool) {
+ if len(m) == 0 {
+ return "", false
+ }
+ for _, k := range []string{
+ key,
+ strings.ToLower(key),
+ http.CanonicalHeaderKey(key),
+ } {
+ v, ok := m[k]
+ if ok {
+ return v, ok
+ }
+ }
+ return "", false
+}
+
+func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (putOpts minio.PutObjectOptions, isMP bool, err error) {
+ meta := make(map[string]string)
+ isSSEC := crypto.SSEC.IsEncrypted(objInfo.UserDefined)
+
+ for k, v := range objInfo.UserDefined {
+ _, isValidSSEHeader := validSSEReplicationHeaders[k]
+ // In case of SSE-C objects copy the allowed internal headers as well
+ if !isSSEC || !isValidSSEHeader {
+ if stringsHasPrefixFold(k, ReservedMetadataPrefixLower) {
+ continue
+ }
+ if isStandardHeader(k) {
+ continue
+ }
+ }
+ if isValidSSEHeader {
+ meta[validSSEReplicationHeaders[k]] = v
+ } else {
+ meta[k] = v
+ }
+ }
+ isMP = objInfo.isMultipart()
+ if len(objInfo.Checksum) > 0 {
+ // Add encrypted CRC to metadata for SSE-C objects.
+ if isSSEC {
+ meta[ReplicationSsecChecksumHeader] = base64.StdEncoding.EncodeToString(objInfo.Checksum)
+ } else {
+ cs, mp := getCRCMeta(objInfo, 0, nil)
+ // Set object checksum.
+ maps.Copy(meta, cs)
+ 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
+ }
+ }
+ }
+
+ if sc == "" && (objInfo.StorageClass == storageclass.STANDARD || objInfo.StorageClass == storageclass.RRS) {
+ sc = objInfo.StorageClass
+ }
+ putOpts = minio.PutObjectOptions{
+ UserMetadata: meta,
+ ContentType: objInfo.ContentType,
+ ContentEncoding: objInfo.ContentEncoding,
+ Expires: objInfo.Expires,
+ StorageClass: sc,
+ Internal: minio.AdvancedPutOptions{
+ SourceVersionID: objInfo.VersionID,
+ ReplicationStatus: minio.ReplicationStatusReplica,
+ SourceMTime: objInfo.ModTime,
+ SourceETag: objInfo.ETag,
+ ReplicationRequest: true, // always set this to distinguish between `mc mirror` replication and serverside
+ },
+ }
+ if objInfo.UserTags != "" {
+ tag, _ := tags.ParseObjectTags(objInfo.UserTags)
+ if tag != nil {
+ putOpts.UserTags = tag.ToMap()
+ // set tag timestamp in opts
+ tagTimestamp := objInfo.ModTime
+ if tagTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+TaggingTimestamp]; ok {
+ tagTimestamp, err = time.Parse(time.RFC3339Nano, tagTmstampStr)
+ if err != nil {
+ return putOpts, false, err
+ }
+ }
+ putOpts.Internal.TaggingTimestamp = tagTimestamp
+ }
+ }
+
+ lkMap := caseInsensitiveMap(objInfo.UserDefined)
+ if lang, ok := lkMap.Lookup(xhttp.ContentLanguage); ok {
+ putOpts.ContentLanguage = lang
+ }
+ if disp, ok := lkMap.Lookup(xhttp.ContentDisposition); ok {
+ putOpts.ContentDisposition = disp
+ }
+ if cc, ok := lkMap.Lookup(xhttp.CacheControl); ok {
+ putOpts.CacheControl = cc
+ }
+ if mode, ok := lkMap.Lookup(xhttp.AmzObjectLockMode); ok {
+ rmode := minio.RetentionMode(mode)
+ putOpts.Mode = rmode
+ }
+ if retainDateStr, ok := lkMap.Lookup(xhttp.AmzObjectLockRetainUntilDate); ok {
+ rdate, err := amztime.ISO8601Parse(retainDateStr)
+ if err != nil {
+ return putOpts, false, err
+ }
+ putOpts.RetainUntilDate = rdate
+ // set retention timestamp in opts
+ retTimestamp := objInfo.ModTime
+ if retainTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp]; ok {
+ retTimestamp, err = time.Parse(time.RFC3339Nano, retainTmstampStr)
+ if err != nil {
+ return putOpts, false, err
+ }
+ }
+ putOpts.Internal.RetentionTimestamp = retTimestamp
+ }
+ if lhold, ok := lkMap.Lookup(xhttp.AmzObjectLockLegalHold); ok {
+ putOpts.LegalHold = minio.LegalHoldStatus(lhold)
+ // set legalhold timestamp in opts
+ lholdTimestamp := objInfo.ModTime
+ if lholdTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp]; ok {
+ lholdTimestamp, err = time.Parse(time.RFC3339Nano, lholdTmstampStr)
+ if err != nil {
+ return putOpts, false, err
+ }
+ }
+ putOpts.Internal.LegalholdTimestamp = lholdTimestamp
+ }
+ if crypto.S3.IsEncrypted(objInfo.UserDefined) {
+ 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 putOpts, isMP, err
+}
+
+type replicationAction string
+
+const (
+ replicateMetadata replicationAction = "metadata"
+ replicateNone replicationAction = "none"
+ replicateAll replicationAction = "all"
+)
+
+// matches k1 with all keys, returns 'true' if one of them matches
+func equals(k1 string, keys ...string) bool {
+ for _, k2 := range keys {
+ if strings.EqualFold(k1, k2) {
+ return true
+ }
+ }
+ return false
+}
+
+// returns replicationAction by comparing metadata between source and target
+func getReplicationAction(oi1 ObjectInfo, oi2 minio.ObjectInfo, opType replication.Type) replicationAction {
+ // Avoid resyncing null versions created prior to enabling replication if target has a newer copy
+ if opType == replication.ExistingObjectReplicationType &&
+ oi1.ModTime.Unix() > oi2.LastModified.Unix() && oi1.VersionID == nullVersionID {
+ return replicateNone
+ }
+ sz, _ := oi1.GetActualSize()
+
+ // needs full replication
+ if oi1.ETag != oi2.ETag ||
+ oi1.VersionID != oi2.VersionID ||
+ sz != oi2.Size ||
+ oi1.DeleteMarker != oi2.IsDeleteMarker ||
+ oi1.ModTime.Unix() != oi2.LastModified.Unix() {
+ return replicateAll
+ }
+
+ if oi1.ContentType != oi2.ContentType {
+ return replicateMetadata
+ }
+
+ if oi1.ContentEncoding != "" {
+ enc, ok := oi2.Metadata[xhttp.ContentEncoding]
+ if !ok {
+ enc, ok = oi2.Metadata[strings.ToLower(xhttp.ContentEncoding)]
+ if !ok {
+ return replicateMetadata
+ }
+ }
+ if strings.Join(enc, ",") != oi1.ContentEncoding {
+ return replicateMetadata
+ }
+ }
+
+ t, _ := tags.ParseObjectTags(oi1.UserTags)
+ oi2Map := make(map[string]string)
+ maps.Copy(oi2Map, oi2.UserTags)
+ if (oi2.UserTagCount > 0 && !reflect.DeepEqual(oi2Map, t.ToMap())) || (oi2.UserTagCount != len(t.ToMap())) {
+ return replicateMetadata
+ }
+
+ // Compare only necessary headers
+ compareKeys := []string{
+ "Expires",
+ "Cache-Control",
+ "Content-Language",
+ "Content-Disposition",
+ "X-Amz-Object-Lock-Mode",
+ "X-Amz-Object-Lock-Retain-Until-Date",
+ "X-Amz-Object-Lock-Legal-Hold",
+ "X-Amz-Website-Redirect-Location",
+ "X-Amz-Meta-",
+ }
+
+ // compare metadata on both maps to see if meta is identical
+ compareMeta1 := make(map[string]string)
+ for k, v := range oi1.UserDefined {
+ var found bool
+ for _, prefix := range compareKeys {
+ if !stringsHasPrefixFold(k, prefix) {
+ continue
+ }
+ found = true
+ break
+ }
+ if found {
+ compareMeta1[strings.ToLower(k)] = v
+ }
+ }
+
+ compareMeta2 := make(map[string]string)
+ for k, v := range oi2.Metadata {
+ var found bool
+ for _, prefix := range compareKeys {
+ if !stringsHasPrefixFold(k, prefix) {
+ continue
+ }
+ found = true
+ break
+ }
+ if found {
+ compareMeta2[strings.ToLower(k)] = strings.Join(v, ",")
+ }
+ }
+
+ if !reflect.DeepEqual(compareMeta1, compareMeta2) {
+ return replicateMetadata
+ }
+
+ return replicateNone
+}
+
+// replicateObject replicates the specified version of the object to destination bucket
+// The source object is then updated to reflect the replication status.
+func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI ObjectLayer) {
+ var replicationStatus replication.StatusType
+ defer func() {
+ if replicationStatus.Empty() {
+ // replication status is empty means
+ // replication was not attempted for some
+ // reason, notify the state of the object
+ // on disk.
+ replicationStatus = ri.ReplicationStatus
+ }
+ auditLogInternal(ctx, AuditLogOptions{
+ Event: ri.EventType,
+ APIName: ReplicateObjectAPI,
+ Bucket: ri.Bucket,
+ Object: ri.Name,
+ VersionID: ri.VersionID,
+ Status: replicationStatus.String(),
+ })
+ }()
+
+ bucket := ri.Bucket
+ object := ri.Name
+
+ cfg, err := getReplicationConfig(ctx, bucket)
+ if err != nil || cfg == nil {
+ replLogOnceIf(ctx, err, "get-replication-config-"+bucket)
+ sendEvent(eventArgs{
+ EventName: event.ObjectReplicationNotTracked,
+ BucketName: bucket,
+ Object: ri.ToObjectInfo(),
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ })
+ return
+ }
+ tgtArns := cfg.FilterTargetArns(replication.ObjectOpts{
+ Name: object,
+ SSEC: ri.SSEC,
+ UserTags: ri.UserTags,
+ })
+ // Lock the object name before starting replication.
+ // Use separate lock that doesn't collide with regular objects.
+ lk := objectAPI.NewNSLock(bucket, "/[replicate]/"+object)
+ lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ sendEvent(eventArgs{
+ EventName: event.ObjectReplicationNotTracked,
+ BucketName: bucket,
+ Object: ri.ToObjectInfo(),
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ })
+ globalReplicationPool.Get().queueMRFSave(ri.ToMRFEntry())
+ return
+ }
+ ctx = lkctx.Context()
+ defer lk.Unlock(lkctx)
+
+ rinfos := replicatedInfos{Targets: make([]replicatedTargetInfo, 0, len(tgtArns))}
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+ for _, tgtArn := range tgtArns {
+ tgt := globalBucketTargetSys.GetRemoteTargetClient(bucket, tgtArn)
+ if tgt == nil {
+ replLogOnceIf(ctx, fmt.Errorf("failed to get target for bucket:%s arn:%s", bucket, tgtArn), tgtArn)
+ sendEvent(eventArgs{
+ EventName: event.ObjectReplicationNotTracked,
+ BucketName: bucket,
+ Object: ri.ToObjectInfo(),
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ })
+ continue
+ }
+ wg.Add(1)
+ go func(tgt *TargetClient) {
+ defer wg.Done()
+
+ var tgtInfo replicatedTargetInfo
+ if ri.OpType == replication.ObjectReplicationType {
+ // all incoming calls go through optimized path.
+ tgtInfo = ri.replicateObject(ctx, objectAPI, tgt)
+ } else {
+ tgtInfo = ri.replicateAll(ctx, objectAPI, tgt)
+ }
+
+ mu.Lock()
+ rinfos.Targets = append(rinfos.Targets, tgtInfo)
+ mu.Unlock()
+ }(tgt)
+ }
+ wg.Wait()
+
+ replicationStatus = rinfos.ReplicationStatus() // used in defer function
+ // FIXME: add support for missing replication events
+ // - event.ObjectReplicationMissedThreshold
+ // - event.ObjectReplicationReplicatedAfterThreshold
+ eventName := event.ObjectReplicationComplete
+ if replicationStatus == replication.Failed {
+ eventName = event.ObjectReplicationFailed
+ }
+ newReplStatusInternal := rinfos.ReplicationStatusInternal()
+ // Note that internal replication status(es) may match for previously replicated objects - in such cases
+ // metadata should be updated with last resync timestamp.
+ objInfo := ri.ToObjectInfo()
+ if ri.ReplicationStatusInternal != newReplStatusInternal || rinfos.ReplicationResynced() {
+ popts := ObjectOptions{
+ MTime: ri.ModTime,
+ VersionID: ri.VersionID,
+ EvalMetadataFn: func(oi *ObjectInfo, gerr error) (dsc ReplicateDecision, err error) {
+ oi.UserDefined[ReservedMetadataPrefixLower+ReplicationStatus] = newReplStatusInternal
+ oi.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano)
+ oi.UserDefined[xhttp.AmzBucketReplicationStatus] = string(rinfos.ReplicationStatus())
+ for _, rinfo := range rinfos.Targets {
+ if rinfo.ResyncTimestamp != "" {
+ oi.UserDefined[targetResetHeader(rinfo.Arn)] = rinfo.ResyncTimestamp
+ }
+ }
+ if ri.UserTags != "" {
+ oi.UserDefined[xhttp.AmzObjectTagging] = ri.UserTags
+ }
+ return dsc, nil
+ },
+ }
+
+ uobjInfo, _ := objectAPI.PutObjectMetadata(ctx, bucket, object, popts)
+ if uobjInfo.Name != "" {
+ objInfo = uobjInfo
+ }
+
+ opType := replication.MetadataReplicationType
+ if rinfos.Action() == replicateAll {
+ opType = replication.ObjectReplicationType
+ }
+ for _, rinfo := range rinfos.Targets {
+ if rinfo.ReplicationStatus != rinfo.PrevReplicationStatus {
+ rinfo.OpType = opType // update optype to reflect correct operation.
+ globalReplicationStats.Load().Update(bucket, rinfo, rinfo.ReplicationStatus, rinfo.PrevReplicationStatus)
+ }
+ }
+ }
+
+ sendEvent(eventArgs{
+ EventName: eventName,
+ BucketName: bucket,
+ Object: objInfo,
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ })
+
+ // re-queue failures once more - keep a retry count to avoid flooding the queue if
+ // the target site is down. Leave it to scanner to catch up instead.
+ if rinfos.ReplicationStatus() != replication.Completed {
+ ri.OpType = replication.HealReplicationType
+ ri.EventType = ReplicateMRF
+ ri.ReplicationStatusInternal = rinfos.ReplicationStatusInternal()
+ ri.RetryCount++
+ globalReplicationPool.Get().queueMRFSave(ri.ToMRFEntry())
+ }
+}
+
+// replicateObject replicates object data for specified version of the object to destination bucket
+// The source object is then updated to reflect the replication status.
+func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI ObjectLayer, tgt *TargetClient) (rinfo replicatedTargetInfo) {
+ startTime := time.Now()
+ bucket := ri.Bucket
+ object := ri.Name
+
+ rAction := replicateAll
+ rinfo = replicatedTargetInfo{
+ Size: ri.ActualSize,
+ Arn: tgt.ARN,
+ PrevReplicationStatus: ri.TargetReplicationStatus(tgt.ARN),
+ ReplicationStatus: replication.Failed,
+ OpType: ri.OpType,
+ ReplicationAction: rAction,
+ endpoint: tgt.EndpointURL().Host,
+ secure: tgt.EndpointURL().Scheme == "https",
+ }
+ if ri.TargetReplicationStatus(tgt.ARN) == replication.Completed && !ri.ExistingObjResync.Empty() && !ri.ExistingObjResync.mustResyncTarget(tgt.ARN) {
+ rinfo.ReplicationStatus = replication.Completed
+ rinfo.ReplicationResynced = true
+ return rinfo
+ }
+
+ if globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
+ replLogOnceIf(ctx, fmt.Errorf("remote target is offline for bucket:%s arn:%s retry:%d", bucket, tgt.ARN, ri.RetryCount), "replication-target-offline"+tgt.ARN)
+ sendEvent(eventArgs{
+ EventName: event.ObjectReplicationNotTracked,
+ BucketName: bucket,
+ Object: ri.ToObjectInfo(),
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ })
+ return rinfo
+ }
+
+ versioned := globalBucketVersioningSys.PrefixEnabled(bucket, object)
+ versionSuspended := globalBucketVersioningSys.PrefixSuspended(bucket, object)
+
+ gr, err := objectAPI.GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, ObjectOptions{
+ VersionID: ri.VersionID,
+ Versioned: versioned,
+ VersionSuspended: versionSuspended,
+ ReplicationRequest: true,
+ })
+ if err != nil {
+ if !isErrVersionNotFound(err) && !isErrObjectNotFound(err) {
+ objInfo := ri.ToObjectInfo()
+ sendEvent(eventArgs{
+ EventName: event.ObjectReplicationNotTracked,
+ BucketName: bucket,
+ Object: objInfo,
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ })
+ replLogOnceIf(ctx, fmt.Errorf("unable to read source object %s/%s(%s): %w", bucket, object, objInfo.VersionID, err), object+":"+objInfo.VersionID)
+ }
+ return rinfo
+ }
+ defer gr.Close()
+
+ objInfo := gr.ObjInfo
+
+ // make sure we have the latest metadata for metrics calculation
+ rinfo.PrevReplicationStatus = objInfo.TargetReplicationStatus(tgt.ARN)
+
+ // Set the encrypted size for SSE-C objects
+ var size int64
+ if crypto.SSEC.IsEncrypted(objInfo.UserDefined) {
+ size = objInfo.Size
+ } else {
+ size, err = objInfo.GetActualSize()
+ if err != nil {
+ replLogIf(ctx, err)
+ sendEvent(eventArgs{
+ EventName: event.ObjectReplicationNotTracked,
+ BucketName: bucket,
+ Object: objInfo,
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ })
+ return rinfo
+ }
+ }
+
+ if tgt.Bucket == "" {
+ replLogIf(ctx, fmt.Errorf("unable to replicate object %s(%s), bucket is empty for target %s", objInfo.Name, objInfo.VersionID, tgt.EndpointURL()))
+ sendEvent(eventArgs{
+ EventName: event.ObjectReplicationNotTracked,
+ BucketName: bucket,
+ Object: objInfo,
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ })
+ return rinfo
+ }
+ defer func() {
+ if rinfo.ReplicationStatus == replication.Completed && ri.OpType == replication.ExistingObjectReplicationType && tgt.ResetID != "" {
+ rinfo.ResyncTimestamp = fmt.Sprintf("%s;%s", UTCNow().Format(http.TimeFormat), tgt.ResetID)
+ rinfo.ReplicationResynced = true
+ }
+ rinfo.Duration = time.Since(startTime)
+ }()
+
+ rinfo.ReplicationStatus = replication.Completed
+ rinfo.Size = size
+ rinfo.ReplicationAction = rAction
+ // use core client to avoid doing multipart on PUT
+ c := &minio.Core{Client: tgt.Client}
+
+ putOpts, isMP, err := putReplicationOpts(ctx, tgt.StorageClass, objInfo)
+ if err != nil {
+ replLogIf(ctx, fmt.Errorf("failure setting options for replication bucket:%s err:%w", bucket, err))
+ sendEvent(eventArgs{
+ EventName: event.ObjectReplicationNotTracked,
+ BucketName: bucket,
+ Object: objInfo,
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ })
+ return rinfo
+ }
+
+ var headerSize int
+ for k, v := range putOpts.Header() {
+ headerSize += len(k) + len(v)
+ }
+
+ opts := &bandwidth.MonitorReaderOptions{
+ BucketOptions: bandwidth.BucketOptions{
+ Name: ri.Bucket,
+ ReplicationARN: tgt.ARN,
+ },
+ HeaderSize: headerSize,
+ }
+ newCtx := ctx
+ if globalBucketMonitor.IsThrottled(bucket, tgt.ARN) && objInfo.Size < minLargeObjSize {
+ var cancel context.CancelFunc
+ newCtx, cancel = context.WithTimeout(ctx, throttleDeadline)
+ defer cancel()
+ }
+ r := bandwidth.NewMonitoredReader(newCtx, globalBucketMonitor, gr, opts)
+ if isMP {
+ rinfo.Err = replicateObjectWithMultipart(ctx, c, tgt.Bucket, object, r, objInfo, putOpts)
+ } else {
+ _, rinfo.Err = c.PutObject(ctx, tgt.Bucket, object, r, size, "", "", putOpts)
+ }
+ if rinfo.Err != nil {
+ if minio.ToErrorResponse(rinfo.Err).Code != "PreconditionFailed" {
+ rinfo.ReplicationStatus = replication.Failed
+ replLogIf(ctx, fmt.Errorf("unable to replicate for object %s/%s(%s): to (target: %s): %w",
+ bucket, objInfo.Name, objInfo.VersionID, tgt.EndpointURL(), rinfo.Err))
+ }
+ if minio.IsNetworkOrHostDown(rinfo.Err, true) && !globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
+ globalBucketTargetSys.markOffline(tgt.EndpointURL())
+ }
+ }
+ return rinfo
+}
+
+// replicateAll replicates metadata for specified version of the object to destination bucket
+// if the destination version is missing it automatically does fully copy as well.
+// The source object is then updated to reflect the replication status.
+func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI ObjectLayer, tgt *TargetClient) (rinfo replicatedTargetInfo) {
+ startTime := time.Now()
+ bucket := ri.Bucket
+ object := ri.Name
+
+ // set defaults for replication action based on operation being performed - actual
+ // replication action can only be determined after stat on remote. This default is
+ // needed for updating replication metrics correctly when target is offline.
+ rAction := replicateMetadata
+
+ rinfo = replicatedTargetInfo{
+ Size: ri.ActualSize,
+ Arn: tgt.ARN,
+ PrevReplicationStatus: ri.TargetReplicationStatus(tgt.ARN),
+ ReplicationStatus: replication.Failed,
+ OpType: ri.OpType,
+ ReplicationAction: rAction,
+ endpoint: tgt.EndpointURL().Host,
+ secure: tgt.EndpointURL().Scheme == "https",
+ }
+
+ if globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
+ replLogOnceIf(ctx, fmt.Errorf("remote target is offline for bucket:%s arn:%s retry:%d", bucket, tgt.ARN, ri.RetryCount), "replication-target-offline-heal"+tgt.ARN)
+ sendEvent(eventArgs{
+ EventName: event.ObjectReplicationNotTracked,
+ BucketName: bucket,
+ Object: ri.ToObjectInfo(),
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ })
+ return rinfo
+ }
+
+ versioned := globalBucketVersioningSys.PrefixEnabled(bucket, object)
+ versionSuspended := globalBucketVersioningSys.PrefixSuspended(bucket, object)
+
+ gr, err := objectAPI.GetObjectNInfo(ctx, bucket, object, nil, http.Header{},
+ ObjectOptions{
+ VersionID: ri.VersionID,
+ Versioned: versioned,
+ VersionSuspended: versionSuspended,
+ ReplicationRequest: true,
+ })
+ if err != nil {
+ if !isErrVersionNotFound(err) && !isErrObjectNotFound(err) {
+ objInfo := ri.ToObjectInfo()
+ sendEvent(eventArgs{
+ EventName: event.ObjectReplicationNotTracked,
+ BucketName: bucket,
+ Object: objInfo,
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ })
+ replLogIf(ctx, fmt.Errorf("unable to replicate to target %s for %s/%s(%s): %w", tgt.EndpointURL(), bucket, object, objInfo.VersionID, err))
+ }
+ return rinfo
+ }
+ defer gr.Close()
+
+ objInfo := gr.ObjInfo
+
+ // make sure we have the latest metadata for metrics calculation
+ rinfo.PrevReplicationStatus = objInfo.TargetReplicationStatus(tgt.ARN)
+
+ // use latest ObjectInfo to check if previous replication attempt succeeded
+ if objInfo.TargetReplicationStatus(tgt.ARN) == replication.Completed && !ri.ExistingObjResync.Empty() && !ri.ExistingObjResync.mustResyncTarget(tgt.ARN) {
+ rinfo.ReplicationStatus = replication.Completed
+ rinfo.ReplicationResynced = true
+ return rinfo
+ }
+
+ size, err := objInfo.GetActualSize()
+ if err != nil {
+ replLogIf(ctx, err)
+ sendEvent(eventArgs{
+ EventName: event.ObjectReplicationNotTracked,
+ BucketName: bucket,
+ Object: objInfo,
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ })
+ return rinfo
+ }
+
+ // Set the encrypted size for SSE-C objects
+ isSSEC := crypto.SSEC.IsEncrypted(objInfo.UserDefined)
+ if isSSEC {
+ size = objInfo.Size
+ }
+
+ if tgt.Bucket == "" {
+ replLogIf(ctx, fmt.Errorf("unable to replicate object %s(%s) to %s, target bucket is missing", objInfo.Name, objInfo.VersionID, tgt.EndpointURL()))
+ sendEvent(eventArgs{
+ EventName: event.ObjectReplicationNotTracked,
+ BucketName: bucket,
+ Object: objInfo,
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ })
+ return rinfo
+ }
+ defer func() {
+ if rinfo.ReplicationStatus == replication.Completed && ri.OpType == replication.ExistingObjectReplicationType && tgt.ResetID != "" {
+ rinfo.ResyncTimestamp = fmt.Sprintf("%s;%s", UTCNow().Format(http.TimeFormat), tgt.ResetID)
+ rinfo.ReplicationResynced = true
+ }
+ rinfo.Duration = time.Since(startTime)
+ }()
+ sOpts := minio.StatObjectOptions{
+ VersionID: objInfo.VersionID,
+ Internal: minio.AdvancedGetOptions{
+ ReplicationProxyRequest: "false",
+ },
+ }
+ sOpts.Set(xhttp.AmzTagDirective, "ACCESS")
+ oi, cerr := tgt.StatObject(ctx, tgt.Bucket, object, sOpts)
+ if cerr == nil {
+ rAction = getReplicationAction(objInfo, oi, ri.OpType)
+ rinfo.ReplicationStatus = replication.Completed
+ if rAction == replicateNone {
+ if ri.OpType == replication.ExistingObjectReplicationType &&
+ objInfo.ModTime.Unix() > oi.LastModified.Unix() && objInfo.VersionID == nullVersionID {
+ replLogIf(ctx, fmt.Errorf("unable to replicate %s/%s (null). Newer version exists on target %s", bucket, object, tgt.EndpointURL()))
+ sendEvent(eventArgs{
+ EventName: event.ObjectReplicationNotTracked,
+ BucketName: bucket,
+ Object: objInfo,
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ })
+ }
+ // object with same VersionID already exists, replication kicked off by
+ // PutObject might have completed
+ if objInfo.TargetReplicationStatus(tgt.ARN) == replication.Pending ||
+ objInfo.TargetReplicationStatus(tgt.ARN) == replication.Failed ||
+ ri.OpType == replication.ExistingObjectReplicationType {
+ // if metadata is not updated for some reason after replication, such as
+ // 503 encountered while updating metadata - make sure to set ReplicationStatus
+ // as Completed.
+ //
+ // Note: Replication Stats would have been updated despite metadata update failure.
+ rinfo.ReplicationAction = rAction
+ rinfo.ReplicationStatus = replication.Completed
+ }
+ return rinfo
+ }
+ } else {
+ // SSEC objects will refuse HeadObject without the decryption key.
+ // Ignore the error, since we know the object exists and versioning prevents overwriting existing versions.
+ if isSSEC && strings.Contains(cerr.Error(), errorCodes[ErrSSEEncryptedObject].Description) {
+ rinfo.ReplicationStatus = replication.Completed
+ rinfo.ReplicationAction = replicateNone
+ goto applyAction
+ }
+ // if target returns error other than NoSuchKey, defer replication attempt
+ if minio.IsNetworkOrHostDown(cerr, true) && !globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
+ globalBucketTargetSys.markOffline(tgt.EndpointURL())
+ }
+
+ serr := ErrorRespToObjectError(cerr, bucket, object, objInfo.VersionID)
+ switch {
+ case isErrMethodNotAllowed(serr):
+ rAction = replicateAll
+ case isErrObjectNotFound(serr), isErrVersionNotFound(serr):
+ rAction = replicateAll
+ case isErrReadQuorum(serr), isErrWriteQuorum(serr):
+ rAction = replicateAll
+ default:
+ rinfo.Err = cerr
+ replLogIf(ctx, fmt.Errorf("unable to replicate %s/%s (%s). Target (%s) returned %s error on HEAD",
+ bucket, object, objInfo.VersionID, tgt.EndpointURL(), cerr))
+ sendEvent(eventArgs{
+ EventName: event.ObjectReplicationNotTracked,
+ BucketName: bucket,
+ Object: objInfo,
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ })
+ return rinfo
+ }
+ }
+applyAction:
+ rinfo.ReplicationStatus = replication.Completed
+ rinfo.Size = size
+ rinfo.ReplicationAction = rAction
+ // use core client to avoid doing multipart on PUT
+ c := &minio.Core{Client: tgt.Client}
+ if rAction != replicateAll {
+ // replicate metadata for object tagging/copy with metadata replacement
+ srcOpts := minio.CopySrcOptions{
+ Bucket: tgt.Bucket,
+ Object: object,
+ VersionID: objInfo.VersionID,
+ }
+ dstOpts := minio.PutObjectOptions{
+ Internal: minio.AdvancedPutOptions{
+ SourceVersionID: objInfo.VersionID,
+ ReplicationRequest: true, // always set this to distinguish between `mc mirror` replication and serverside
+ },
+ }
+ // 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)
+ if err == nil {
+ dstOpts.Internal.TaggingTimestamp = ondiskTimestamp
+ }
+ }
+ if retTmStr, ok := lkMap.Lookup(ReservedMetadataPrefixLower + ObjectLockRetentionTimestamp); ok {
+ ondiskTimestamp, err := time.Parse(time.RFC3339, retTmStr)
+ if err == nil {
+ dstOpts.Internal.RetentionTimestamp = ondiskTimestamp
+ }
+ }
+ if lholdTmStr, ok := lkMap.Lookup(ReservedMetadataPrefixLower + ObjectLockLegalHoldTimestamp); ok {
+ ondiskTimestamp, err := time.Parse(time.RFC3339, lholdTmStr)
+ if err == nil {
+ dstOpts.Internal.LegalholdTimestamp = ondiskTimestamp
+ }
+ }
+ if _, rinfo.Err = c.CopyObject(ctx, tgt.Bucket, object, tgt.Bucket, object, getCopyObjMetadata(objInfo, tgt.StorageClass), srcOpts, dstOpts); rinfo.Err != nil {
+ rinfo.ReplicationStatus = replication.Failed
+ 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 {
+ putOpts, isMP, err := putReplicationOpts(ctx, tgt.StorageClass, objInfo)
+ 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))
+ sendEvent(eventArgs{
+ EventName: event.ObjectReplicationNotTracked,
+ BucketName: bucket,
+ Object: objInfo,
+ UserAgent: "Internal: [Replication]",
+ Host: globalLocalNodeName,
+ })
+ return rinfo
+ }
+ var headerSize int
+ for k, v := range putOpts.Header() {
+ headerSize += len(k) + len(v)
+ }
+
+ opts := &bandwidth.MonitorReaderOptions{
+ BucketOptions: bandwidth.BucketOptions{
+ Name: objInfo.Bucket,
+ ReplicationARN: tgt.ARN,
+ },
+ HeaderSize: headerSize,
+ }
+ newCtx := ctx
+ if globalBucketMonitor.IsThrottled(bucket, tgt.ARN) && objInfo.Size < minLargeObjSize {
+ var cancel context.CancelFunc
+ newCtx, cancel = context.WithTimeout(ctx, throttleDeadline)
+ defer cancel()
+ }
+ r := bandwidth.NewMonitoredReader(newCtx, globalBucketMonitor, gr, opts)
+ if isMP {
+ rinfo.Err = replicateObjectWithMultipart(ctx, c, tgt.Bucket, object, r, objInfo, putOpts)
+ } else {
+ _, rinfo.Err = c.PutObject(ctx, tgt.Bucket, object, r, size, "", "", putOpts)
+ }
+ if rinfo.Err != nil {
+ if minio.ToErrorResponse(rinfo.Err).Code != "PreconditionFailed" {
+ rinfo.ReplicationStatus = replication.Failed
+ replLogIf(ctx, fmt.Errorf("unable to replicate for object %s/%s(%s) to target %s: %w",
+ bucket, objInfo.Name, objInfo.VersionID, tgt.EndpointURL(), rinfo.Err))
+ }
+ if minio.IsNetworkOrHostDown(rinfo.Err, true) && !globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
+ globalBucketTargetSys.markOffline(tgt.EndpointURL())
+ }
+ }
+ }
+ return rinfo
+}
+
+func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, object string, r io.Reader, objInfo ObjectInfo, opts minio.PutObjectOptions) (err error) {
+ var uploadedParts []minio.CompletePart
+ // new multipart must not set mtime as it may lead to erroneous cleanups at various intervals.
+ opts.Internal.SourceMTime = time.Time{} // this value is saved properly in CompleteMultipartUpload()
+ var uploadID string
+ attempts := 1
+ for attempts <= 3 {
+ nctx, cancel := context.WithTimeout(ctx, time.Minute)
+ uploadID, err = c.NewMultipartUpload(nctx, bucket, object, opts)
+ cancel()
+ if err == nil {
+ break
+ }
+ if minio.ToErrorResponse(err).Code == "PreconditionFailed" {
+ return nil
+ }
+ attempts++
+ time.Sleep(time.Duration(rand.Int63n(int64(time.Second))))
+ }
+ if err != nil {
+ return err
+ }
+
+ defer func() {
+ if err != nil {
+ // block and abort remote upload upon failure.
+ attempts := 1
+ for attempts <= 3 {
+ actx, acancel := context.WithTimeout(ctx, time.Minute)
+ aerr := c.AbortMultipartUpload(actx, bucket, object, uploadID)
+ acancel()
+ if aerr == nil {
+ return
+ }
+ attempts++
+ time.Sleep(time.Duration(rand.Int63n(int64(time.Second))))
+ }
+ }
+ }()
+
+ var (
+ hr *hash.Reader
+ isSSEC = crypto.SSEC.IsEncrypted(objInfo.UserDefined)
+ )
+
+ var objectSize int64
+ for _, partInfo := range objInfo.Parts {
+ if isSSEC {
+ hr, err = hash.NewReader(ctx, io.LimitReader(r, partInfo.Size), partInfo.Size, "", "", partInfo.ActualSize)
+ } else {
+ hr, err = hash.NewReader(ctx, io.LimitReader(r, partInfo.ActualSize), partInfo.ActualSize, "", "", partInfo.ActualSize)
+ }
+ if err != nil {
+ return err
+ }
+
+ cHeader := http.Header{}
+ cHeader.Add(xhttp.MinIOSourceReplicationRequest, "true")
+ if !isSSEC {
+ cs, _ := getCRCMeta(objInfo, partInfo.Number, nil)
+ for k, v := range cs {
+ cHeader.Add(k, v)
+ }
+ }
+ popts := minio.PutObjectPartOptions{
+ SSE: opts.ServerSideEncryption,
+ CustomHeader: cHeader,
+ }
+
+ var size int64
+ if isSSEC {
+ size = partInfo.Size
+ } else {
+ size = partInfo.ActualSize
+ }
+ objectSize += size
+ pInfo, err := c.PutObjectPart(ctx, bucket, object, uploadID, partInfo.Number, hr, size, popts)
+ if err != nil {
+ return err
+ }
+ if pInfo.Size != size {
+ return fmt.Errorf("ssec(%t): Part size mismatch: got %d, want %d", isSSEC, pInfo.Size, size)
+ }
+ uploadedParts = append(uploadedParts, minio.CompletePart{
+ PartNumber: pInfo.PartNumber,
+ ETag: pInfo.ETag,
+ ChecksumCRC32: pInfo.ChecksumCRC32,
+ ChecksumCRC32C: pInfo.ChecksumCRC32C,
+ ChecksumSHA1: pInfo.ChecksumSHA1,
+ ChecksumSHA256: pInfo.ChecksumSHA256,
+ ChecksumCRC64NVME: pInfo.ChecksumCRC64NVME,
+ })
+ }
+ userMeta := map[string]string{
+ xhttp.MinIOReplicationActualObjectSize: objInfo.UserDefined[ReservedMetadataPrefix+"actual-size"],
+ }
+ if isSSEC && objInfo.UserDefined[ReplicationSsecChecksumHeader] != "" {
+ userMeta[ReplicationSsecChecksumHeader] = objInfo.UserDefined[ReplicationSsecChecksumHeader]
+ }
+
+ // really big value but its okay on heavily loaded systems. This is just tail end timeout.
+ cctx, ccancel := context.WithTimeout(ctx, 10*time.Minute)
+ 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{
+ UserMetadata: userMeta,
+ Internal: minio.AdvancedPutOptions{
+ SourceMTime: objInfo.ModTime,
+ SourceETag: objInfo.ETag,
+ // always set this to distinguish between `mc mirror` replication and serverside
+ ReplicationRequest: true,
+ },
+ })
+ return err
+}
+
+// filterReplicationStatusMetadata filters replication status metadata for COPY
+func filterReplicationStatusMetadata(metadata map[string]string) map[string]string {
+ // Copy on write
+ dst := metadata
+ var copied bool
+ delKey := func(key string) {
+ if _, ok := metadata[key]; !ok {
+ return
+ }
+ if !copied {
+ dst = make(map[string]string, len(metadata))
+ maps.Copy(dst, metadata)
+ copied = true
+ }
+ delete(dst, key)
+ }
+
+ delKey(xhttp.AmzBucketReplicationStatus)
+ return dst
+}
+
+// DeletedObjectReplicationInfo has info on deleted object
+type DeletedObjectReplicationInfo struct {
+ DeletedObject
+ Bucket string
+ EventType string
+ OpType replication.Type
+ ResetID string
+ TargetArn string
+}
+
+// ToMRFEntry returns the relevant info needed by MRF
+func (di DeletedObjectReplicationInfo) ToMRFEntry() MRFReplicateEntry {
+ versionID := di.DeleteMarkerVersionID
+ if versionID == "" {
+ versionID = di.VersionID
+ }
+ return MRFReplicateEntry{
+ Bucket: di.Bucket,
+ Object: di.ObjectName,
+ versionID: versionID,
+ }
+}
+
+// Replication specific APIName
+const (
+ ReplicateObjectAPI = "ReplicateObject"
+ ReplicateDeleteAPI = "ReplicateDelete"
+)
+
+const (
+ // ReplicateQueued - replication being queued trail
+ ReplicateQueued = "replicate:queue"
+
+ // ReplicateExisting - audit trail for existing objects replication
+ ReplicateExisting = "replicate:existing"
+ // ReplicateExistingDelete - audit trail for delete replication triggered for existing delete markers
+ ReplicateExistingDelete = "replicate:existing:delete"
+
+ // ReplicateMRF - audit trail for replication from Most Recent Failures (MRF) queue
+ ReplicateMRF = "replicate:mrf"
+ // ReplicateIncoming - audit trail of inline replication
+ ReplicateIncoming = "replicate:incoming"
+ // ReplicateIncomingDelete - audit trail of inline replication of deletes.
+ ReplicateIncomingDelete = "replicate:incoming:delete"
+
+ // ReplicateHeal - audit trail for healing of failed/pending replications
+ ReplicateHeal = "replicate:heal"
+ // ReplicateHealDelete - audit trail of healing of failed/pending delete replications.
+ ReplicateHealDelete = "replicate:heal:delete"
+)
+
+var (
+ globalReplicationPool = once.NewSingleton[ReplicationPool]()
+ globalReplicationStats atomic.Pointer[ReplicationStats]
+)
+
+// ReplicationPool describes replication pool
+type ReplicationPool struct {
+ // atomic ops:
+ activeWorkers int32
+ activeLrgWorkers int32
+ activeMRFWorkers int32
+
+ objLayer ObjectLayer
+ ctx context.Context
+ priority string
+ maxWorkers int
+ maxLWorkers int
+ stats *ReplicationStats
+
+ mu sync.RWMutex
+ mrfMU sync.Mutex
+ resyncer *replicationResyncer
+
+ // workers:
+ workers []chan ReplicationWorkerOperation
+ lrgworkers []chan ReplicationWorkerOperation
+
+ // mrf:
+ mrfWorkerKillCh chan struct{}
+ mrfReplicaCh chan ReplicationWorkerOperation
+ mrfSaveCh chan MRFReplicateEntry
+ mrfStopCh chan struct{}
+ mrfWorkerSize int
+}
+
+// ReplicationWorkerOperation is a shared interface of replication operations.
+type ReplicationWorkerOperation interface {
+ ToMRFEntry() MRFReplicateEntry
+}
+
+const (
+ // WorkerMaxLimit max number of workers per node for "fast" mode
+ WorkerMaxLimit = 500
+
+ // WorkerMinLimit min number of workers per node for "slow" mode
+ WorkerMinLimit = 50
+
+ // WorkerAutoDefault is default number of workers for "auto" mode
+ WorkerAutoDefault = 100
+
+ // MRFWorkerMaxLimit max number of mrf workers per node for "fast" mode
+ MRFWorkerMaxLimit = 8
+
+ // MRFWorkerMinLimit min number of mrf workers per node for "slow" mode
+ MRFWorkerMinLimit = 2
+
+ // MRFWorkerAutoDefault is default number of mrf workers for "auto" mode
+ MRFWorkerAutoDefault = 4
+
+ // LargeWorkerCount is default number of workers assigned to large uploads ( >= 128MiB)
+ LargeWorkerCount = 10
+)
+
+// NewReplicationPool creates a pool of replication workers of specified size
+func NewReplicationPool(ctx context.Context, o ObjectLayer, opts replicationPoolOpts, stats *ReplicationStats) *ReplicationPool {
+ var workers, failedWorkers int
+ priority := "auto"
+ maxWorkers := WorkerMaxLimit
+ if opts.Priority != "" {
+ priority = opts.Priority
+ }
+ if opts.MaxWorkers > 0 {
+ maxWorkers = opts.MaxWorkers
+ }
+ switch priority {
+ case "fast":
+ workers = WorkerMaxLimit
+ failedWorkers = MRFWorkerMaxLimit
+ case "slow":
+ workers = WorkerMinLimit
+ failedWorkers = MRFWorkerMinLimit
+ default:
+ workers = WorkerAutoDefault
+ failedWorkers = MRFWorkerAutoDefault
+ }
+ if maxWorkers > 0 && workers > maxWorkers {
+ workers = maxWorkers
+ }
+
+ if maxWorkers > 0 && failedWorkers > maxWorkers {
+ failedWorkers = maxWorkers
+ }
+ maxLWorkers := LargeWorkerCount
+ if opts.MaxLWorkers > 0 {
+ maxLWorkers = opts.MaxLWorkers
+ }
+ pool := &ReplicationPool{
+ workers: make([]chan ReplicationWorkerOperation, 0, workers),
+ lrgworkers: make([]chan ReplicationWorkerOperation, 0, maxLWorkers),
+ mrfReplicaCh: make(chan ReplicationWorkerOperation, 100000),
+ mrfWorkerKillCh: make(chan struct{}, failedWorkers),
+ resyncer: newresyncer(),
+ mrfSaveCh: make(chan MRFReplicateEntry, 100000),
+ mrfStopCh: make(chan struct{}, 1),
+ ctx: ctx,
+ objLayer: o,
+ stats: stats,
+ priority: priority,
+ maxWorkers: maxWorkers,
+ maxLWorkers: maxLWorkers,
+ }
+
+ pool.ResizeLrgWorkers(maxLWorkers, 0)
+ pool.ResizeWorkers(workers, 0)
+ pool.ResizeFailedWorkers(failedWorkers)
+ go pool.resyncer.PersistToDisk(ctx, o)
+ go pool.processMRF()
+ go pool.persistMRF()
+ return pool
+}
+
+// AddMRFWorker adds a pending/failed replication worker to handle requests that could not be queued
+// to the other workers
+func (p *ReplicationPool) AddMRFWorker() {
+ for {
+ select {
+ case <-p.ctx.Done():
+ return
+ case oi, ok := <-p.mrfReplicaCh:
+ if !ok {
+ return
+ }
+ switch v := oi.(type) {
+ case ReplicateObjectInfo:
+ p.stats.incQ(v.Bucket, v.Size, v.DeleteMarker, v.OpType)
+ atomic.AddInt32(&p.activeMRFWorkers, 1)
+ replicateObject(p.ctx, v, p.objLayer)
+ atomic.AddInt32(&p.activeMRFWorkers, -1)
+ p.stats.decQ(v.Bucket, v.Size, v.DeleteMarker, v.OpType)
+
+ default:
+ bugLogIf(p.ctx, fmt.Errorf("unknown mrf replication type: %T", oi), "unknown-mrf-replicate-type")
+ }
+ case <-p.mrfWorkerKillCh:
+ return
+ }
+ }
+}
+
+// AddWorker adds a replication worker to the pool.
+// An optional pointer to a tracker that will be atomically
+// incremented when operations are running can be provided.
+func (p *ReplicationPool) AddWorker(input <-chan ReplicationWorkerOperation, opTracker *int32) {
+ for {
+ select {
+ case <-p.ctx.Done():
+ return
+ case oi, ok := <-input:
+ if !ok {
+ return
+ }
+ switch v := oi.(type) {
+ case ReplicateObjectInfo:
+ if opTracker != nil {
+ atomic.AddInt32(opTracker, 1)
+ }
+ p.stats.incQ(v.Bucket, v.Size, v.DeleteMarker, v.OpType)
+ replicateObject(p.ctx, v, p.objLayer)
+ p.stats.decQ(v.Bucket, v.Size, v.DeleteMarker, v.OpType)
+ if opTracker != nil {
+ atomic.AddInt32(opTracker, -1)
+ }
+ case DeletedObjectReplicationInfo:
+ if opTracker != nil {
+ atomic.AddInt32(opTracker, 1)
+ }
+ p.stats.incQ(v.Bucket, 0, true, v.OpType)
+
+ replicateDelete(p.ctx, v, p.objLayer)
+ p.stats.decQ(v.Bucket, 0, true, v.OpType)
+
+ if opTracker != nil {
+ atomic.AddInt32(opTracker, -1)
+ }
+ default:
+ bugLogIf(p.ctx, fmt.Errorf("unknown replication type: %T", oi), "unknown-replicate-type")
+ }
+ }
+ }
+}
+
+// AddLargeWorker adds a replication worker to the static pool for large uploads.
+func (p *ReplicationPool) AddLargeWorker(input <-chan ReplicationWorkerOperation, opTracker *int32) {
+ for {
+ select {
+ case <-p.ctx.Done():
+ return
+ case oi, ok := <-input:
+ if !ok {
+ return
+ }
+ switch v := oi.(type) {
+ case ReplicateObjectInfo:
+ if opTracker != nil {
+ atomic.AddInt32(opTracker, 1)
+ }
+ p.stats.incQ(v.Bucket, v.Size, v.DeleteMarker, v.OpType)
+ replicateObject(p.ctx, v, p.objLayer)
+ p.stats.decQ(v.Bucket, v.Size, v.DeleteMarker, v.OpType)
+ if opTracker != nil {
+ atomic.AddInt32(opTracker, -1)
+ }
+ case DeletedObjectReplicationInfo:
+ if opTracker != nil {
+ atomic.AddInt32(opTracker, 1)
+ }
+ replicateDelete(p.ctx, v, p.objLayer)
+ if opTracker != nil {
+ atomic.AddInt32(opTracker, -1)
+ }
+ default:
+ bugLogIf(p.ctx, fmt.Errorf("unknown replication type: %T", oi), "unknown-replicate-type")
+ }
+ }
+ }
+}
+
+// ResizeLrgWorkers sets replication workers pool for large transfers(>=128MiB) to new size.
+// checkOld can be set to an expected value.
+// If the worker count changed
+func (p *ReplicationPool) ResizeLrgWorkers(n, checkOld int) {
+ p.mu.Lock()
+ defer p.mu.Unlock()
+
+ if (checkOld > 0 && len(p.lrgworkers) != checkOld) || n == len(p.lrgworkers) || n < 1 {
+ // Either already satisfied or worker count changed while we waited for the lock.
+ return
+ }
+ for len(p.lrgworkers) < n {
+ input := make(chan ReplicationWorkerOperation, 100000)
+ p.lrgworkers = append(p.lrgworkers, input)
+
+ go p.AddLargeWorker(input, &p.activeLrgWorkers)
+ }
+ for len(p.lrgworkers) > n {
+ worker := p.lrgworkers[len(p.lrgworkers)-1]
+ p.lrgworkers = p.lrgworkers[:len(p.lrgworkers)-1]
+ xioutil.SafeClose(worker)
+ }
+}
+
+// ActiveWorkers returns the number of active workers handling replication traffic.
+func (p *ReplicationPool) ActiveWorkers() int {
+ return int(atomic.LoadInt32(&p.activeWorkers))
+}
+
+// ActiveMRFWorkers returns the number of active workers handling replication failures.
+func (p *ReplicationPool) ActiveMRFWorkers() int {
+ return int(atomic.LoadInt32(&p.activeMRFWorkers))
+}
+
+// ActiveLrgWorkers returns the number of active workers handling traffic > 128MiB object size.
+func (p *ReplicationPool) ActiveLrgWorkers() int {
+ return int(atomic.LoadInt32(&p.activeLrgWorkers))
+}
+
+// ResizeWorkers sets replication workers pool to new size.
+// checkOld can be set to an expected value.
+// If the worker count changed
+func (p *ReplicationPool) ResizeWorkers(n, checkOld int) {
+ p.mu.Lock()
+ defer p.mu.Unlock()
+
+ if (checkOld > 0 && len(p.workers) != checkOld) || n == len(p.workers) || n < 1 {
+ // Either already satisfied or worker count changed while we waited for the lock.
+ return
+ }
+ for len(p.workers) < n {
+ input := make(chan ReplicationWorkerOperation, 10000)
+ p.workers = append(p.workers, input)
+
+ go p.AddWorker(input, &p.activeWorkers)
+ }
+ for len(p.workers) > n {
+ worker := p.workers[len(p.workers)-1]
+ p.workers = p.workers[:len(p.workers)-1]
+ xioutil.SafeClose(worker)
+ }
+}
+
+// ResizeWorkerPriority sets replication failed workers pool size
+func (p *ReplicationPool) ResizeWorkerPriority(pri string, maxWorkers, maxLWorkers int) {
+ var workers, mrfWorkers int
+ p.mu.Lock()
+ switch pri {
+ case "fast":
+ workers = WorkerMaxLimit
+ mrfWorkers = MRFWorkerMaxLimit
+ case "slow":
+ workers = WorkerMinLimit
+ mrfWorkers = MRFWorkerMinLimit
+ default:
+ workers = WorkerAutoDefault
+ mrfWorkers = MRFWorkerAutoDefault
+ if len(p.workers) < WorkerAutoDefault {
+ workers = min(len(p.workers)+1, WorkerAutoDefault)
+ }
+ if p.mrfWorkerSize < MRFWorkerAutoDefault {
+ mrfWorkers = min(p.mrfWorkerSize+1, MRFWorkerAutoDefault)
+ }
+ }
+ if maxWorkers > 0 && workers > maxWorkers {
+ workers = maxWorkers
+ }
+
+ if maxWorkers > 0 && mrfWorkers > maxWorkers {
+ mrfWorkers = maxWorkers
+ }
+ if maxLWorkers <= 0 {
+ maxLWorkers = LargeWorkerCount
+ }
+ p.priority = pri
+ p.maxWorkers = maxWorkers
+ p.mu.Unlock()
+ p.ResizeWorkers(workers, 0)
+ p.ResizeFailedWorkers(mrfWorkers)
+ p.ResizeLrgWorkers(maxLWorkers, 0)
+}
+
+// ResizeFailedWorkers sets replication failed workers pool size
+func (p *ReplicationPool) ResizeFailedWorkers(n int) {
+ p.mu.Lock()
+ defer p.mu.Unlock()
+
+ for p.mrfWorkerSize < n {
+ p.mrfWorkerSize++
+ go p.AddMRFWorker()
+ }
+ for p.mrfWorkerSize > n {
+ p.mrfWorkerSize--
+ go func() { p.mrfWorkerKillCh <- struct{}{} }()
+ }
+}
+
+const (
+ minLargeObjSize = 128 * humanize.MiByte // 128MiB
+)
+
+// getWorkerCh gets a worker channel deterministically based on bucket and object names.
+// Must be able to grab read lock from p.
+
+func (p *ReplicationPool) getWorkerCh(bucket, object string, sz int64) chan<- ReplicationWorkerOperation {
+ h := xxh3.HashString(bucket + object)
+ p.mu.RLock()
+ defer p.mu.RUnlock()
+ if len(p.workers) == 0 {
+ return nil
+ }
+ return p.workers[h%uint64(len(p.workers))]
+}
+
+func (p *ReplicationPool) queueReplicaTask(ri ReplicateObjectInfo) {
+ if p == nil {
+ return
+ }
+ // if object is large, queue it to a static set of large workers
+ if ri.Size >= int64(minLargeObjSize) {
+ h := xxh3.HashString(ri.Bucket + ri.Name)
+ select {
+ case <-p.ctx.Done():
+ case p.lrgworkers[h%uint64(len(p.lrgworkers))] <- ri:
+ default:
+ p.queueMRFSave(ri.ToMRFEntry())
+ p.mu.RLock()
+ maxLWorkers := p.maxLWorkers
+ existing := len(p.lrgworkers)
+ p.mu.RUnlock()
+ maxLWorkers = min(maxLWorkers, LargeWorkerCount)
+ if p.ActiveLrgWorkers() < maxLWorkers {
+ workers := min(existing+1, maxLWorkers)
+ p.ResizeLrgWorkers(workers, existing)
+ }
+ }
+ return
+ }
+
+ var ch, healCh chan<- ReplicationWorkerOperation
+ switch ri.OpType {
+ case replication.HealReplicationType, replication.ExistingObjectReplicationType:
+ ch = p.mrfReplicaCh
+ healCh = p.getWorkerCh(ri.Name, ri.Bucket, ri.Size)
+ default:
+ ch = p.getWorkerCh(ri.Name, ri.Bucket, ri.Size)
+ }
+ if ch == nil && healCh == nil {
+ return
+ }
+
+ select {
+ case <-p.ctx.Done():
+ case healCh <- ri:
+ case ch <- ri:
+ default:
+ globalReplicationPool.Get().queueMRFSave(ri.ToMRFEntry())
+ p.mu.RLock()
+ prio := p.priority
+ maxWorkers := p.maxWorkers
+ p.mu.RUnlock()
+ switch prio {
+ case "fast":
+ replLogOnceIf(GlobalContext, fmt.Errorf("Unable to keep up with incoming traffic"), string(replicationSubsystem), logger.WarningKind)
+ case "slow":
+ replLogOnceIf(GlobalContext, fmt.Errorf("Unable to keep up with incoming traffic - we recommend increasing replication priority with `mc admin config set api replication_priority=auto`"), string(replicationSubsystem), logger.WarningKind)
+ default:
+ maxWorkers = min(maxWorkers, WorkerMaxLimit)
+ if p.ActiveWorkers() < maxWorkers {
+ p.mu.RLock()
+ workers := min(len(p.workers)+1, maxWorkers)
+ existing := len(p.workers)
+ p.mu.RUnlock()
+ p.ResizeWorkers(workers, existing)
+ }
+ maxMRFWorkers := min(maxWorkers, MRFWorkerMaxLimit)
+ if p.ActiveMRFWorkers() < maxMRFWorkers {
+ p.mu.RLock()
+ workers := min(p.mrfWorkerSize+1, maxMRFWorkers)
+ p.mu.RUnlock()
+ p.ResizeFailedWorkers(workers)
+ }
+ }
+ }
+}
+
+func queueReplicateDeletesWrapper(doi DeletedObjectReplicationInfo, existingObjectResync ResyncDecision) {
+ for k, v := range existingObjectResync.targets {
+ if v.Replicate {
+ doi.ResetID = v.ResetID
+ doi.TargetArn = k
+
+ globalReplicationPool.Get().queueReplicaDeleteTask(doi)
+ }
+ }
+}
+
+func (p *ReplicationPool) queueReplicaDeleteTask(doi DeletedObjectReplicationInfo) {
+ if p == nil {
+ return
+ }
+ var ch chan<- ReplicationWorkerOperation
+ switch doi.OpType {
+ case replication.HealReplicationType, replication.ExistingObjectReplicationType:
+ fallthrough
+ default:
+ ch = p.getWorkerCh(doi.Bucket, doi.ObjectName, 0)
+ }
+
+ select {
+ case <-p.ctx.Done():
+ case ch <- doi:
+ default:
+ p.queueMRFSave(doi.ToMRFEntry())
+ p.mu.RLock()
+ prio := p.priority
+ maxWorkers := p.maxWorkers
+ p.mu.RUnlock()
+ switch prio {
+ case "fast":
+ replLogOnceIf(GlobalContext, fmt.Errorf("Unable to keep up with incoming deletes"), string(replicationSubsystem), logger.WarningKind)
+ case "slow":
+ replLogOnceIf(GlobalContext, fmt.Errorf("Unable to keep up with incoming deletes - we recommend increasing replication priority with `mc admin config set api replication_priority=auto`"), string(replicationSubsystem), logger.WarningKind)
+ default:
+ maxWorkers = min(maxWorkers, WorkerMaxLimit)
+ if p.ActiveWorkers() < maxWorkers {
+ p.mu.RLock()
+ workers := min(len(p.workers)+1, maxWorkers)
+ existing := len(p.workers)
+ p.mu.RUnlock()
+ p.ResizeWorkers(workers, existing)
+ }
+ }
+ }
+}
+
+type replicationPoolOpts struct {
+ Priority string
+ MaxWorkers int
+ MaxLWorkers int
+}
+
+func initBackgroundReplication(ctx context.Context, objectAPI ObjectLayer) {
+ stats := NewReplicationStats(ctx, objectAPI)
+ globalReplicationPool.Set(NewReplicationPool(ctx, objectAPI, globalAPIConfig.getReplicationOpts(), stats))
+ globalReplicationStats.Store(stats)
+ go stats.trackEWMA()
+}
+
+type proxyResult struct {
+ Proxy bool
+ Err error
+}
+
+// get Reader from replication target if active-active replication is in place and
+// this node returns a 404
+func proxyGetToReplicationTarget(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions, proxyTargets *madmin.BucketTargets) (gr *GetObjectReader, proxy proxyResult, err error) {
+ tgt, oi, proxy := proxyHeadToRepTarget(ctx, bucket, object, rs, opts, proxyTargets)
+ if !proxy.Proxy {
+ return nil, proxy, nil
+ }
+ fn, _, _, err := NewGetObjectReader(nil, oi, opts, h)
+ if err != nil {
+ return nil, proxy, err
+ }
+ gopts := minio.GetObjectOptions{
+ VersionID: opts.VersionID,
+ ServerSideEncryption: opts.ServerSideEncryption,
+ Internal: minio.AdvancedGetOptions{
+ ReplicationProxyRequest: "true",
+ },
+ PartNumber: opts.PartNumber,
+ }
+ // get correct offsets for encrypted object
+ if rs != nil {
+ h, err := rs.ToHeader()
+ if err != nil {
+ return nil, proxy, err
+ }
+ gopts.Set(xhttp.Range, h)
+ }
+ // Make sure to match ETag when proxying.
+ if err = gopts.SetMatchETag(oi.ETag); err != nil {
+ return nil, proxy, err
+ }
+ c := minio.Core{Client: tgt.Client}
+ obj, _, h, err := c.GetObject(ctx, tgt.Bucket, object, gopts)
+ if err != nil {
+ return nil, proxy, err
+ }
+ closeReader := func() { obj.Close() }
+ reader, err := fn(obj, h, closeReader)
+ if err != nil {
+ return nil, proxy, err
+ }
+ reader.ObjInfo = oi.Clone()
+ if rs != nil {
+ contentSize, err := parseSizeFromContentRange(h)
+ if err != nil {
+ return nil, proxy, err
+ }
+ reader.ObjInfo.Size = contentSize
+ }
+
+ return reader, proxyResult{Proxy: true}, nil
+}
+
+func getProxyTargets(ctx context.Context, bucket, object string, opts ObjectOptions) (tgts *madmin.BucketTargets) {
+ if opts.VersionSuspended {
+ return &madmin.BucketTargets{}
+ }
+ if opts.ProxyRequest || (opts.ProxyHeaderSet && !opts.ProxyRequest) {
+ return &madmin.BucketTargets{}
+ }
+ cfg, err := getReplicationConfig(ctx, bucket)
+ if err != nil || cfg == nil {
+ replLogOnceIf(ctx, err, bucket)
+
+ return &madmin.BucketTargets{}
+ }
+ topts := replication.ObjectOpts{Name: object}
+ tgtArns := cfg.FilterTargetArns(topts)
+ tgts = &madmin.BucketTargets{Targets: make([]madmin.BucketTarget, len(tgtArns))}
+ for i, tgtArn := range tgtArns {
+ tgt := globalBucketTargetSys.GetRemoteBucketTargetByArn(ctx, bucket, tgtArn)
+ tgts.Targets[i] = tgt
+ }
+
+ return tgts
+}
+
+func proxyHeadToRepTarget(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, opts ObjectOptions, proxyTargets *madmin.BucketTargets) (tgt *TargetClient, oi ObjectInfo, proxy proxyResult) {
+ // this option is set when active-active replication is in place between site A -> B,
+ // and site B does not have the object yet.
+ if opts.ProxyRequest || (opts.ProxyHeaderSet && !opts.ProxyRequest) { // true only when site B sets MinIOSourceProxyRequest header
+ return nil, oi, proxy
+ }
+ var perr error
+ for _, t := range proxyTargets.Targets {
+ tgt = globalBucketTargetSys.GetRemoteTargetClient(bucket, t.Arn)
+ if tgt == nil || globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
+ continue
+ }
+ // if proxying explicitly disabled on remote target
+ if tgt.disableProxy {
+ continue
+ }
+
+ gopts := minio.GetObjectOptions{
+ VersionID: opts.VersionID,
+ ServerSideEncryption: opts.ServerSideEncryption,
+ Internal: minio.AdvancedGetOptions{
+ ReplicationProxyRequest: "true",
+ },
+ PartNumber: opts.PartNumber,
+ }
+ if rs != nil {
+ h, err := rs.ToHeader()
+ if err != nil {
+ replLogIf(ctx, fmt.Errorf("invalid range header for %s/%s(%s) - %w", bucket, object, opts.VersionID, err))
+ continue
+ }
+ gopts.Set(xhttp.Range, h)
+ }
+
+ objInfo, err := tgt.StatObject(ctx, t.TargetBucket, object, gopts)
+ if err != nil {
+ perr = err
+ if isErrInvalidRange(ErrorRespToObjectError(err, bucket, object)) {
+ return nil, oi, proxyResult{Err: err}
+ }
+ continue
+ }
+
+ tags, _ := tags.MapToObjectTags(objInfo.UserTags)
+ oi = ObjectInfo{
+ Bucket: bucket,
+ Name: object,
+ ModTime: objInfo.LastModified,
+ Size: objInfo.Size,
+ ETag: objInfo.ETag,
+ VersionID: objInfo.VersionID,
+ IsLatest: objInfo.IsLatest,
+ DeleteMarker: objInfo.IsDeleteMarker,
+ ContentType: objInfo.ContentType,
+ Expires: objInfo.Expires,
+ StorageClass: objInfo.StorageClass,
+ ReplicationStatusInternal: objInfo.ReplicationStatus,
+ UserTags: tags.String(),
+ ReplicationStatus: replication.StatusType(objInfo.ReplicationStatus),
+ }
+ oi.UserDefined = make(map[string]string, len(objInfo.Metadata))
+ for k, v := range objInfo.Metadata {
+ oi.UserDefined[k] = v[0]
+ }
+ ce, ok := oi.UserDefined[xhttp.ContentEncoding]
+ if !ok {
+ ce, ok = oi.UserDefined[strings.ToLower(xhttp.ContentEncoding)]
+ }
+ if ok {
+ oi.ContentEncoding = ce
+ }
+ return tgt, oi, proxyResult{Proxy: true}
+ }
+ proxy.Err = perr
+ return nil, oi, proxy
+}
+
+// get object info from replication target if active-active replication is in place and
+// this node returns a 404
+func proxyHeadToReplicationTarget(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, opts ObjectOptions, proxyTargets *madmin.BucketTargets) (oi ObjectInfo, proxy proxyResult) {
+ _, oi, proxy = proxyHeadToRepTarget(ctx, bucket, object, rs, opts, proxyTargets)
+ return oi, proxy
+}
+
+func scheduleReplication(ctx context.Context, oi ObjectInfo, o ObjectLayer, dsc ReplicateDecision, opType replication.Type) {
+ tgtStatuses := replicationStatusesMap(oi.ReplicationStatusInternal)
+ purgeStatuses := versionPurgeStatusesMap(oi.VersionPurgeStatusInternal)
+ tm, _ := time.Parse(time.RFC3339Nano, oi.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp])
+ rstate := oi.ReplicationState()
+ rstate.ReplicateDecisionStr = dsc.String()
+ asz, _ := oi.GetActualSize()
+
+ ri := ReplicateObjectInfo{
+ Name: oi.Name,
+ Size: oi.Size,
+ ActualSize: asz,
+ Bucket: oi.Bucket,
+ VersionID: oi.VersionID,
+ ETag: oi.ETag,
+ ModTime: oi.ModTime,
+ ReplicationStatus: oi.ReplicationStatus,
+ ReplicationStatusInternal: oi.ReplicationStatusInternal,
+ DeleteMarker: oi.DeleteMarker,
+ VersionPurgeStatusInternal: oi.VersionPurgeStatusInternal,
+ VersionPurgeStatus: oi.VersionPurgeStatus,
+
+ ReplicationState: rstate,
+ OpType: opType,
+ Dsc: dsc,
+ TargetStatuses: tgtStatuses,
+ TargetPurgeStatuses: purgeStatuses,
+ ReplicationTimestamp: tm,
+ SSEC: crypto.SSEC.IsEncrypted(oi.UserDefined),
+ UserTags: oi.UserTags,
+ }
+ if ri.SSEC {
+ ri.Checksum = oi.Checksum
+ }
+ if dsc.Synchronous() {
+ replicateObject(ctx, ri, o)
+ } else {
+ globalReplicationPool.Get().queueReplicaTask(ri)
+ }
+}
+
+// proxyTaggingToRepTarget proxies tagging requests to remote targets for
+// active-active replicated setups
+func proxyTaggingToRepTarget(ctx context.Context, bucket, object string, tags *tags.Tags, opts ObjectOptions, proxyTargets *madmin.BucketTargets) (proxy proxyResult) {
+ // this option is set when active-active replication is in place between site A -> B,
+ // and request hits site B that does not have the object yet.
+ if opts.ProxyRequest || (opts.ProxyHeaderSet && !opts.ProxyRequest) { // true only when site B sets MinIOSourceProxyRequest header
+ return proxy
+ }
+ var wg sync.WaitGroup
+ errs := make([]error, len(proxyTargets.Targets))
+ for idx, t := range proxyTargets.Targets {
+ tgt := globalBucketTargetSys.GetRemoteTargetClient(bucket, t.Arn)
+ if tgt == nil || globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
+ continue
+ }
+ // if proxying explicitly disabled on remote target
+ if tgt.disableProxy {
+ continue
+ }
+ idx := idx
+ wg.Add(1)
+ go func(idx int, tgt *TargetClient) {
+ defer wg.Done()
+ var err error
+ if tags != nil {
+ popts := minio.PutObjectTaggingOptions{
+ VersionID: opts.VersionID,
+ Internal: minio.AdvancedObjectTaggingOptions{
+ ReplicationProxyRequest: "true",
+ },
+ }
+ err = tgt.PutObjectTagging(ctx, tgt.Bucket, object, tags, popts)
+ } else {
+ dopts := minio.RemoveObjectTaggingOptions{
+ VersionID: opts.VersionID,
+ Internal: minio.AdvancedObjectTaggingOptions{
+ ReplicationProxyRequest: "true",
+ },
+ }
+ err = tgt.RemoveObjectTagging(ctx, tgt.Bucket, object, dopts)
+ }
+ if err != nil {
+ errs[idx] = err
+ }
+ }(idx, tgt)
+ }
+ wg.Wait()
+
+ var (
+ terr error
+ taggedCount int
+ )
+ for _, err := range errs {
+ if err == nil {
+ taggedCount++
+ continue
+ }
+ if err != nil {
+ terr = err
+ }
+ }
+ // don't return error if at least one target was tagged successfully
+ if taggedCount == 0 && terr != nil {
+ proxy.Err = terr
+ }
+ return proxy
+}
+
+// proxyGetTaggingToRepTarget proxies get tagging requests to remote targets for
+// active-active replicated setups
+func proxyGetTaggingToRepTarget(ctx context.Context, bucket, object string, opts ObjectOptions, proxyTargets *madmin.BucketTargets) (tgs *tags.Tags, proxy proxyResult) {
+ // this option is set when active-active replication is in place between site A -> B,
+ // and request hits site B that does not have the object yet.
+ if opts.ProxyRequest || (opts.ProxyHeaderSet && !opts.ProxyRequest) { // true only when site B sets MinIOSourceProxyRequest header
+ return nil, proxy
+ }
+ var wg sync.WaitGroup
+ errs := make([]error, len(proxyTargets.Targets))
+ tagSlc := make([]map[string]string, len(proxyTargets.Targets))
+ for idx, t := range proxyTargets.Targets {
+ tgt := globalBucketTargetSys.GetRemoteTargetClient(bucket, t.Arn)
+ if tgt == nil || globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
+ continue
+ }
+ // if proxying explicitly disabled on remote target
+ if tgt.disableProxy {
+ continue
+ }
+ idx := idx
+ wg.Add(1)
+ go func(idx int, tgt *TargetClient) {
+ defer wg.Done()
+ var err error
+ gopts := minio.GetObjectTaggingOptions{
+ VersionID: opts.VersionID,
+ Internal: minio.AdvancedObjectTaggingOptions{
+ ReplicationProxyRequest: "true",
+ },
+ }
+ tgs, err = tgt.GetObjectTagging(ctx, tgt.Bucket, object, gopts)
+ if err != nil {
+ errs[idx] = err
+ } else {
+ tagSlc[idx] = tgs.ToMap()
+ }
+ }(idx, tgt)
+ }
+ wg.Wait()
+ for idx, err := range errs {
+ errCode := minio.ToErrorResponse(err).Code
+ if err != nil && errCode != "NoSuchKey" && errCode != "NoSuchVersion" {
+ return nil, proxyResult{Err: err}
+ }
+ if err == nil {
+ tgs, _ = tags.MapToObjectTags(tagSlc[idx])
+ }
+ }
+ if len(errs) == 1 {
+ proxy.Err = errs[0]
+ }
+ return tgs, proxy
+}
+
+func scheduleReplicationDelete(ctx context.Context, dv DeletedObjectReplicationInfo, o ObjectLayer) {
+ globalReplicationPool.Get().queueReplicaDeleteTask(dv)
+ for arn := range dv.ReplicationState.Targets {
+ globalReplicationStats.Load().Update(dv.Bucket, replicatedTargetInfo{Arn: arn, Size: 0, Duration: 0, OpType: replication.DeleteReplicationType}, replication.Pending, replication.StatusType(""))
+ }
+}
+
+type replicationConfig struct {
+ Config *replication.Config
+ remotes *madmin.BucketTargets
+}
+
+func (c replicationConfig) Empty() bool {
+ return c.Config == nil
+}
+
+func (c replicationConfig) Replicate(opts replication.ObjectOpts) bool {
+ return c.Config.Replicate(opts)
+}
+
+// Resync returns true if replication reset is requested
+func (c replicationConfig) Resync(ctx context.Context, oi ObjectInfo, dsc ReplicateDecision, tgtStatuses map[string]replication.StatusType) (r ResyncDecision) {
+ if c.Empty() {
+ return r
+ }
+
+ // Now overlay existing object replication choices for target
+ if oi.DeleteMarker {
+ opts := replication.ObjectOpts{
+ Name: oi.Name,
+ DeleteMarker: oi.DeleteMarker,
+ VersionID: oi.VersionID,
+ OpType: replication.DeleteReplicationType,
+ ExistingObject: true,
+ }
+
+ tgtArns := c.Config.FilterTargetArns(opts)
+ // indicates no matching target with Existing object replication enabled.
+ if len(tgtArns) == 0 {
+ return r
+ }
+ for _, t := range tgtArns {
+ opts.TargetArn = t
+ // Update replication decision for target based on existing object replciation rule.
+ dsc.Set(newReplicateTargetDecision(t, c.Replicate(opts), false))
+ }
+ return c.resync(oi, dsc, tgtStatuses)
+ }
+
+ // Ignore previous replication status when deciding if object can be re-replicated
+ userDefined := cloneMSS(oi.UserDefined)
+ delete(userDefined, xhttp.AmzBucketReplicationStatus)
+
+ rdsc := mustReplicate(ctx, oi.Bucket, oi.Name, getMustReplicateOptions(userDefined, oi.UserTags, "", replication.ExistingObjectReplicationType, ObjectOptions{}))
+ return c.resync(oi, rdsc, tgtStatuses)
+}
+
+// wrapper function for testability. Returns true if a new reset is requested on
+// already replicated objects OR object qualifies for existing object replication
+// and no reset requested.
+func (c replicationConfig) resync(oi ObjectInfo, dsc ReplicateDecision, tgtStatuses map[string]replication.StatusType) (r ResyncDecision) {
+ r = ResyncDecision{
+ targets: make(map[string]ResyncTargetDecision, len(dsc.targetsMap)),
+ }
+ if c.remotes == nil {
+ return r
+ }
+ for _, tgt := range c.remotes.Targets {
+ d, ok := dsc.targetsMap[tgt.Arn]
+ if !ok {
+ continue
+ }
+ if !d.Replicate {
+ continue
+ }
+ r.targets[d.Arn] = resyncTarget(oi, tgt.Arn, tgt.ResetID, tgt.ResetBeforeDate, tgtStatuses[tgt.Arn])
+ }
+ return r
+}
+
+func targetResetHeader(arn string) string {
+ return fmt.Sprintf("%s-%s", ReservedMetadataPrefixLower+ReplicationReset, arn)
+}
+
+func resyncTarget(oi ObjectInfo, arn string, resetID string, resetBeforeDate time.Time, tgtStatus replication.StatusType) (rd ResyncTargetDecision) {
+ rd = ResyncTargetDecision{
+ ResetID: resetID,
+ ResetBeforeDate: resetBeforeDate,
+ }
+ rs, ok := oi.UserDefined[targetResetHeader(arn)]
+ if !ok {
+ rs, ok = oi.UserDefined[xhttp.MinIOReplicationResetStatus] // for backward compatibility
+ }
+ if !ok { // existing object replication is enabled and object version is unreplicated so far.
+ if resetID != "" && oi.ModTime.Before(resetBeforeDate) { // trigger replication if `mc replicate reset` requested
+ rd.Replicate = true
+ return rd
+ }
+ // For existing object reset - this condition is needed
+ rd.Replicate = tgtStatus == ""
+ return rd
+ }
+ if resetID == "" || resetBeforeDate.Equal(timeSentinel) { // no reset in progress
+ return rd
+ }
+
+ // if already replicated, return true if a new reset was requested.
+ splits := strings.SplitN(rs, ";", 2)
+ if len(splits) != 2 {
+ return rd
+ }
+ newReset := splits[1] != resetID
+ if !newReset && tgtStatus == replication.Completed {
+ // already replicated and no reset requested
+ return rd
+ }
+ rd.Replicate = newReset && oi.ModTime.Before(resetBeforeDate)
+ return rd
+}
+
+const resyncTimeInterval = time.Minute * 1
+
+// PersistToDisk persists in-memory resync metadata stats to disk at periodic intervals
+func (s *replicationResyncer) PersistToDisk(ctx context.Context, objectAPI ObjectLayer) {
+ resyncTimer := time.NewTimer(resyncTimeInterval)
+ defer resyncTimer.Stop()
+
+ // For each bucket name, store the last timestamp of the
+ // successful save of replication status in the backend disks.
+ lastResyncStatusSave := make(map[string]time.Time)
+
+ for {
+ select {
+ case <-resyncTimer.C:
+ s.RLock()
+ for bucket, brs := range s.statusMap {
+ var updt bool
+ // Save the replication status if one resync to any bucket target is still not finished
+ for _, st := range brs.TargetsMap {
+ if st.LastUpdate.Equal(timeSentinel) {
+ updt = true
+ break
+ }
+ }
+ // Save the replication status if a new stats update is found and not saved in the backend yet
+ if brs.LastUpdate.After(lastResyncStatusSave[bucket]) {
+ updt = true
+ }
+ if updt {
+ if err := saveResyncStatus(ctx, bucket, brs, objectAPI); err != nil {
+ replLogIf(ctx, fmt.Errorf("could not save resync metadata to drive for %s - %w", bucket, err))
+ } else {
+ lastResyncStatusSave[bucket] = brs.LastUpdate
+ }
+ }
+ }
+ s.RUnlock()
+
+ resyncTimer.Reset(resyncTimeInterval)
+ case <-ctx.Done():
+ // server could be restarting - need
+ // to exit immediately
+ return
+ }
+ }
+}
+
+const (
+ resyncWorkerCnt = 10 // limit of number of bucket resyncs is progress at any given time
+ resyncParallelRoutines = 10 // number of parallel resync ops per bucket
+)
+
+func newresyncer() *replicationResyncer {
+ rs := replicationResyncer{
+ statusMap: make(map[string]BucketReplicationResyncStatus),
+ workerSize: resyncWorkerCnt,
+ resyncCancelCh: make(chan struct{}, resyncWorkerCnt),
+ workerCh: make(chan struct{}, resyncWorkerCnt),
+ }
+ for i := 0; i < rs.workerSize; i++ {
+ rs.workerCh <- struct{}{}
+ }
+ return &rs
+}
+
+// mark status of replication resync on remote target for the bucket
+func (s *replicationResyncer) markStatus(status ResyncStatusType, opts resyncOpts, objAPI ObjectLayer) {
+ s.Lock()
+ defer s.Unlock()
+
+ m := s.statusMap[opts.bucket]
+ st := m.TargetsMap[opts.arn]
+ st.LastUpdate = UTCNow()
+ st.ResyncStatus = status
+ m.TargetsMap[opts.arn] = st
+ m.LastUpdate = UTCNow()
+ s.statusMap[opts.bucket] = m
+
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ saveResyncStatus(ctx, opts.bucket, m, objAPI)
+}
+
+// update replication resync stats for bucket's remote target
+func (s *replicationResyncer) incStats(ts TargetReplicationResyncStatus, opts resyncOpts) {
+ s.Lock()
+ defer s.Unlock()
+ m := s.statusMap[opts.bucket]
+ st := m.TargetsMap[opts.arn]
+ st.Object = ts.Object
+ st.ReplicatedCount += ts.ReplicatedCount
+ st.FailedCount += ts.FailedCount
+ st.ReplicatedSize += ts.ReplicatedSize
+ st.FailedSize += ts.FailedSize
+ m.TargetsMap[opts.arn] = st
+ m.LastUpdate = UTCNow()
+ s.statusMap[opts.bucket] = m
+}
+
+// resyncBucket resyncs all qualifying objects as per replication rules for the target
+// ARN
+func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI ObjectLayer, heal bool, opts resyncOpts) {
+ select {
+ case <-s.workerCh: // block till a worker is available
+ case <-ctx.Done():
+ return
+ }
+
+ resyncStatus := ResyncFailed
+ defer func() {
+ s.markStatus(resyncStatus, opts, objectAPI)
+ globalSiteResyncMetrics.incBucket(opts, resyncStatus)
+ s.workerCh <- struct{}{}
+ }()
+ // Allocate new results channel to receive ObjectInfo.
+ objInfoCh := make(chan itemOrErr[ObjectInfo])
+ cfg, err := getReplicationConfig(ctx, opts.bucket)
+ if err != nil {
+ replLogIf(ctx, fmt.Errorf("replication resync of %s for arn %s failed with %w", opts.bucket, opts.arn, err))
+ return
+ }
+ tgts, err := globalBucketTargetSys.ListBucketTargets(ctx, opts.bucket)
+ if err != nil {
+ replLogIf(ctx, fmt.Errorf("replication resync of %s for arn %s failed %w", opts.bucket, opts.arn, err))
+ return
+ }
+ rcfg := replicationConfig{
+ Config: cfg,
+ remotes: tgts,
+ }
+ tgtArns := cfg.FilterTargetArns(
+ replication.ObjectOpts{
+ OpType: replication.ResyncReplicationType,
+ TargetArn: opts.arn,
+ })
+ if len(tgtArns) != 1 {
+ replLogIf(ctx, fmt.Errorf("replication resync failed for %s - arn specified %s is missing in the replication config", opts.bucket, opts.arn))
+ return
+ }
+ tgt := globalBucketTargetSys.GetRemoteTargetClient(opts.bucket, opts.arn)
+ if tgt == nil {
+ replLogIf(ctx, fmt.Errorf("replication resync failed for %s - target could not be created for arn %s", opts.bucket, opts.arn))
+ return
+ }
+ // mark resync status as resync started
+ if !heal {
+ s.markStatus(ResyncStarted, opts, objectAPI)
+ }
+
+ // Walk through all object versions - Walk() is always in ascending order needed to ensure
+ // delete marker replicated to target after object version is first created.
+ if err := objectAPI.Walk(ctx, opts.bucket, "", objInfoCh, WalkOptions{}); err != nil {
+ replLogIf(ctx, err)
+ return
+ }
+
+ s.RLock()
+ m := s.statusMap[opts.bucket]
+ st := m.TargetsMap[opts.arn]
+ s.RUnlock()
+ var lastCheckpoint string
+ if st.ResyncStatus == ResyncStarted || st.ResyncStatus == ResyncFailed {
+ lastCheckpoint = st.Object
+ }
+ workers := make([]chan ReplicateObjectInfo, resyncParallelRoutines)
+ resultCh := make(chan TargetReplicationResyncStatus, 1)
+ defer xioutil.SafeClose(resultCh)
+ go func() {
+ for r := range resultCh {
+ s.incStats(r, opts)
+ globalSiteResyncMetrics.updateMetric(r, opts.resyncID)
+ }
+ }()
+
+ var wg sync.WaitGroup
+ for i := range resyncParallelRoutines {
+ wg.Add(1)
+ workers[i] = make(chan ReplicateObjectInfo, 100)
+ i := i
+ go func(ctx context.Context, idx int) {
+ defer wg.Done()
+ for roi := range workers[idx] {
+ select {
+ case <-ctx.Done():
+ return
+ case <-s.resyncCancelCh:
+ default:
+ }
+ traceFn := s.trace(tgt.ResetID, fmt.Sprintf("%s/%s (%s)", opts.bucket, roi.Name, roi.VersionID))
+ if roi.DeleteMarker || !roi.VersionPurgeStatus.Empty() {
+ versionID := ""
+ dmVersionID := ""
+ if roi.VersionPurgeStatus.Empty() {
+ dmVersionID = roi.VersionID
+ } else {
+ versionID = roi.VersionID
+ }
+
+ doi := DeletedObjectReplicationInfo{
+ DeletedObject: DeletedObject{
+ ObjectName: roi.Name,
+ DeleteMarkerVersionID: dmVersionID,
+ VersionID: versionID,
+ ReplicationState: roi.ReplicationState,
+ DeleteMarkerMTime: DeleteMarkerMTime{roi.ModTime},
+ DeleteMarker: roi.DeleteMarker,
+ },
+ Bucket: roi.Bucket,
+ OpType: replication.ExistingObjectReplicationType,
+ EventType: ReplicateExistingDelete,
+ }
+ replicateDelete(ctx, doi, objectAPI)
+ } else {
+ roi.OpType = replication.ExistingObjectReplicationType
+ roi.EventType = ReplicateExisting
+ replicateObject(ctx, roi, objectAPI)
+ }
+
+ st := TargetReplicationResyncStatus{
+ Object: roi.Name,
+ Bucket: roi.Bucket,
+ }
+
+ _, err := tgt.StatObject(ctx, tgt.Bucket, roi.Name, minio.StatObjectOptions{
+ VersionID: roi.VersionID,
+ Internal: minio.AdvancedGetOptions{
+ ReplicationProxyRequest: "false",
+ },
+ })
+ sz := roi.Size
+ if err != nil {
+ if roi.DeleteMarker && isErrMethodNotAllowed(ErrorRespToObjectError(err, opts.bucket, roi.Name)) {
+ st.ReplicatedCount++
+ } else {
+ st.FailedCount++
+ }
+ sz = 0
+ } else {
+ st.ReplicatedCount++
+ st.ReplicatedSize += roi.Size
+ }
+ traceFn(sz, err)
+ select {
+ case <-ctx.Done():
+ return
+ case <-s.resyncCancelCh:
+ return
+ case resultCh <- st:
+ }
+ }
+ }(ctx, i)
+ }
+ for res := range objInfoCh {
+ if res.Err != nil {
+ resyncStatus = ResyncFailed
+ replLogIf(ctx, res.Err)
+ return
+ }
+ select {
+ case <-s.resyncCancelCh:
+ resyncStatus = ResyncCanceled
+ return
+ case <-ctx.Done():
+ return
+ default:
+ }
+ if heal && lastCheckpoint != "" && lastCheckpoint != res.Item.Name {
+ continue
+ }
+ lastCheckpoint = ""
+ roi := getHealReplicateObjectInfo(res.Item, rcfg)
+ if !roi.ExistingObjResync.mustResync() {
+ continue
+ }
+ select {
+ case <-s.resyncCancelCh:
+ return
+ case <-ctx.Done():
+ return
+ default:
+ h := xxh3.HashString(roi.Bucket + roi.Name)
+ workers[h%uint64(resyncParallelRoutines)] <- roi
+ }
+ }
+ for i := range resyncParallelRoutines {
+ xioutil.SafeClose(workers[i])
+ }
+ wg.Wait()
+ resyncStatus = ResyncCompleted
+}
+
+// start replication resync for the remote target ARN specified
+func (s *replicationResyncer) start(ctx context.Context, objAPI ObjectLayer, opts resyncOpts) error {
+ if opts.bucket == "" {
+ return fmt.Errorf("bucket name is empty")
+ }
+ if opts.arn == "" {
+ return fmt.Errorf("target ARN specified for resync is empty")
+ }
+ // Check if the current bucket has quota restrictions, if not skip it
+ cfg, err := getReplicationConfig(ctx, opts.bucket)
+ if err != nil {
+ return err
+ }
+ tgtArns := cfg.FilterTargetArns(
+ replication.ObjectOpts{
+ OpType: replication.ResyncReplicationType,
+ TargetArn: opts.arn,
+ })
+
+ if len(tgtArns) == 0 {
+ return fmt.Errorf("arn %s specified for resync not found in replication config", opts.arn)
+ }
+ globalReplicationPool.Get().resyncer.RLock()
+ data, ok := globalReplicationPool.Get().resyncer.statusMap[opts.bucket]
+ globalReplicationPool.Get().resyncer.RUnlock()
+ if !ok {
+ data, err = loadBucketResyncMetadata(ctx, opts.bucket, objAPI)
+ if err != nil {
+ return err
+ }
+ }
+ // validate if resync is in progress for this arn
+ for tArn, st := range data.TargetsMap {
+ if opts.arn == tArn && (st.ResyncStatus == ResyncStarted || st.ResyncStatus == ResyncPending) {
+ return fmt.Errorf("Resync of bucket %s is already in progress for remote bucket %s", opts.bucket, opts.arn)
+ }
+ }
+
+ status := TargetReplicationResyncStatus{
+ ResyncID: opts.resyncID,
+ ResyncBeforeDate: opts.resyncBefore,
+ StartTime: UTCNow(),
+ ResyncStatus: ResyncPending,
+ Bucket: opts.bucket,
+ }
+ data.TargetsMap[opts.arn] = status
+ if err = saveResyncStatus(ctx, opts.bucket, data, objAPI); err != nil {
+ return err
+ }
+
+ globalReplicationPool.Get().resyncer.Lock()
+ defer globalReplicationPool.Get().resyncer.Unlock()
+ brs, ok := globalReplicationPool.Get().resyncer.statusMap[opts.bucket]
+ if !ok {
+ brs = BucketReplicationResyncStatus{
+ Version: resyncMetaVersion,
+ TargetsMap: make(map[string]TargetReplicationResyncStatus),
+ }
+ }
+ brs.TargetsMap[opts.arn] = status
+ globalReplicationPool.Get().resyncer.statusMap[opts.bucket] = brs
+ go globalReplicationPool.Get().resyncer.resyncBucket(GlobalContext, objAPI, false, opts)
+ return nil
+}
+
+func (s *replicationResyncer) trace(resyncID string, path string) func(sz int64, err error) {
+ startTime := time.Now()
+ return func(sz int64, err error) {
+ duration := time.Since(startTime)
+ if globalTrace.NumSubscribers(madmin.TraceReplicationResync) > 0 {
+ globalTrace.Publish(replicationResyncTrace(resyncID, startTime, duration, path, err, sz))
+ }
+ }
+}
+
+func replicationResyncTrace(resyncID string, startTime time.Time, duration time.Duration, path string, err error, sz int64) madmin.TraceInfo {
+ var errStr string
+ if err != nil {
+ errStr = err.Error()
+ }
+ funcName := fmt.Sprintf("replication.(resyncID=%s)", resyncID)
+ return madmin.TraceInfo{
+ TraceType: madmin.TraceReplicationResync,
+ Time: startTime,
+ NodeName: globalLocalNodeName,
+ FuncName: funcName,
+ Duration: duration,
+ Path: path,
+ Error: errStr,
+ Bytes: sz,
+ }
+}
+
+// delete resync metadata from replication resync state in memory
+func (p *ReplicationPool) deleteResyncMetadata(ctx context.Context, bucket string) {
+ if p == nil {
+ return
+ }
+ p.resyncer.Lock()
+ delete(p.resyncer.statusMap, bucket)
+ defer p.resyncer.Unlock()
+
+ globalSiteResyncMetrics.deleteBucket(bucket)
+}
+
+// initResync - initializes bucket replication resync for all buckets.
+func (p *ReplicationPool) initResync(ctx context.Context, buckets []string, objAPI ObjectLayer) error {
+ if objAPI == nil {
+ return errServerNotInitialized
+ }
+ // Load bucket metadata sys in background
+ go p.startResyncRoutine(ctx, buckets, objAPI)
+ return nil
+}
+
+func (p *ReplicationPool) startResyncRoutine(ctx context.Context, buckets []string, objAPI ObjectLayer) {
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+ // Run the replication resync in a loop
+ for {
+ if err := p.loadResync(ctx, buckets, objAPI); err == nil {
+ <-ctx.Done()
+ return
+ }
+ duration := max(time.Duration(r.Float64()*float64(time.Minute)),
+ // Make sure to sleep at least a second to avoid high CPU ticks.
+ time.Second)
+ time.Sleep(duration)
+ }
+}
+
+// Loads bucket replication resync statuses into memory.
+func (p *ReplicationPool) loadResync(ctx context.Context, buckets []string, objAPI ObjectLayer) error {
+ // Make sure only one node running resync on the cluster.
+ ctx, cancel := globalLeaderLock.GetLock(ctx)
+ defer cancel()
+
+ for index := range buckets {
+ bucket := buckets[index]
+
+ meta, err := loadBucketResyncMetadata(ctx, bucket, objAPI)
+ if err != nil {
+ if !errors.Is(err, errVolumeNotFound) {
+ replLogIf(ctx, err)
+ }
+ continue
+ }
+
+ p.resyncer.Lock()
+ p.resyncer.statusMap[bucket] = meta
+ p.resyncer.Unlock()
+
+ tgts := meta.cloneTgtStats()
+ for arn, st := range tgts {
+ switch st.ResyncStatus {
+ case ResyncFailed, ResyncStarted, ResyncPending:
+ go p.resyncer.resyncBucket(ctx, objAPI, true, resyncOpts{
+ bucket: bucket,
+ arn: arn,
+ resyncID: st.ResyncID,
+ resyncBefore: st.ResyncBeforeDate,
+ })
+ }
+ }
+ }
+ return nil
+}
+
+// load bucket resync metadata from disk
+func loadBucketResyncMetadata(ctx context.Context, bucket string, objAPI ObjectLayer) (brs BucketReplicationResyncStatus, e error) {
+ brs = newBucketResyncStatus(bucket)
+ resyncDirPath := path.Join(bucketMetaPrefix, bucket, replicationDir)
+ data, err := readConfig(GlobalContext, objAPI, pathJoin(resyncDirPath, resyncFileName))
+ if err != nil && err != errConfigNotFound {
+ return brs, err
+ }
+ if len(data) == 0 {
+ // Seems to be empty.
+ return brs, nil
+ }
+ if len(data) <= 4 {
+ return brs, fmt.Errorf("replication resync: no data")
+ }
+ // Read resync meta header
+ switch binary.LittleEndian.Uint16(data[0:2]) {
+ case resyncMetaFormat:
+ default:
+ return brs, fmt.Errorf("resyncMeta: unknown format: %d", binary.LittleEndian.Uint16(data[0:2]))
+ }
+ switch binary.LittleEndian.Uint16(data[2:4]) {
+ case resyncMetaVersion:
+ default:
+ return brs, fmt.Errorf("resyncMeta: unknown version: %d", binary.LittleEndian.Uint16(data[2:4]))
+ }
+ // OK, parse data.
+ if _, err = brs.UnmarshalMsg(data[4:]); err != nil {
+ return brs, err
+ }
+
+ switch brs.Version {
+ case resyncMetaVersionV1:
+ default:
+ return brs, fmt.Errorf("unexpected resync meta version: %d", brs.Version)
+ }
+ return brs, nil
+}
+
+// save resync status to resync.bin
+func saveResyncStatus(ctx context.Context, bucket string, brs BucketReplicationResyncStatus, objectAPI ObjectLayer) error {
+ data := make([]byte, 4, brs.Msgsize()+4)
+
+ // Initialize the resync meta header.
+ binary.LittleEndian.PutUint16(data[0:2], resyncMetaFormat)
+ binary.LittleEndian.PutUint16(data[2:4], resyncMetaVersion)
+
+ buf, err := brs.MarshalMsg(data)
+ if err != nil {
+ return err
+ }
+
+ configFile := path.Join(bucketMetaPrefix, bucket, replicationDir, resyncFileName)
+ return saveConfig(ctx, objectAPI, configFile, buf)
+}
+
+// getReplicationDiff returns un-replicated objects in a channel.
+// If a non-nil channel is returned it must be consumed fully or
+// the provided context must be canceled.
+func getReplicationDiff(ctx context.Context, objAPI ObjectLayer, bucket string, opts madmin.ReplDiffOpts) (chan madmin.DiffInfo, error) {
+ cfg, err := getReplicationConfig(ctx, bucket)
+ if err != nil {
+ replLogOnceIf(ctx, err, bucket)
+ return nil, err
+ }
+ tgts, err := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
+ if err != nil {
+ replLogIf(ctx, err)
+ return nil, err
+ }
+
+ objInfoCh := make(chan itemOrErr[ObjectInfo], 10)
+ if err := objAPI.Walk(ctx, bucket, opts.Prefix, objInfoCh, WalkOptions{}); err != nil {
+ replLogIf(ctx, err)
+ return nil, err
+ }
+ rcfg := replicationConfig{
+ Config: cfg,
+ remotes: tgts,
+ }
+ diffCh := make(chan madmin.DiffInfo, 4000)
+ go func() {
+ defer xioutil.SafeClose(diffCh)
+ for res := range objInfoCh {
+ if res.Err != nil {
+ diffCh <- madmin.DiffInfo{Err: res.Err}
+ return
+ }
+ if contextCanceled(ctx) {
+ // Just consume input...
+ continue
+ }
+ obj := res.Item
+
+ // Ignore object prefixes which are excluded
+ // from versioning via the MinIO bucket versioning extension.
+ if globalBucketVersioningSys.PrefixSuspended(bucket, obj.Name) {
+ continue
+ }
+ roi := getHealReplicateObjectInfo(obj, rcfg)
+ switch roi.ReplicationStatus {
+ case replication.Completed, replication.Replica:
+ if !opts.Verbose {
+ continue
+ }
+ fallthrough
+ default:
+ // ignore pre-existing objects that don't satisfy replication rule(s)
+ if roi.ReplicationStatus.Empty() && !roi.ExistingObjResync.mustResync() {
+ continue
+ }
+ tgtsMap := make(map[string]madmin.TgtDiffInfo)
+ for arn, st := range roi.TargetStatuses {
+ if opts.ARN == "" || opts.ARN == arn {
+ if !opts.Verbose && (st == replication.Completed || st == replication.Replica) {
+ continue
+ }
+ tgtsMap[arn] = madmin.TgtDiffInfo{
+ ReplicationStatus: st.String(),
+ }
+ }
+ }
+ for arn, st := range roi.TargetPurgeStatuses {
+ if opts.ARN == "" || opts.ARN == arn {
+ if !opts.Verbose && st == replication.VersionPurgeComplete {
+ continue
+ }
+ t, ok := tgtsMap[arn]
+ if !ok {
+ t = madmin.TgtDiffInfo{}
+ }
+ t.DeleteReplicationStatus = string(st)
+ tgtsMap[arn] = t
+ }
+ }
+ select {
+ case diffCh <- madmin.DiffInfo{
+ Object: obj.Name,
+ VersionID: obj.VersionID,
+ LastModified: obj.ModTime,
+ IsDeleteMarker: obj.DeleteMarker,
+ ReplicationStatus: string(roi.ReplicationStatus),
+ DeleteReplicationStatus: string(roi.VersionPurgeStatus),
+ ReplicationTimestamp: roi.ReplicationTimestamp,
+ Targets: tgtsMap,
+ }:
+ case <-ctx.Done():
+ continue
+ }
+ }
+ }
+ }()
+ return diffCh, nil
+}
+
+// QueueReplicationHeal is a wrapper for queueReplicationHeal
+func QueueReplicationHeal(ctx context.Context, bucket string, oi ObjectInfo, retryCount int) {
+ // ignore modtime zero objects
+ if oi.ModTime.IsZero() {
+ return
+ }
+ rcfg, err := getReplicationConfig(ctx, bucket)
+ if err != nil {
+ replLogOnceIf(ctx, err, bucket)
+ return
+ }
+ tgts, _ := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
+ queueReplicationHeal(ctx, bucket, oi, replicationConfig{
+ Config: rcfg,
+ remotes: tgts,
+ }, retryCount)
+}
+
+// queueReplicationHeal enqueues objects that failed replication OR eligible for resyncing through
+// an ongoing resync operation or via existing objects replication configuration setting.
+func queueReplicationHeal(ctx context.Context, bucket string, oi ObjectInfo, rcfg replicationConfig, retryCount int) (roi ReplicateObjectInfo) {
+ // ignore modtime zero objects
+ if oi.ModTime.IsZero() {
+ return roi
+ }
+
+ if isVeeamSOSAPIObject(oi.Name) {
+ return roi
+ }
+ if rcfg.Config == nil || rcfg.remotes == nil {
+ return roi
+ }
+ roi = getHealReplicateObjectInfo(oi, rcfg)
+ roi.RetryCount = uint32(retryCount)
+ if !roi.Dsc.ReplicateAny() {
+ return roi
+ }
+ // early return if replication already done, otherwise we need to determine if this
+ // version is an existing object that needs healing.
+ if oi.ReplicationStatus == replication.Completed && oi.VersionPurgeStatus.Empty() && !roi.ExistingObjResync.mustResync() {
+ return roi
+ }
+
+ if roi.DeleteMarker || !roi.VersionPurgeStatus.Empty() {
+ versionID := ""
+ dmVersionID := ""
+ if roi.VersionPurgeStatus.Empty() {
+ dmVersionID = roi.VersionID
+ } else {
+ versionID = roi.VersionID
+ }
+
+ dv := DeletedObjectReplicationInfo{
+ DeletedObject: DeletedObject{
+ ObjectName: roi.Name,
+ DeleteMarkerVersionID: dmVersionID,
+ VersionID: versionID,
+ ReplicationState: roi.ReplicationState,
+ DeleteMarkerMTime: DeleteMarkerMTime{roi.ModTime},
+ DeleteMarker: roi.DeleteMarker,
+ },
+ Bucket: roi.Bucket,
+ OpType: replication.HealReplicationType,
+ EventType: ReplicateHealDelete,
+ }
+ // heal delete marker replication failure or versioned delete replication failure
+ if roi.ReplicationStatus == replication.Pending ||
+ roi.ReplicationStatus == replication.Failed ||
+ roi.VersionPurgeStatus == replication.VersionPurgeFailed || roi.VersionPurgeStatus == replication.VersionPurgePending {
+ globalReplicationPool.Get().queueReplicaDeleteTask(dv)
+ return roi
+ }
+ // if replication status is Complete on DeleteMarker and existing object resync required
+ if roi.ExistingObjResync.mustResync() && (roi.ReplicationStatus == replication.Completed || roi.ReplicationStatus.Empty()) {
+ queueReplicateDeletesWrapper(dv, roi.ExistingObjResync)
+ return roi
+ }
+ return roi
+ }
+ if roi.ExistingObjResync.mustResync() {
+ roi.OpType = replication.ExistingObjectReplicationType
+ }
+ switch roi.ReplicationStatus {
+ case replication.Pending, replication.Failed:
+ roi.EventType = ReplicateHeal
+ globalReplicationPool.Get().queueReplicaTask(roi)
+ return roi
+ }
+ if roi.ExistingObjResync.mustResync() {
+ roi.EventType = ReplicateExisting
+ globalReplicationPool.Get().queueReplicaTask(roi)
+ }
+ return roi
+}
+
+const (
+ mrfSaveInterval = 5 * time.Minute
+ mrfQueueInterval = mrfSaveInterval + time.Minute // A minute higher than save interval
+
+ mrfRetryLimit = 3 // max number of retries before letting scanner catch up on this object version
+ mrfMaxEntries = 1000000
+)
+
+func (p *ReplicationPool) persistMRF() {
+ if !p.initialized() {
+ return
+ }
+
+ entries := make(map[string]MRFReplicateEntry)
+ mTimer := time.NewTimer(mrfSaveInterval)
+ defer mTimer.Stop()
+
+ saveMRFToDisk := func() {
+ if len(entries) == 0 {
+ return
+ }
+
+ // queue all entries for healing before overwriting the node mrf file
+ if !contextCanceled(p.ctx) {
+ p.queueMRFHeal()
+ }
+
+ p.saveMRFEntries(p.ctx, entries)
+
+ entries = make(map[string]MRFReplicateEntry)
+ }
+ for {
+ select {
+ case <-mTimer.C:
+ saveMRFToDisk()
+ mTimer.Reset(mrfSaveInterval)
+ case <-p.ctx.Done():
+ p.mrfStopCh <- struct{}{}
+ xioutil.SafeClose(p.mrfSaveCh)
+ // We try to save if possible, but we don't care beyond that.
+ saveMRFToDisk()
+ return
+ case e, ok := <-p.mrfSaveCh:
+ if !ok {
+ return
+ }
+ entries[e.versionID] = e
+
+ if len(entries) >= mrfMaxEntries {
+ saveMRFToDisk()
+ }
+ }
+ }
+}
+
+func (p *ReplicationPool) queueMRFSave(entry MRFReplicateEntry) {
+ if !p.initialized() {
+ return
+ }
+ if entry.RetryCount > mrfRetryLimit { // let scanner catch up if retry count exceeded
+ atomic.AddUint64(&p.stats.mrfStats.TotalDroppedCount, 1)
+ atomic.AddUint64(&p.stats.mrfStats.TotalDroppedBytes, uint64(entry.sz))
+ return
+ }
+
+ select {
+ case <-GlobalContext.Done():
+ return
+ case <-p.mrfStopCh:
+ return
+ default:
+ select {
+ case p.mrfSaveCh <- entry:
+ default:
+ atomic.AddUint64(&p.stats.mrfStats.TotalDroppedCount, 1)
+ atomic.AddUint64(&p.stats.mrfStats.TotalDroppedBytes, uint64(entry.sz))
+ }
+ }
+}
+
+func (p *ReplicationPool) persistToDrive(ctx context.Context, v MRFReplicateEntries) {
+ newReader := func() io.ReadCloser {
+ r, w := io.Pipe()
+ go func() {
+ // Initialize MRF meta header.
+ var data [4]byte
+ binary.LittleEndian.PutUint16(data[0:2], mrfMetaFormat)
+ binary.LittleEndian.PutUint16(data[2:4], mrfMetaVersion)
+ mw := msgp.NewWriter(w)
+ n, err := mw.Write(data[:])
+ if err != nil {
+ w.CloseWithError(err)
+ return
+ }
+ if n != len(data) {
+ w.CloseWithError(io.ErrShortWrite)
+ return
+ }
+ err = v.EncodeMsg(mw)
+ mw.Flush()
+ w.CloseWithError(err)
+ }()
+ return r
+ }
+
+ globalLocalDrivesMu.RLock()
+ localDrives := cloneDrives(globalLocalDrivesMap)
+ globalLocalDrivesMu.RUnlock()
+
+ for _, localDrive := range localDrives {
+ r := newReader()
+ err := localDrive.CreateFile(ctx, "", minioMetaBucket, pathJoin(replicationMRFDir, globalLocalNodeNameHex+".bin"), -1, r)
+ r.Close()
+ if err == nil {
+ break
+ }
+ }
+}
+
+// save mrf entries to nodenamehex.bin
+func (p *ReplicationPool) saveMRFEntries(ctx context.Context, entries map[string]MRFReplicateEntry) {
+ if !p.initialized() {
+ return
+ }
+ atomic.StoreUint64(&p.stats.mrfStats.LastFailedCount, uint64(len(entries)))
+ if len(entries) == 0 {
+ return
+ }
+
+ v := MRFReplicateEntries{
+ Entries: entries,
+ Version: mrfMetaVersion,
+ }
+
+ p.persistToDrive(ctx, v)
+}
+
+// load mrf entries from disk
+func (p *ReplicationPool) loadMRF() (mrfRec MRFReplicateEntries, err error) {
+ loadMRF := func(rc io.ReadCloser) (re MRFReplicateEntries, err error) {
+ defer rc.Close()
+
+ if !p.initialized() {
+ return re, nil
+ }
+ var data [4]byte
+ n, err := rc.Read(data[:])
+ if err != nil {
+ return re, err
+ }
+ if n != len(data) {
+ return re, errors.New("replication mrf: no data")
+ }
+ // Read resync meta header
+ switch binary.LittleEndian.Uint16(data[0:2]) {
+ case mrfMetaFormat:
+ default:
+ return re, fmt.Errorf("replication mrf: unknown format: %d", binary.LittleEndian.Uint16(data[0:2]))
+ }
+ switch binary.LittleEndian.Uint16(data[2:4]) {
+ case mrfMetaVersion:
+ default:
+ return re, fmt.Errorf("replication mrf: unknown version: %d", binary.LittleEndian.Uint16(data[2:4]))
+ }
+
+ // OK, parse data.
+ // ignore any parsing errors, we do not care this file is generated again anyways.
+ re.DecodeMsg(msgp.NewReader(rc))
+
+ return re, nil
+ }
+
+ globalLocalDrivesMu.RLock()
+ localDrives := cloneDrives(globalLocalDrivesMap)
+ globalLocalDrivesMu.RUnlock()
+
+ for _, localDrive := range localDrives {
+ rc, err := localDrive.ReadFileStream(p.ctx, minioMetaBucket, pathJoin(replicationMRFDir, globalLocalNodeNameHex+".bin"), 0, -1)
+ if err != nil {
+ continue
+ }
+
+ mrfRec, err = loadMRF(rc)
+ if err != nil {
+ continue
+ }
+
+ // finally delete the file after processing mrf entries
+ localDrive.Delete(p.ctx, minioMetaBucket, pathJoin(replicationMRFDir, globalLocalNodeNameHex+".bin"), DeleteOptions{})
+ break
+ }
+
+ return mrfRec, nil
+}
+
+func (p *ReplicationPool) processMRF() {
+ if !p.initialized() {
+ return
+ }
+ pTimer := time.NewTimer(mrfQueueInterval)
+ defer pTimer.Stop()
+ for {
+ select {
+ case <-pTimer.C:
+ // skip healing if all targets are offline
+ var offlineCnt int
+ tgts := globalBucketTargetSys.ListTargets(p.ctx, "", "")
+ for _, tgt := range tgts {
+ if globalBucketTargetSys.isOffline(tgt.URL()) {
+ offlineCnt++
+ }
+ }
+ if len(tgts) == offlineCnt {
+ pTimer.Reset(mrfQueueInterval)
+ continue
+ }
+ if err := p.queueMRFHeal(); err != nil && !osIsNotExist(err) {
+ replLogIf(p.ctx, err)
+ }
+ pTimer.Reset(mrfQueueInterval)
+ case <-p.ctx.Done():
+ return
+ }
+ }
+}
+
+// process sends error logs to the heal channel for an attempt to heal replication.
+func (p *ReplicationPool) queueMRFHeal() error {
+ p.mrfMU.Lock()
+ defer p.mrfMU.Unlock()
+
+ if !p.initialized() {
+ return errServerNotInitialized
+ }
+
+ mrfRec, err := p.loadMRF()
+ if err != nil {
+ return err
+ }
+
+ // queue replication heal in a goroutine to avoid holding up mrf save routine
+ go func() {
+ for vID, e := range mrfRec.Entries {
+ ctx, cancel := context.WithTimeout(p.ctx, time.Second) // Do not waste more than a second on this.
+
+ oi, err := p.objLayer.GetObjectInfo(ctx, e.Bucket, e.Object, ObjectOptions{
+ VersionID: vID,
+ })
+ cancel()
+ if err != nil {
+ continue
+ }
+
+ QueueReplicationHeal(p.ctx, e.Bucket, oi, e.RetryCount)
+ }
+ }()
+
+ return nil
+}
+
+func (p *ReplicationPool) initialized() bool {
+ return p != nil && p.objLayer != nil
+}
+
+// getMRF returns MRF entries for this node.
+func (p *ReplicationPool) getMRF(ctx context.Context, bucket string) (ch <-chan madmin.ReplicationMRF, err error) {
+ mrfRec, err := p.loadMRF()
+ if err != nil {
+ return nil, err
+ }
+
+ mrfCh := make(chan madmin.ReplicationMRF, 100)
+ go func() {
+ defer xioutil.SafeClose(mrfCh)
+ for vID, e := range mrfRec.Entries {
+ if bucket != "" && e.Bucket != bucket {
+ continue
+ }
+ select {
+ case mrfCh <- madmin.ReplicationMRF{
+ NodeName: globalLocalNodeName,
+ Object: e.Object,
+ VersionID: vID,
+ Bucket: e.Bucket,
+ RetryCount: e.RetryCount,
+ }:
+ case <-ctx.Done():
+ return
+ }
+ }
+ }()
+
+ return mrfCh, nil
+}
+
+// validateReplicationDestinationOptions is used to configure the validation of the replication destination.
+// validateReplicationDestination uses this to configure the validation.
+type validateReplicationDestinationOptions struct {
+ CheckRemoteBucket bool
+ CheckReady bool
+
+ 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 {
+ if k == xhttp.AmzChecksumType {
+ continue
+ }
+ cktype := hash.ChecksumStringToType(k)
+ if cktype.IsSet() {
+ meta[cktype.Key()] = v
+ meta[xhttp.AmzChecksumAlgo] = cktype.String()
+ }
+ }
+ return meta, isMP
+}
diff --git a/cmd/bucket-replication_test.go b/cmd/bucket-replication_test.go
new file mode 100644
index 00000000..ada944d2
--- /dev/null
+++ b/cmd/bucket-replication_test.go
@@ -0,0 +1,289 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/bucket/replication"
+ xhttp "github.com/minio/minio/internal/http"
+)
+
+var configs = []replication.Config{
+ { // Config0 - Replication config has no filters, existing object replication enabled
+ Rules: []replication.Rule{
+ {
+ Status: replication.Enabled,
+ Priority: 1,
+ DeleteMarkerReplication: replication.DeleteMarkerReplication{Status: replication.Enabled},
+ DeleteReplication: replication.DeleteReplication{Status: replication.Enabled},
+ Filter: replication.Filter{},
+ ExistingObjectReplication: replication.ExistingObjectReplication{Status: replication.Enabled},
+ SourceSelectionCriteria: replication.SourceSelectionCriteria{
+ ReplicaModifications: replication.ReplicaModifications{Status: replication.Enabled},
+ },
+ },
+ },
+ },
+}
+
+var replicationConfigTests = []struct {
+ info ObjectInfo
+ name string
+ rcfg replicationConfig
+ dsc ReplicateDecision
+ tgtStatuses map[string]replication.StatusType
+ expectedSync bool
+}{
+ { // 1. no replication config
+ name: "no replication config",
+ info: ObjectInfo{Size: 100},
+ rcfg: replicationConfig{Config: nil},
+ expectedSync: false,
+ },
+ { // 2. existing object replication config enabled, no versioning
+ name: "existing object replication config enabled, no versioning",
+ info: ObjectInfo{Size: 100},
+ rcfg: replicationConfig{Config: &configs[0]},
+ expectedSync: false,
+ },
+ { // 3. existing object replication config enabled, versioning suspended
+ name: "existing object replication config enabled, versioning suspended",
+ info: ObjectInfo{Size: 100, VersionID: nullVersionID},
+ rcfg: replicationConfig{Config: &configs[0]},
+ expectedSync: false,
+ },
+ { // 4. existing object replication enabled, versioning enabled; no reset in progress
+ name: "existing object replication enabled, versioning enabled; no reset in progress",
+ info: ObjectInfo{
+ Size: 100,
+ ReplicationStatus: replication.Completed,
+ VersionID: "a3348c34-c352-4498-82f0-1098e8b34df9",
+ },
+ rcfg: replicationConfig{Config: &configs[0]},
+ expectedSync: false,
+ },
+}
+
+func TestReplicationResync(t *testing.T) {
+ ctx := t.Context()
+ for i, test := range replicationConfigTests {
+ if sync := test.rcfg.Resync(ctx, test.info, test.dsc, test.tgtStatuses); sync.mustResync() != test.expectedSync {
+ t.Errorf("Test%d (%s): Resync got %t , want %t", i+1, test.name, sync.mustResync(), test.expectedSync)
+ }
+ }
+}
+
+var (
+ start = UTCNow().AddDate(0, 0, -1)
+ replicationConfigTests2 = []struct {
+ info ObjectInfo
+ name string
+ rcfg replicationConfig
+ dsc ReplicateDecision
+ tgtStatuses map[string]replication.StatusType
+ expectedSync bool
+ }{
+ { // Cases 1-4: existing object replication enabled, versioning enabled, no reset - replication status varies
+ // 1: Pending replication
+ name: "existing object replication on object in Pending replication status",
+ info: ObjectInfo{
+ Size: 100,
+ ReplicationStatusInternal: "arn1:PENDING;",
+ ReplicationStatus: replication.Pending,
+ VersionID: "a3348c34-c352-4498-82f0-1098e8b34df9",
+ },
+ rcfg: replicationConfig{remotes: &madmin.BucketTargets{Targets: []madmin.BucketTarget{{
+ Arn: "arn1",
+ }}}},
+ dsc: ReplicateDecision{targetsMap: map[string]replicateTargetDecision{"arn1": newReplicateTargetDecision("arn1", true, false)}},
+ expectedSync: true,
+ },
+
+ { // 2. replication status Failed
+ name: "existing object replication on object in Failed replication status",
+ info: ObjectInfo{
+ Size: 100,
+ ReplicationStatusInternal: "arn1:FAILED",
+ ReplicationStatus: replication.Failed,
+ VersionID: "a3348c34-c352-4498-82f0-1098e8b34df9",
+ },
+ dsc: ReplicateDecision{targetsMap: map[string]replicateTargetDecision{"arn1": newReplicateTargetDecision("arn1", true, false)}},
+ rcfg: replicationConfig{remotes: &madmin.BucketTargets{Targets: []madmin.BucketTarget{{
+ Arn: "arn1",
+ }}}},
+ expectedSync: true,
+ },
+ { // 3. replication status unset
+ name: "existing object replication on pre-existing unreplicated object",
+ info: ObjectInfo{
+ Size: 100,
+ ReplicationStatus: replication.StatusType(""),
+ VersionID: "a3348c34-c352-4498-82f0-1098e8b34df9",
+ },
+ rcfg: replicationConfig{remotes: &madmin.BucketTargets{Targets: []madmin.BucketTarget{{
+ Arn: "arn1",
+ }}}},
+ dsc: ReplicateDecision{targetsMap: map[string]replicateTargetDecision{"arn1": newReplicateTargetDecision("arn1", true, false)}},
+ expectedSync: true,
+ },
+ { // 4. replication status Complete
+ name: "existing object replication on object in Completed replication status",
+ info: ObjectInfo{
+ Size: 100,
+ ReplicationStatusInternal: "arn1:COMPLETED",
+ ReplicationStatus: replication.Completed,
+ VersionID: "a3348c34-c352-4498-82f0-1098e8b34df9",
+ },
+ dsc: ReplicateDecision{targetsMap: map[string]replicateTargetDecision{"arn1": newReplicateTargetDecision("arn1", false, false)}},
+ rcfg: replicationConfig{remotes: &madmin.BucketTargets{Targets: []madmin.BucketTarget{{
+ Arn: "arn1",
+ }}}},
+ expectedSync: false,
+ },
+ { // 5. existing object replication enabled, versioning enabled, replication status Pending & reset ID present
+ name: "existing object replication with reset in progress and object in Pending status",
+ info: ObjectInfo{
+ Size: 100,
+ ReplicationStatusInternal: "arn1:PENDING;",
+ ReplicationStatus: replication.Pending,
+ VersionID: "a3348c34-c352-4498-82f0-1098e8b34df9",
+ UserDefined: map[string]string{xhttp.MinIOReplicationResetStatus: fmt.Sprintf("%s;abc", UTCNow().AddDate(0, -1, 0).String())},
+ },
+ expectedSync: true,
+ dsc: ReplicateDecision{targetsMap: map[string]replicateTargetDecision{"arn1": newReplicateTargetDecision("arn1", true, false)}},
+ rcfg: replicationConfig{
+ remotes: &madmin.BucketTargets{Targets: []madmin.BucketTarget{{
+ Arn: "arn1",
+ ResetID: "xyz",
+ ResetBeforeDate: UTCNow(),
+ }}},
+ },
+ },
+ { // 6. existing object replication enabled, versioning enabled, replication status Failed & reset ID present
+ name: "existing object replication with reset in progress and object in Failed status",
+ info: ObjectInfo{
+ Size: 100,
+ ReplicationStatusInternal: "arn1:FAILED;",
+ ReplicationStatus: replication.Failed,
+ VersionID: "a3348c34-c352-4498-82f0-1098e8b34df9",
+ UserDefined: map[string]string{xhttp.MinIOReplicationResetStatus: fmt.Sprintf("%s;abc", UTCNow().AddDate(0, -1, 0).String())},
+ },
+ dsc: ReplicateDecision{targetsMap: map[string]replicateTargetDecision{"arn1": newReplicateTargetDecision("arn1", true, false)}},
+ rcfg: replicationConfig{
+ remotes: &madmin.BucketTargets{Targets: []madmin.BucketTarget{{
+ Arn: "arn1",
+ ResetID: "xyz",
+ ResetBeforeDate: UTCNow(),
+ }}},
+ },
+ expectedSync: true,
+ },
+ { // 7. existing object replication enabled, versioning enabled, replication status unset & reset ID present
+ name: "existing object replication with reset in progress and object never replicated before",
+ info: ObjectInfo{
+ Size: 100,
+ ReplicationStatus: replication.StatusType(""),
+ VersionID: "a3348c34-c352-4498-82f0-1098e8b34df9",
+ UserDefined: map[string]string{xhttp.MinIOReplicationResetStatus: fmt.Sprintf("%s;abc", UTCNow().AddDate(0, -1, 0).String())},
+ },
+ dsc: ReplicateDecision{targetsMap: map[string]replicateTargetDecision{"arn1": newReplicateTargetDecision("arn1", true, false)}},
+ rcfg: replicationConfig{
+ remotes: &madmin.BucketTargets{Targets: []madmin.BucketTarget{{
+ Arn: "arn1",
+ ResetID: "xyz",
+ ResetBeforeDate: UTCNow(),
+ }}},
+ },
+
+ expectedSync: true,
+ },
+
+ { // 8. existing object replication enabled, versioning enabled, replication status Complete & reset ID present
+ name: "existing object replication enabled - reset in progress for an object in Completed status",
+ info: ObjectInfo{
+ Size: 100,
+ ReplicationStatusInternal: "arn1:COMPLETED;",
+ ReplicationStatus: replication.Completed,
+ VersionID: "a3348c34-c352-4498-82f0-1098e8b34df8",
+ UserDefined: map[string]string{xhttp.MinIOReplicationResetStatus: fmt.Sprintf("%s;abc", UTCNow().AddDate(0, -1, 0).String())},
+ },
+ expectedSync: true,
+ dsc: ReplicateDecision{targetsMap: map[string]replicateTargetDecision{"arn1": newReplicateTargetDecision("arn1", true, false)}},
+ rcfg: replicationConfig{
+ remotes: &madmin.BucketTargets{Targets: []madmin.BucketTarget{{
+ Arn: "arn1",
+ ResetID: "xyz",
+ ResetBeforeDate: UTCNow(),
+ }}},
+ },
+ },
+ { // 9. existing object replication enabled, versioning enabled, replication status Pending & reset ID different
+ name: "existing object replication enabled, newer reset in progress on object in Pending replication status",
+ info: ObjectInfo{
+ Size: 100,
+ ReplicationStatusInternal: "arn1:PENDING;",
+
+ ReplicationStatus: replication.Pending,
+ VersionID: "a3348c34-c352-4498-82f0-1098e8b34df9",
+ UserDefined: map[string]string{xhttp.MinIOReplicationResetStatus: fmt.Sprintf("%s;%s", UTCNow().AddDate(0, 0, -1).Format(http.TimeFormat), "abc")},
+ ModTime: UTCNow().AddDate(0, 0, -2),
+ },
+ expectedSync: true,
+ dsc: ReplicateDecision{targetsMap: map[string]replicateTargetDecision{"arn1": newReplicateTargetDecision("arn1", true, false)}},
+ rcfg: replicationConfig{
+ remotes: &madmin.BucketTargets{Targets: []madmin.BucketTarget{{
+ Arn: "arn1",
+ ResetID: "xyz",
+ ResetBeforeDate: UTCNow(),
+ }}},
+ },
+ },
+ { // 10. existing object replication enabled, versioning enabled, replication status Complete & reset done
+ name: "reset done on object in Completed Status - ineligbile for re-replication",
+ info: ObjectInfo{
+ Size: 100,
+ ReplicationStatusInternal: "arn1:COMPLETED;",
+ ReplicationStatus: replication.Completed,
+ VersionID: "a3348c34-c352-4498-82f0-1098e8b34df9",
+ UserDefined: map[string]string{xhttp.MinIOReplicationResetStatus: fmt.Sprintf("%s;%s", start.Format(http.TimeFormat), "xyz")},
+ },
+ expectedSync: false,
+ dsc: ReplicateDecision{targetsMap: map[string]replicateTargetDecision{"arn1": newReplicateTargetDecision("arn1", true, false)}},
+ rcfg: replicationConfig{
+ remotes: &madmin.BucketTargets{Targets: []madmin.BucketTarget{{
+ Arn: "arn1",
+ ResetID: "xyz",
+ ResetBeforeDate: start,
+ }}},
+ },
+ },
+ }
+)
+
+func TestReplicationResyncwrapper(t *testing.T) {
+ for i, test := range replicationConfigTests2 {
+ if sync := test.rcfg.resync(test.info, test.dsc, test.tgtStatuses); sync.mustResync() != test.expectedSync {
+ t.Errorf("%s (%s): Replicationresync got %t , want %t", fmt.Sprintf("Test%d - %s", i+1, time.Now().Format(http.TimeFormat)), test.name, sync.mustResync(), test.expectedSync)
+ }
+ }
+}
diff --git a/cmd/bucket-stats.go b/cmd/bucket-stats.go
new file mode 100644
index 00000000..e2eb00aa
--- /dev/null
+++ b/cmd/bucket-stats.go
@@ -0,0 +1,432 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "fmt"
+ "maps"
+ "math"
+ "sync/atomic"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+)
+
+//go:generate msgp -file $GOFILE
+
+// ReplicationLatency holds information of bucket operations latency, such us uploads
+type ReplicationLatency struct {
+ // Single & Multipart PUTs latency
+ UploadHistogram LastMinuteHistogram
+}
+
+// Merge two replication latency into a new one
+func (rl ReplicationLatency) merge(other ReplicationLatency) (newReplLatency ReplicationLatency) {
+ newReplLatency.UploadHistogram = rl.UploadHistogram.Merge(other.UploadHistogram)
+ return newReplLatency
+}
+
+// Get upload latency of each object size range
+func (rl ReplicationLatency) getUploadLatency() (ret map[string]uint64) {
+ ret = make(map[string]uint64)
+ avg := rl.UploadHistogram.GetAvgData()
+ for k, v := range avg {
+ // Convert nanoseconds to milliseconds
+ ret[sizeTagToString(k)] = uint64(v.avg() / time.Millisecond)
+ }
+ return ret
+}
+
+// Update replication upload latency with a new value
+func (rl *ReplicationLatency) update(size int64, duration time.Duration) {
+ rl.UploadHistogram.Add(size, duration)
+}
+
+// ReplicationLastMinute has last minute replication counters
+type ReplicationLastMinute struct {
+ LastMinute lastMinuteLatency
+}
+
+func (rl ReplicationLastMinute) merge(other ReplicationLastMinute) (nl ReplicationLastMinute) {
+ nl = ReplicationLastMinute{rl.LastMinute.merge(other.LastMinute)}
+ return nl
+}
+
+func (rl *ReplicationLastMinute) addsize(n int64) {
+ t := time.Now().Unix()
+ rl.LastMinute.addAll(t-1, AccElem{Total: t - 1, Size: n, N: 1})
+}
+
+func (rl *ReplicationLastMinute) String() string {
+ t := rl.LastMinute.getTotal()
+ return fmt.Sprintf("ReplicationLastMinute sz= %d, n=%d , dur=%d", t.Size, t.N, t.Total)
+}
+
+func (rl *ReplicationLastMinute) getTotal() AccElem {
+ return rl.LastMinute.getTotal()
+}
+
+// ReplicationLastHour keeps track of replication counts over the last hour
+type ReplicationLastHour struct {
+ Totals [60]AccElem
+ LastMin int64
+}
+
+// Merge data of two ReplicationLastHour structure
+func (l ReplicationLastHour) merge(o ReplicationLastHour) (merged ReplicationLastHour) {
+ if l.LastMin > o.LastMin {
+ o.forwardTo(l.LastMin)
+ merged.LastMin = l.LastMin
+ } else {
+ l.forwardTo(o.LastMin)
+ merged.LastMin = o.LastMin
+ }
+
+ for i := range merged.Totals {
+ merged.Totals[i] = AccElem{
+ Total: l.Totals[i].Total + o.Totals[i].Total,
+ N: l.Totals[i].N + o.Totals[i].N,
+ Size: l.Totals[i].Size + o.Totals[i].Size,
+ }
+ }
+ return merged
+}
+
+// Add a new duration data
+func (l *ReplicationLastHour) addsize(sz int64) {
+ minutes := time.Now().Unix() / 60
+ l.forwardTo(minutes)
+ winIdx := minutes % 60
+ l.Totals[winIdx].merge(AccElem{Total: minutes, Size: sz, N: 1})
+ l.LastMin = minutes
+}
+
+// Merge all recorded counts of last hour into one
+func (l *ReplicationLastHour) getTotal() AccElem {
+ var res AccElem
+ minutes := time.Now().Unix() / 60
+ l.forwardTo(minutes)
+ for _, elem := range l.Totals[:] {
+ res.merge(elem)
+ }
+ return res
+}
+
+// forwardTo time t, clearing any entries in between.
+func (l *ReplicationLastHour) forwardTo(t int64) {
+ if l.LastMin >= t {
+ return
+ }
+ if t-l.LastMin >= 60 {
+ l.Totals = [60]AccElem{}
+ return
+ }
+ for l.LastMin != t {
+ // Clear next element.
+ idx := (l.LastMin + 1) % 60
+ l.Totals[idx] = AccElem{}
+ l.LastMin++
+ }
+}
+
+// BucketStatsMap captures bucket statistics for all buckets
+type BucketStatsMap struct {
+ Stats map[string]BucketStats
+ Timestamp time.Time
+}
+
+// BucketStats bucket statistics
+type BucketStats struct {
+ Uptime int64 `json:"uptime"`
+ ReplicationStats BucketReplicationStats `json:"currStats"` // current replication stats since cluster startup
+ QueueStats ReplicationQueueStats `json:"queueStats"` // replication queue stats
+ ProxyStats ProxyMetric `json:"proxyStats"`
+}
+
+// BucketReplicationStats represents inline replication statistics
+// such as pending, failed and completed bytes in total for a bucket
+type BucketReplicationStats struct {
+ Stats map[string]*BucketReplicationStat `json:",omitempty"`
+ // Completed size in bytes
+ ReplicatedSize int64 `json:"completedReplicationSize"`
+ // Total Replica size in bytes
+ ReplicaSize int64 `json:"replicaSize"`
+ // Total failed operations including metadata updates for various time frames
+ Failed madmin.TimedErrStats `json:"failed"`
+
+ // Total number of completed operations
+ ReplicatedCount int64 `json:"replicationCount"`
+ // Total number of replica received
+ ReplicaCount int64 `json:"replicaCount"`
+
+ // in Queue stats for bucket - from qCache
+ QStat InQueueMetric `json:"queued"`
+ // Deprecated fields
+ // Pending size in bytes
+ PendingSize int64 `json:"pendingReplicationSize"`
+ // Failed size in bytes
+ FailedSize int64 `json:"failedReplicationSize"`
+ // Total number of pending operations including metadata updates
+ PendingCount int64 `json:"pendingReplicationCount"`
+ // Total number of failed operations including metadata updates
+ FailedCount int64 `json:"failedReplicationCount"`
+}
+
+func newBucketReplicationStats() *BucketReplicationStats {
+ return &BucketReplicationStats{
+ Stats: make(map[string]*BucketReplicationStat),
+ }
+}
+
+// Empty returns true if there are no target stats
+func (brs *BucketReplicationStats) Empty() bool {
+ return len(brs.Stats) == 0 && brs.ReplicaSize == 0
+}
+
+// Clone creates a new BucketReplicationStats copy
+func (brs BucketReplicationStats) Clone() (c BucketReplicationStats) {
+ // This is called only by replicationStats cache and already holds a
+ // read lock before calling Clone()
+
+ c = brs
+ // We need to copy the map, so we do not reference the one in `brs`.
+ c.Stats = make(map[string]*BucketReplicationStat, len(brs.Stats))
+ for arn, st := range brs.Stats {
+ // make a copy of `*st`
+ s := BucketReplicationStat{
+ ReplicatedSize: st.ReplicatedSize,
+ ReplicaSize: st.ReplicaSize,
+ Latency: st.Latency,
+ BandWidthLimitInBytesPerSecond: st.BandWidthLimitInBytesPerSecond,
+ CurrentBandwidthInBytesPerSecond: st.CurrentBandwidthInBytesPerSecond,
+ XferRateLrg: st.XferRateLrg.Clone(),
+ XferRateSml: st.XferRateSml.Clone(),
+ ReplicatedCount: st.ReplicatedCount,
+ Failed: st.Failed,
+ FailStats: st.FailStats,
+ }
+ if s.Failed.ErrCounts == nil {
+ s.Failed.ErrCounts = make(map[string]int)
+ maps.Copy(s.Failed.ErrCounts, st.Failed.ErrCounts)
+ }
+ c.Stats[arn] = &s
+ }
+ return c
+}
+
+// BucketReplicationStat represents inline replication statistics
+// such as pending, failed and completed bytes in total for a bucket
+// remote target
+type BucketReplicationStat struct {
+ // Pending size in bytes
+ // PendingSize int64 `json:"pendingReplicationSize"`
+ // Completed size in bytes
+ ReplicatedSize int64 `json:"completedReplicationSize"`
+ // Total Replica size in bytes
+ ReplicaSize int64 `json:"replicaSize"`
+ // Collect stats for failures
+ FailStats RTimedMetrics `json:"-"`
+
+ // Total number of failed operations including metadata updates in the last minute
+ Failed madmin.TimedErrStats `json:"failed"`
+ // Total number of completed operations
+ ReplicatedCount int64 `json:"replicationCount"`
+ // Replication latency information
+ Latency ReplicationLatency `json:"replicationLatency"`
+ // bandwidth limit for target
+ BandWidthLimitInBytesPerSecond int64 `json:"limitInBits"`
+ // current bandwidth reported
+ CurrentBandwidthInBytesPerSecond float64 `json:"currentBandwidth"`
+ // transfer rate for large uploads
+ XferRateLrg *XferStats `json:"-" msg:"lt"`
+ // transfer rate for small uploads
+ XferRateSml *XferStats `json:"-" msg:"st"`
+
+ // Deprecated fields
+ // Pending size in bytes
+ PendingSize int64 `json:"pendingReplicationSize"`
+ // Failed size in bytes
+ FailedSize int64 `json:"failedReplicationSize"`
+ // Total number of pending operations including metadata updates
+ PendingCount int64 `json:"pendingReplicationCount"`
+ // Total number of failed operations including metadata updates
+ FailedCount int64 `json:"failedReplicationCount"`
+}
+
+func (bs *BucketReplicationStat) hasReplicationUsage() bool {
+ return bs.FailStats.SinceUptime.Count > 0 ||
+ bs.ReplicatedSize > 0 ||
+ bs.ReplicaSize > 0
+}
+
+func (bs *BucketReplicationStat) updateXferRate(sz int64, duration time.Duration) {
+ if sz > minLargeObjSize {
+ bs.XferRateLrg.addSize(sz, duration)
+ } else {
+ bs.XferRateSml.addSize(sz, duration)
+ }
+}
+
+// RMetricName - name of replication metric
+type RMetricName string
+
+const (
+ // Large - objects larger than 128MiB
+ Large RMetricName = "Large"
+ // Small - objects smaller than 128MiB
+ Small RMetricName = "Small"
+ // Total - metric pertaining to totals
+ Total RMetricName = "Total"
+)
+
+// ReplQNodeStats holds queue stats for replication per node
+type ReplQNodeStats struct {
+ NodeName string `json:"nodeName"`
+ Uptime int64 `json:"uptime"`
+ ActiveWorkers ActiveWorkerStat `json:"activeWorkers"`
+ XferStats map[RMetricName]XferStats `json:"transferSummary"`
+ TgtXferStats map[string]map[RMetricName]XferStats `json:"tgtTransferStats"`
+ QStats InQueueMetric `json:"queueStats"`
+ MRFStats ReplicationMRFStats `json:"mrfStats"`
+}
+
+// getNodeQueueStats returns replication operational stats at the node level
+func (r *ReplicationStats) getNodeQueueStats(bucket string) (qs ReplQNodeStats) {
+ qs.NodeName = globalLocalNodeName
+ qs.Uptime = UTCNow().Unix() - globalBootTime.Unix()
+ grs := globalReplicationStats.Load()
+ if grs != nil {
+ qs.ActiveWorkers = grs.ActiveWorkers()
+ } else {
+ qs.ActiveWorkers = ActiveWorkerStat{}
+ }
+ qs.XferStats = make(map[RMetricName]XferStats)
+ qs.QStats = r.qCache.getBucketStats(bucket)
+ qs.TgtXferStats = make(map[string]map[RMetricName]XferStats)
+ qs.MRFStats = ReplicationMRFStats{
+ LastFailedCount: atomic.LoadUint64(&r.mrfStats.LastFailedCount),
+ }
+
+ r.RLock()
+ defer r.RUnlock()
+
+ brs, ok := r.Cache[bucket]
+ if !ok {
+ return qs
+ }
+ for arn := range brs.Stats {
+ qs.TgtXferStats[arn] = make(map[RMetricName]XferStats)
+ }
+ count := 0
+ var totPeak float64
+ // calculate large, small transfers and total transfer rates per replication target at bucket level
+ for arn, v := range brs.Stats {
+ lcurrTgt := v.XferRateLrg.curr()
+ scurrTgt := v.XferRateSml.curr()
+ totPeak = math.Max(math.Max(v.XferRateLrg.Peak, v.XferRateSml.Peak), totPeak)
+ totPeak = math.Max(math.Max(lcurrTgt, scurrTgt), totPeak)
+ tcount := 0
+ if v.XferRateLrg.Peak > 0 {
+ tcount++
+ }
+ if v.XferRateSml.Peak > 0 {
+ tcount++
+ }
+ qs.TgtXferStats[arn][Large] = XferStats{
+ Avg: v.XferRateLrg.Avg,
+ Curr: lcurrTgt,
+ Peak: math.Max(v.XferRateLrg.Peak, lcurrTgt),
+ }
+ qs.TgtXferStats[arn][Small] = XferStats{
+ Avg: v.XferRateSml.Avg,
+ Curr: scurrTgt,
+ Peak: math.Max(v.XferRateSml.Peak, scurrTgt),
+ }
+ if tcount > 0 {
+ qs.TgtXferStats[arn][Total] = XferStats{
+ Avg: (v.XferRateLrg.Avg + v.XferRateSml.Avg) / float64(tcount),
+ Curr: (scurrTgt + lcurrTgt) / float64(tcount),
+ Peak: totPeak,
+ }
+ }
+ }
+ // calculate large, small and total transfer rates for a minio node
+ var lavg, lcurr, lpeak, savg, scurr, speak, totpeak float64
+ for _, v := range qs.TgtXferStats {
+ tot := v[Total]
+ lavg += v[Large].Avg
+ lcurr += v[Large].Curr
+ savg += v[Small].Avg
+ scurr += v[Small].Curr
+ totpeak = math.Max(math.Max(tot.Peak, totpeak), tot.Curr)
+ lpeak = math.Max(math.Max(v[Large].Peak, lpeak), v[Large].Curr)
+ speak = math.Max(math.Max(v[Small].Peak, speak), v[Small].Curr)
+ if lpeak > 0 || speak > 0 {
+ count++
+ }
+ }
+ if count > 0 {
+ lrg := XferStats{
+ Avg: lavg / float64(count),
+ Curr: lcurr / float64(count),
+ Peak: lpeak,
+ }
+ sml := XferStats{
+ Avg: savg / float64(count),
+ Curr: scurr / float64(count),
+ Peak: speak,
+ }
+ qs.XferStats[Large] = lrg
+ qs.XferStats[Small] = sml
+ qs.XferStats[Total] = XferStats{
+ Avg: (savg + lavg) / float64(count),
+ Curr: (lcurr + scurr) / float64(count),
+ Peak: totpeak,
+ }
+ }
+ return qs
+}
+
+// populate queue totals for node and active workers in use for metrics
+func (r *ReplicationStats) getNodeQueueStatsSummary() (qs ReplQNodeStats) {
+ qs.NodeName = globalLocalNodeName
+ qs.Uptime = UTCNow().Unix() - globalBootTime.Unix()
+ qs.ActiveWorkers = globalReplicationStats.Load().ActiveWorkers()
+ qs.XferStats = make(map[RMetricName]XferStats)
+ qs.QStats = r.qCache.getSiteStats()
+ qs.MRFStats = ReplicationMRFStats{
+ LastFailedCount: atomic.LoadUint64(&r.mrfStats.LastFailedCount),
+ }
+ r.RLock()
+ defer r.RUnlock()
+ tx := newXferStats()
+ for _, brs := range r.Cache {
+ for _, v := range brs.Stats {
+ tx := tx.merge(*v.XferRateLrg)
+ tx = tx.merge(*v.XferRateSml)
+ }
+ }
+ qs.XferStats[Total] = *tx
+ return qs
+}
+
+// ReplicationQueueStats holds overall queue stats for replication
+type ReplicationQueueStats struct {
+ Nodes []ReplQNodeStats `json:"nodes"`
+ Uptime int64 `json:"uptime"`
+}
diff --git a/cmd/bucket-stats_gen.go b/cmd/bucket-stats_gen.go
new file mode 100644
index 00000000..201c574c
--- /dev/null
+++ b/cmd/bucket-stats_gen.go
@@ -0,0 +1,2393 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "github.com/tinylib/msgp/msgp"
+)
+
+// DecodeMsg implements msgp.Decodable
+func (z *BucketReplicationStat) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ReplicatedSize":
+ z.ReplicatedSize, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedSize")
+ return
+ }
+ case "ReplicaSize":
+ z.ReplicaSize, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicaSize")
+ return
+ }
+ case "FailStats":
+ err = z.FailStats.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "FailStats")
+ return
+ }
+ case "Failed":
+ err = z.Failed.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Failed")
+ return
+ }
+ case "ReplicatedCount":
+ z.ReplicatedCount, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedCount")
+ return
+ }
+ case "Latency":
+ var zb0002 uint32
+ zb0002, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Latency")
+ return
+ }
+ for zb0002 > 0 {
+ zb0002--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err, "Latency")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "UploadHistogram":
+ err = z.Latency.UploadHistogram.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Latency", "UploadHistogram")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err, "Latency")
+ return
+ }
+ }
+ }
+ case "BandWidthLimitInBytesPerSecond":
+ z.BandWidthLimitInBytesPerSecond, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "BandWidthLimitInBytesPerSecond")
+ return
+ }
+ case "CurrentBandwidthInBytesPerSecond":
+ z.CurrentBandwidthInBytesPerSecond, err = dc.ReadFloat64()
+ if err != nil {
+ err = msgp.WrapError(err, "CurrentBandwidthInBytesPerSecond")
+ return
+ }
+ case "lt":
+ if dc.IsNil() {
+ err = dc.ReadNil()
+ if err != nil {
+ err = msgp.WrapError(err, "XferRateLrg")
+ return
+ }
+ z.XferRateLrg = nil
+ } else {
+ if z.XferRateLrg == nil {
+ z.XferRateLrg = new(XferStats)
+ }
+ err = z.XferRateLrg.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "XferRateLrg")
+ return
+ }
+ }
+ case "st":
+ if dc.IsNil() {
+ err = dc.ReadNil()
+ if err != nil {
+ err = msgp.WrapError(err, "XferRateSml")
+ return
+ }
+ z.XferRateSml = nil
+ } else {
+ if z.XferRateSml == nil {
+ z.XferRateSml = new(XferStats)
+ }
+ err = z.XferRateSml.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "XferRateSml")
+ return
+ }
+ }
+ case "PendingSize":
+ z.PendingSize, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "PendingSize")
+ return
+ }
+ case "FailedSize":
+ z.FailedSize, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "FailedSize")
+ return
+ }
+ case "PendingCount":
+ z.PendingCount, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "PendingCount")
+ return
+ }
+ case "FailedCount":
+ z.FailedCount, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "FailedCount")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *BucketReplicationStat) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 14
+ // write "ReplicatedSize"
+ err = en.Append(0x8e, 0xae, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.ReplicatedSize)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedSize")
+ return
+ }
+ // write "ReplicaSize"
+ err = en.Append(0xab, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x53, 0x69, 0x7a, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.ReplicaSize)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicaSize")
+ return
+ }
+ // write "FailStats"
+ err = en.Append(0xa9, 0x46, 0x61, 0x69, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = z.FailStats.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "FailStats")
+ return
+ }
+ // write "Failed"
+ err = en.Append(0xa6, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = z.Failed.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Failed")
+ return
+ }
+ // write "ReplicatedCount"
+ err = en.Append(0xaf, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.ReplicatedCount)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedCount")
+ return
+ }
+ // write "Latency"
+ err = en.Append(0xa7, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79)
+ if err != nil {
+ return
+ }
+ // map header, size 1
+ // write "UploadHistogram"
+ err = en.Append(0x81, 0xaf, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x67, 0x72, 0x61, 0x6d)
+ if err != nil {
+ return
+ }
+ err = z.Latency.UploadHistogram.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Latency", "UploadHistogram")
+ return
+ }
+ // write "BandWidthLimitInBytesPerSecond"
+ err = en.Append(0xbe, 0x42, 0x61, 0x6e, 0x64, 0x57, 0x69, 0x64, 0x74, 0x68, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x49, 0x6e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x50, 0x65, 0x72, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.BandWidthLimitInBytesPerSecond)
+ if err != nil {
+ err = msgp.WrapError(err, "BandWidthLimitInBytesPerSecond")
+ return
+ }
+ // write "CurrentBandwidthInBytesPerSecond"
+ err = en.Append(0xd9, 0x20, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x64, 0x77, 0x69, 0x64, 0x74, 0x68, 0x49, 0x6e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x50, 0x65, 0x72, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteFloat64(z.CurrentBandwidthInBytesPerSecond)
+ if err != nil {
+ err = msgp.WrapError(err, "CurrentBandwidthInBytesPerSecond")
+ return
+ }
+ // write "lt"
+ err = en.Append(0xa2, 0x6c, 0x74)
+ if err != nil {
+ return
+ }
+ if z.XferRateLrg == nil {
+ err = en.WriteNil()
+ if err != nil {
+ return
+ }
+ } else {
+ err = z.XferRateLrg.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "XferRateLrg")
+ return
+ }
+ }
+ // write "st"
+ err = en.Append(0xa2, 0x73, 0x74)
+ if err != nil {
+ return
+ }
+ if z.XferRateSml == nil {
+ err = en.WriteNil()
+ if err != nil {
+ return
+ }
+ } else {
+ err = z.XferRateSml.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "XferRateSml")
+ return
+ }
+ }
+ // write "PendingSize"
+ err = en.Append(0xab, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.PendingSize)
+ if err != nil {
+ err = msgp.WrapError(err, "PendingSize")
+ return
+ }
+ // write "FailedSize"
+ err = en.Append(0xaa, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.FailedSize)
+ if err != nil {
+ err = msgp.WrapError(err, "FailedSize")
+ return
+ }
+ // write "PendingCount"
+ err = en.Append(0xac, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.PendingCount)
+ if err != nil {
+ err = msgp.WrapError(err, "PendingCount")
+ return
+ }
+ // write "FailedCount"
+ err = en.Append(0xab, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.FailedCount)
+ if err != nil {
+ err = msgp.WrapError(err, "FailedCount")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *BucketReplicationStat) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 14
+ // string "ReplicatedSize"
+ o = append(o, 0x8e, 0xae, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65)
+ o = msgp.AppendInt64(o, z.ReplicatedSize)
+ // string "ReplicaSize"
+ o = append(o, 0xab, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x53, 0x69, 0x7a, 0x65)
+ o = msgp.AppendInt64(o, z.ReplicaSize)
+ // string "FailStats"
+ o = append(o, 0xa9, 0x46, 0x61, 0x69, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x73)
+ o, err = z.FailStats.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "FailStats")
+ return
+ }
+ // string "Failed"
+ o = append(o, 0xa6, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
+ o, err = z.Failed.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Failed")
+ return
+ }
+ // string "ReplicatedCount"
+ o = append(o, 0xaf, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ o = msgp.AppendInt64(o, z.ReplicatedCount)
+ // string "Latency"
+ o = append(o, 0xa7, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79)
+ // map header, size 1
+ // string "UploadHistogram"
+ o = append(o, 0x81, 0xaf, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x67, 0x72, 0x61, 0x6d)
+ o, err = z.Latency.UploadHistogram.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Latency", "UploadHistogram")
+ return
+ }
+ // string "BandWidthLimitInBytesPerSecond"
+ o = append(o, 0xbe, 0x42, 0x61, 0x6e, 0x64, 0x57, 0x69, 0x64, 0x74, 0x68, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x49, 0x6e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x50, 0x65, 0x72, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64)
+ o = msgp.AppendInt64(o, z.BandWidthLimitInBytesPerSecond)
+ // string "CurrentBandwidthInBytesPerSecond"
+ o = append(o, 0xd9, 0x20, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x64, 0x77, 0x69, 0x64, 0x74, 0x68, 0x49, 0x6e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x50, 0x65, 0x72, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64)
+ o = msgp.AppendFloat64(o, z.CurrentBandwidthInBytesPerSecond)
+ // string "lt"
+ o = append(o, 0xa2, 0x6c, 0x74)
+ if z.XferRateLrg == nil {
+ o = msgp.AppendNil(o)
+ } else {
+ o, err = z.XferRateLrg.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "XferRateLrg")
+ return
+ }
+ }
+ // string "st"
+ o = append(o, 0xa2, 0x73, 0x74)
+ if z.XferRateSml == nil {
+ o = msgp.AppendNil(o)
+ } else {
+ o, err = z.XferRateSml.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "XferRateSml")
+ return
+ }
+ }
+ // string "PendingSize"
+ o = append(o, 0xab, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65)
+ o = msgp.AppendInt64(o, z.PendingSize)
+ // string "FailedSize"
+ o = append(o, 0xaa, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65)
+ o = msgp.AppendInt64(o, z.FailedSize)
+ // string "PendingCount"
+ o = append(o, 0xac, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ o = msgp.AppendInt64(o, z.PendingCount)
+ // string "FailedCount"
+ o = append(o, 0xab, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ o = msgp.AppendInt64(o, z.FailedCount)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BucketReplicationStat) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ReplicatedSize":
+ z.ReplicatedSize, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedSize")
+ return
+ }
+ case "ReplicaSize":
+ z.ReplicaSize, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicaSize")
+ return
+ }
+ case "FailStats":
+ bts, err = z.FailStats.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "FailStats")
+ return
+ }
+ case "Failed":
+ bts, err = z.Failed.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Failed")
+ return
+ }
+ case "ReplicatedCount":
+ z.ReplicatedCount, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedCount")
+ return
+ }
+ case "Latency":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Latency")
+ return
+ }
+ for zb0002 > 0 {
+ zb0002--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Latency")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "UploadHistogram":
+ bts, err = z.Latency.UploadHistogram.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Latency", "UploadHistogram")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Latency")
+ return
+ }
+ }
+ }
+ case "BandWidthLimitInBytesPerSecond":
+ z.BandWidthLimitInBytesPerSecond, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "BandWidthLimitInBytesPerSecond")
+ return
+ }
+ case "CurrentBandwidthInBytesPerSecond":
+ z.CurrentBandwidthInBytesPerSecond, bts, err = msgp.ReadFloat64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "CurrentBandwidthInBytesPerSecond")
+ return
+ }
+ case "lt":
+ if msgp.IsNil(bts) {
+ bts, err = msgp.ReadNilBytes(bts)
+ if err != nil {
+ return
+ }
+ z.XferRateLrg = nil
+ } else {
+ if z.XferRateLrg == nil {
+ z.XferRateLrg = new(XferStats)
+ }
+ bts, err = z.XferRateLrg.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "XferRateLrg")
+ return
+ }
+ }
+ case "st":
+ if msgp.IsNil(bts) {
+ bts, err = msgp.ReadNilBytes(bts)
+ if err != nil {
+ return
+ }
+ z.XferRateSml = nil
+ } else {
+ if z.XferRateSml == nil {
+ z.XferRateSml = new(XferStats)
+ }
+ bts, err = z.XferRateSml.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "XferRateSml")
+ return
+ }
+ }
+ case "PendingSize":
+ z.PendingSize, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "PendingSize")
+ return
+ }
+ case "FailedSize":
+ z.FailedSize, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "FailedSize")
+ return
+ }
+ case "PendingCount":
+ z.PendingCount, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "PendingCount")
+ return
+ }
+ case "FailedCount":
+ z.FailedCount, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "FailedCount")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *BucketReplicationStat) Msgsize() (s int) {
+ s = 1 + 15 + msgp.Int64Size + 12 + msgp.Int64Size + 10 + z.FailStats.Msgsize() + 7 + z.Failed.Msgsize() + 16 + msgp.Int64Size + 8 + 1 + 16 + z.Latency.UploadHistogram.Msgsize() + 31 + msgp.Int64Size + 34 + msgp.Float64Size + 3
+ if z.XferRateLrg == nil {
+ s += msgp.NilSize
+ } else {
+ s += z.XferRateLrg.Msgsize()
+ }
+ s += 3
+ if z.XferRateSml == nil {
+ s += msgp.NilSize
+ } else {
+ s += z.XferRateSml.Msgsize()
+ }
+ s += 12 + msgp.Int64Size + 11 + msgp.Int64Size + 13 + msgp.Int64Size + 12 + msgp.Int64Size
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *BucketReplicationStats) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Stats":
+ var zb0002 uint32
+ zb0002, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Stats")
+ return
+ }
+ if z.Stats == nil {
+ z.Stats = make(map[string]*BucketReplicationStat, zb0002)
+ } else if len(z.Stats) > 0 {
+ clear(z.Stats)
+ }
+ for zb0002 > 0 {
+ zb0002--
+ var za0001 string
+ za0001, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Stats")
+ return
+ }
+ var za0002 *BucketReplicationStat
+ if dc.IsNil() {
+ err = dc.ReadNil()
+ if err != nil {
+ err = msgp.WrapError(err, "Stats", za0001)
+ return
+ }
+ za0002 = nil
+ } else {
+ if za0002 == nil {
+ za0002 = new(BucketReplicationStat)
+ }
+ err = za0002.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Stats", za0001)
+ return
+ }
+ }
+ z.Stats[za0001] = za0002
+ }
+ case "ReplicatedSize":
+ z.ReplicatedSize, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedSize")
+ return
+ }
+ case "ReplicaSize":
+ z.ReplicaSize, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicaSize")
+ return
+ }
+ case "Failed":
+ err = z.Failed.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Failed")
+ return
+ }
+ case "ReplicatedCount":
+ z.ReplicatedCount, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedCount")
+ return
+ }
+ case "ReplicaCount":
+ z.ReplicaCount, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicaCount")
+ return
+ }
+ case "QStat":
+ err = z.QStat.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "QStat")
+ return
+ }
+ case "PendingSize":
+ z.PendingSize, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "PendingSize")
+ return
+ }
+ case "FailedSize":
+ z.FailedSize, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "FailedSize")
+ return
+ }
+ case "PendingCount":
+ z.PendingCount, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "PendingCount")
+ return
+ }
+ case "FailedCount":
+ z.FailedCount, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "FailedCount")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *BucketReplicationStats) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 11
+ // write "Stats"
+ err = en.Append(0x8b, 0xa5, 0x53, 0x74, 0x61, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteMapHeader(uint32(len(z.Stats)))
+ if err != nil {
+ err = msgp.WrapError(err, "Stats")
+ return
+ }
+ for za0001, za0002 := range z.Stats {
+ err = en.WriteString(za0001)
+ if err != nil {
+ err = msgp.WrapError(err, "Stats")
+ return
+ }
+ if za0002 == nil {
+ err = en.WriteNil()
+ if err != nil {
+ return
+ }
+ } else {
+ err = za0002.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Stats", za0001)
+ return
+ }
+ }
+ }
+ // write "ReplicatedSize"
+ err = en.Append(0xae, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.ReplicatedSize)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedSize")
+ return
+ }
+ // write "ReplicaSize"
+ err = en.Append(0xab, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x53, 0x69, 0x7a, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.ReplicaSize)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicaSize")
+ return
+ }
+ // write "Failed"
+ err = en.Append(0xa6, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = z.Failed.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Failed")
+ return
+ }
+ // write "ReplicatedCount"
+ err = en.Append(0xaf, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.ReplicatedCount)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedCount")
+ return
+ }
+ // write "ReplicaCount"
+ err = en.Append(0xac, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.ReplicaCount)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicaCount")
+ return
+ }
+ // write "QStat"
+ err = en.Append(0xa5, 0x51, 0x53, 0x74, 0x61, 0x74)
+ if err != nil {
+ return
+ }
+ err = z.QStat.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "QStat")
+ return
+ }
+ // write "PendingSize"
+ err = en.Append(0xab, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.PendingSize)
+ if err != nil {
+ err = msgp.WrapError(err, "PendingSize")
+ return
+ }
+ // write "FailedSize"
+ err = en.Append(0xaa, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.FailedSize)
+ if err != nil {
+ err = msgp.WrapError(err, "FailedSize")
+ return
+ }
+ // write "PendingCount"
+ err = en.Append(0xac, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.PendingCount)
+ if err != nil {
+ err = msgp.WrapError(err, "PendingCount")
+ return
+ }
+ // write "FailedCount"
+ err = en.Append(0xab, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.FailedCount)
+ if err != nil {
+ err = msgp.WrapError(err, "FailedCount")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *BucketReplicationStats) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 11
+ // string "Stats"
+ o = append(o, 0x8b, 0xa5, 0x53, 0x74, 0x61, 0x74, 0x73)
+ o = msgp.AppendMapHeader(o, uint32(len(z.Stats)))
+ for za0001, za0002 := range z.Stats {
+ o = msgp.AppendString(o, za0001)
+ if za0002 == nil {
+ o = msgp.AppendNil(o)
+ } else {
+ o, err = za0002.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Stats", za0001)
+ return
+ }
+ }
+ }
+ // string "ReplicatedSize"
+ o = append(o, 0xae, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65)
+ o = msgp.AppendInt64(o, z.ReplicatedSize)
+ // string "ReplicaSize"
+ o = append(o, 0xab, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x53, 0x69, 0x7a, 0x65)
+ o = msgp.AppendInt64(o, z.ReplicaSize)
+ // string "Failed"
+ o = append(o, 0xa6, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
+ o, err = z.Failed.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Failed")
+ return
+ }
+ // string "ReplicatedCount"
+ o = append(o, 0xaf, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ o = msgp.AppendInt64(o, z.ReplicatedCount)
+ // string "ReplicaCount"
+ o = append(o, 0xac, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ o = msgp.AppendInt64(o, z.ReplicaCount)
+ // string "QStat"
+ o = append(o, 0xa5, 0x51, 0x53, 0x74, 0x61, 0x74)
+ o, err = z.QStat.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "QStat")
+ return
+ }
+ // string "PendingSize"
+ o = append(o, 0xab, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65)
+ o = msgp.AppendInt64(o, z.PendingSize)
+ // string "FailedSize"
+ o = append(o, 0xaa, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65)
+ o = msgp.AppendInt64(o, z.FailedSize)
+ // string "PendingCount"
+ o = append(o, 0xac, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ o = msgp.AppendInt64(o, z.PendingCount)
+ // string "FailedCount"
+ o = append(o, 0xab, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ o = msgp.AppendInt64(o, z.FailedCount)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BucketReplicationStats) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Stats":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Stats")
+ return
+ }
+ if z.Stats == nil {
+ z.Stats = make(map[string]*BucketReplicationStat, zb0002)
+ } else if len(z.Stats) > 0 {
+ clear(z.Stats)
+ }
+ for zb0002 > 0 {
+ var za0002 *BucketReplicationStat
+ zb0002--
+ var za0001 string
+ za0001, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Stats")
+ return
+ }
+ if msgp.IsNil(bts) {
+ bts, err = msgp.ReadNilBytes(bts)
+ if err != nil {
+ return
+ }
+ za0002 = nil
+ } else {
+ if za0002 == nil {
+ za0002 = new(BucketReplicationStat)
+ }
+ bts, err = za0002.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Stats", za0001)
+ return
+ }
+ }
+ z.Stats[za0001] = za0002
+ }
+ case "ReplicatedSize":
+ z.ReplicatedSize, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedSize")
+ return
+ }
+ case "ReplicaSize":
+ z.ReplicaSize, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicaSize")
+ return
+ }
+ case "Failed":
+ bts, err = z.Failed.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Failed")
+ return
+ }
+ case "ReplicatedCount":
+ z.ReplicatedCount, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicatedCount")
+ return
+ }
+ case "ReplicaCount":
+ z.ReplicaCount, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicaCount")
+ return
+ }
+ case "QStat":
+ bts, err = z.QStat.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "QStat")
+ return
+ }
+ case "PendingSize":
+ z.PendingSize, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "PendingSize")
+ return
+ }
+ case "FailedSize":
+ z.FailedSize, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "FailedSize")
+ return
+ }
+ case "PendingCount":
+ z.PendingCount, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "PendingCount")
+ return
+ }
+ case "FailedCount":
+ z.FailedCount, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "FailedCount")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *BucketReplicationStats) Msgsize() (s int) {
+ s = 1 + 6 + msgp.MapHeaderSize
+ if z.Stats != nil {
+ for za0001, za0002 := range z.Stats {
+ _ = za0002
+ s += msgp.StringPrefixSize + len(za0001)
+ if za0002 == nil {
+ s += msgp.NilSize
+ } else {
+ s += za0002.Msgsize()
+ }
+ }
+ }
+ s += 15 + msgp.Int64Size + 12 + msgp.Int64Size + 7 + z.Failed.Msgsize() + 16 + msgp.Int64Size + 13 + msgp.Int64Size + 6 + z.QStat.Msgsize() + 12 + msgp.Int64Size + 11 + msgp.Int64Size + 13 + msgp.Int64Size + 12 + msgp.Int64Size
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *BucketStats) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Uptime":
+ z.Uptime, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "Uptime")
+ return
+ }
+ case "ReplicationStats":
+ err = z.ReplicationStats.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationStats")
+ return
+ }
+ case "QueueStats":
+ var zb0002 uint32
+ zb0002, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "QueueStats")
+ return
+ }
+ for zb0002 > 0 {
+ zb0002--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err, "QueueStats")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Nodes":
+ var zb0003 uint32
+ zb0003, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "QueueStats", "Nodes")
+ return
+ }
+ if cap(z.QueueStats.Nodes) >= int(zb0003) {
+ z.QueueStats.Nodes = (z.QueueStats.Nodes)[:zb0003]
+ } else {
+ z.QueueStats.Nodes = make([]ReplQNodeStats, zb0003)
+ }
+ for za0001 := range z.QueueStats.Nodes {
+ err = z.QueueStats.Nodes[za0001].DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "QueueStats", "Nodes", za0001)
+ return
+ }
+ }
+ case "Uptime":
+ z.QueueStats.Uptime, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "QueueStats", "Uptime")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err, "QueueStats")
+ return
+ }
+ }
+ }
+ case "ProxyStats":
+ err = z.ProxyStats.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "ProxyStats")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *BucketStats) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 4
+ // write "Uptime"
+ err = en.Append(0x84, 0xa6, 0x55, 0x70, 0x74, 0x69, 0x6d, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.Uptime)
+ if err != nil {
+ err = msgp.WrapError(err, "Uptime")
+ return
+ }
+ // write "ReplicationStats"
+ err = en.Append(0xb0, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = z.ReplicationStats.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationStats")
+ return
+ }
+ // write "QueueStats"
+ err = en.Append(0xaa, 0x51, 0x75, 0x65, 0x75, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ // map header, size 2
+ // write "Nodes"
+ err = en.Append(0x82, 0xa5, 0x4e, 0x6f, 0x64, 0x65, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(len(z.QueueStats.Nodes)))
+ if err != nil {
+ err = msgp.WrapError(err, "QueueStats", "Nodes")
+ return
+ }
+ for za0001 := range z.QueueStats.Nodes {
+ err = z.QueueStats.Nodes[za0001].EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "QueueStats", "Nodes", za0001)
+ return
+ }
+ }
+ // write "Uptime"
+ err = en.Append(0xa6, 0x55, 0x70, 0x74, 0x69, 0x6d, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.QueueStats.Uptime)
+ if err != nil {
+ err = msgp.WrapError(err, "QueueStats", "Uptime")
+ return
+ }
+ // write "ProxyStats"
+ err = en.Append(0xaa, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = z.ProxyStats.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "ProxyStats")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *BucketStats) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 4
+ // string "Uptime"
+ o = append(o, 0x84, 0xa6, 0x55, 0x70, 0x74, 0x69, 0x6d, 0x65)
+ o = msgp.AppendInt64(o, z.Uptime)
+ // string "ReplicationStats"
+ o = append(o, 0xb0, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73)
+ o, err = z.ReplicationStats.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationStats")
+ return
+ }
+ // string "QueueStats"
+ o = append(o, 0xaa, 0x51, 0x75, 0x65, 0x75, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73)
+ // map header, size 2
+ // string "Nodes"
+ o = append(o, 0x82, 0xa5, 0x4e, 0x6f, 0x64, 0x65, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.QueueStats.Nodes)))
+ for za0001 := range z.QueueStats.Nodes {
+ o, err = z.QueueStats.Nodes[za0001].MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "QueueStats", "Nodes", za0001)
+ return
+ }
+ }
+ // string "Uptime"
+ o = append(o, 0xa6, 0x55, 0x70, 0x74, 0x69, 0x6d, 0x65)
+ o = msgp.AppendInt64(o, z.QueueStats.Uptime)
+ // string "ProxyStats"
+ o = append(o, 0xaa, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73)
+ o, err = z.ProxyStats.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "ProxyStats")
+ return
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BucketStats) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Uptime":
+ z.Uptime, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Uptime")
+ return
+ }
+ case "ReplicationStats":
+ bts, err = z.ReplicationStats.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationStats")
+ return
+ }
+ case "QueueStats":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "QueueStats")
+ return
+ }
+ for zb0002 > 0 {
+ zb0002--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "QueueStats")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Nodes":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "QueueStats", "Nodes")
+ return
+ }
+ if cap(z.QueueStats.Nodes) >= int(zb0003) {
+ z.QueueStats.Nodes = (z.QueueStats.Nodes)[:zb0003]
+ } else {
+ z.QueueStats.Nodes = make([]ReplQNodeStats, zb0003)
+ }
+ for za0001 := range z.QueueStats.Nodes {
+ bts, err = z.QueueStats.Nodes[za0001].UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "QueueStats", "Nodes", za0001)
+ return
+ }
+ }
+ case "Uptime":
+ z.QueueStats.Uptime, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "QueueStats", "Uptime")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "QueueStats")
+ return
+ }
+ }
+ }
+ case "ProxyStats":
+ bts, err = z.ProxyStats.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ProxyStats")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *BucketStats) Msgsize() (s int) {
+ s = 1 + 7 + msgp.Int64Size + 17 + z.ReplicationStats.Msgsize() + 11 + 1 + 6 + msgp.ArrayHeaderSize
+ for za0001 := range z.QueueStats.Nodes {
+ s += z.QueueStats.Nodes[za0001].Msgsize()
+ }
+ s += 7 + msgp.Int64Size + 11 + z.ProxyStats.Msgsize()
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *BucketStatsMap) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Stats":
+ var zb0002 uint32
+ zb0002, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Stats")
+ return
+ }
+ if z.Stats == nil {
+ z.Stats = make(map[string]BucketStats, zb0002)
+ } else if len(z.Stats) > 0 {
+ clear(z.Stats)
+ }
+ for zb0002 > 0 {
+ zb0002--
+ var za0001 string
+ za0001, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Stats")
+ return
+ }
+ var za0002 BucketStats
+ err = za0002.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Stats", za0001)
+ return
+ }
+ z.Stats[za0001] = za0002
+ }
+ case "Timestamp":
+ z.Timestamp, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "Timestamp")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *BucketStatsMap) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 2
+ // write "Stats"
+ err = en.Append(0x82, 0xa5, 0x53, 0x74, 0x61, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteMapHeader(uint32(len(z.Stats)))
+ if err != nil {
+ err = msgp.WrapError(err, "Stats")
+ return
+ }
+ for za0001, za0002 := range z.Stats {
+ err = en.WriteString(za0001)
+ if err != nil {
+ err = msgp.WrapError(err, "Stats")
+ return
+ }
+ err = za0002.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Stats", za0001)
+ return
+ }
+ }
+ // write "Timestamp"
+ err = en.Append(0xa9, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.Timestamp)
+ if err != nil {
+ err = msgp.WrapError(err, "Timestamp")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *BucketStatsMap) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 2
+ // string "Stats"
+ o = append(o, 0x82, 0xa5, 0x53, 0x74, 0x61, 0x74, 0x73)
+ o = msgp.AppendMapHeader(o, uint32(len(z.Stats)))
+ for za0001, za0002 := range z.Stats {
+ o = msgp.AppendString(o, za0001)
+ o, err = za0002.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Stats", za0001)
+ return
+ }
+ }
+ // string "Timestamp"
+ o = append(o, 0xa9, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70)
+ o = msgp.AppendTime(o, z.Timestamp)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BucketStatsMap) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Stats":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Stats")
+ return
+ }
+ if z.Stats == nil {
+ z.Stats = make(map[string]BucketStats, zb0002)
+ } else if len(z.Stats) > 0 {
+ clear(z.Stats)
+ }
+ for zb0002 > 0 {
+ var za0002 BucketStats
+ zb0002--
+ var za0001 string
+ za0001, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Stats")
+ return
+ }
+ bts, err = za0002.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Stats", za0001)
+ return
+ }
+ z.Stats[za0001] = za0002
+ }
+ case "Timestamp":
+ z.Timestamp, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Timestamp")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *BucketStatsMap) Msgsize() (s int) {
+ s = 1 + 6 + msgp.MapHeaderSize
+ if z.Stats != nil {
+ for za0001, za0002 := range z.Stats {
+ _ = za0002
+ s += msgp.StringPrefixSize + len(za0001) + za0002.Msgsize()
+ }
+ }
+ s += 10 + msgp.TimeSize
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *RMetricName) DecodeMsg(dc *msgp.Reader) (err error) {
+ {
+ var zb0001 string
+ zb0001, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ (*z) = RMetricName(zb0001)
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z RMetricName) 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 RMetricName) 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 *RMetricName) 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) = RMetricName(zb0001)
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z RMetricName) Msgsize() (s int) {
+ s = msgp.StringPrefixSize + len(string(z))
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *ReplQNodeStats) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "NodeName":
+ z.NodeName, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "NodeName")
+ return
+ }
+ case "Uptime":
+ z.Uptime, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "Uptime")
+ return
+ }
+ case "ActiveWorkers":
+ err = z.ActiveWorkers.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "ActiveWorkers")
+ return
+ }
+ case "QStats":
+ err = z.QStats.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "QStats")
+ return
+ }
+ case "MRFStats":
+ err = z.MRFStats.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "MRFStats")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *ReplQNodeStats) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 5
+ // write "NodeName"
+ err = en.Append(0x85, 0xa8, 0x4e, 0x6f, 0x64, 0x65, 0x4e, 0x61, 0x6d, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.NodeName)
+ if err != nil {
+ err = msgp.WrapError(err, "NodeName")
+ return
+ }
+ // write "Uptime"
+ err = en.Append(0xa6, 0x55, 0x70, 0x74, 0x69, 0x6d, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.Uptime)
+ if err != nil {
+ err = msgp.WrapError(err, "Uptime")
+ return
+ }
+ // write "ActiveWorkers"
+ err = en.Append(0xad, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x73)
+ if err != nil {
+ return
+ }
+ err = z.ActiveWorkers.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "ActiveWorkers")
+ return
+ }
+ // write "QStats"
+ err = en.Append(0xa6, 0x51, 0x53, 0x74, 0x61, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = z.QStats.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "QStats")
+ return
+ }
+ // write "MRFStats"
+ err = en.Append(0xa8, 0x4d, 0x52, 0x46, 0x53, 0x74, 0x61, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = z.MRFStats.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "MRFStats")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *ReplQNodeStats) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 5
+ // string "NodeName"
+ o = append(o, 0x85, 0xa8, 0x4e, 0x6f, 0x64, 0x65, 0x4e, 0x61, 0x6d, 0x65)
+ o = msgp.AppendString(o, z.NodeName)
+ // string "Uptime"
+ o = append(o, 0xa6, 0x55, 0x70, 0x74, 0x69, 0x6d, 0x65)
+ o = msgp.AppendInt64(o, z.Uptime)
+ // string "ActiveWorkers"
+ o = append(o, 0xad, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x73)
+ o, err = z.ActiveWorkers.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "ActiveWorkers")
+ return
+ }
+ // string "QStats"
+ o = append(o, 0xa6, 0x51, 0x53, 0x74, 0x61, 0x74, 0x73)
+ o, err = z.QStats.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "QStats")
+ return
+ }
+ // string "MRFStats"
+ o = append(o, 0xa8, 0x4d, 0x52, 0x46, 0x53, 0x74, 0x61, 0x74, 0x73)
+ o, err = z.MRFStats.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "MRFStats")
+ return
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ReplQNodeStats) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "NodeName":
+ z.NodeName, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "NodeName")
+ return
+ }
+ case "Uptime":
+ z.Uptime, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Uptime")
+ return
+ }
+ case "ActiveWorkers":
+ bts, err = z.ActiveWorkers.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ActiveWorkers")
+ return
+ }
+ case "QStats":
+ bts, err = z.QStats.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "QStats")
+ return
+ }
+ case "MRFStats":
+ bts, err = z.MRFStats.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "MRFStats")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *ReplQNodeStats) Msgsize() (s int) {
+ s = 1 + 9 + msgp.StringPrefixSize + len(z.NodeName) + 7 + msgp.Int64Size + 14 + z.ActiveWorkers.Msgsize() + 7 + z.QStats.Msgsize() + 9 + z.MRFStats.Msgsize()
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *ReplicationLastHour) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Totals":
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Totals")
+ return
+ }
+ if zb0002 != uint32(60) {
+ err = msgp.ArrayError{Wanted: uint32(60), Got: zb0002}
+ return
+ }
+ for za0001 := range z.Totals {
+ err = z.Totals[za0001].DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Totals", za0001)
+ return
+ }
+ }
+ case "LastMin":
+ z.LastMin, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "LastMin")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *ReplicationLastHour) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 2
+ // write "Totals"
+ err = en.Append(0x82, 0xa6, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(60))
+ if err != nil {
+ err = msgp.WrapError(err, "Totals")
+ return
+ }
+ for za0001 := range z.Totals {
+ err = z.Totals[za0001].EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Totals", za0001)
+ return
+ }
+ }
+ // write "LastMin"
+ err = en.Append(0xa7, 0x4c, 0x61, 0x73, 0x74, 0x4d, 0x69, 0x6e)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.LastMin)
+ if err != nil {
+ err = msgp.WrapError(err, "LastMin")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *ReplicationLastHour) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 2
+ // string "Totals"
+ o = append(o, 0x82, 0xa6, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(60))
+ for za0001 := range z.Totals {
+ o, err = z.Totals[za0001].MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Totals", za0001)
+ return
+ }
+ }
+ // string "LastMin"
+ o = append(o, 0xa7, 0x4c, 0x61, 0x73, 0x74, 0x4d, 0x69, 0x6e)
+ o = msgp.AppendInt64(o, z.LastMin)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ReplicationLastHour) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Totals":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Totals")
+ return
+ }
+ if zb0002 != uint32(60) {
+ err = msgp.ArrayError{Wanted: uint32(60), Got: zb0002}
+ return
+ }
+ for za0001 := range z.Totals {
+ bts, err = z.Totals[za0001].UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Totals", za0001)
+ return
+ }
+ }
+ case "LastMin":
+ z.LastMin, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "LastMin")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *ReplicationLastHour) Msgsize() (s int) {
+ s = 1 + 7 + msgp.ArrayHeaderSize
+ for za0001 := range z.Totals {
+ s += z.Totals[za0001].Msgsize()
+ }
+ s += 8 + msgp.Int64Size
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *ReplicationLastMinute) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "LastMinute":
+ err = z.LastMinute.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "LastMinute")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *ReplicationLastMinute) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 1
+ // write "LastMinute"
+ err = en.Append(0x81, 0xaa, 0x4c, 0x61, 0x73, 0x74, 0x4d, 0x69, 0x6e, 0x75, 0x74, 0x65)
+ if err != nil {
+ return
+ }
+ err = z.LastMinute.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "LastMinute")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *ReplicationLastMinute) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 1
+ // string "LastMinute"
+ o = append(o, 0x81, 0xaa, 0x4c, 0x61, 0x73, 0x74, 0x4d, 0x69, 0x6e, 0x75, 0x74, 0x65)
+ o, err = z.LastMinute.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "LastMinute")
+ return
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ReplicationLastMinute) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "LastMinute":
+ bts, err = z.LastMinute.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "LastMinute")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *ReplicationLastMinute) Msgsize() (s int) {
+ s = 1 + 11 + z.LastMinute.Msgsize()
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *ReplicationLatency) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "UploadHistogram":
+ err = z.UploadHistogram.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "UploadHistogram")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *ReplicationLatency) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 1
+ // write "UploadHistogram"
+ err = en.Append(0x81, 0xaf, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x67, 0x72, 0x61, 0x6d)
+ if err != nil {
+ return
+ }
+ err = z.UploadHistogram.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "UploadHistogram")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *ReplicationLatency) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 1
+ // string "UploadHistogram"
+ o = append(o, 0x81, 0xaf, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x67, 0x72, 0x61, 0x6d)
+ o, err = z.UploadHistogram.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "UploadHistogram")
+ return
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ReplicationLatency) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "UploadHistogram":
+ bts, err = z.UploadHistogram.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "UploadHistogram")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *ReplicationLatency) Msgsize() (s int) {
+ s = 1 + 16 + z.UploadHistogram.Msgsize()
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *ReplicationQueueStats) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Nodes":
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Nodes")
+ return
+ }
+ if cap(z.Nodes) >= int(zb0002) {
+ z.Nodes = (z.Nodes)[:zb0002]
+ } else {
+ z.Nodes = make([]ReplQNodeStats, zb0002)
+ }
+ for za0001 := range z.Nodes {
+ err = z.Nodes[za0001].DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Nodes", za0001)
+ return
+ }
+ }
+ case "Uptime":
+ z.Uptime, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "Uptime")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *ReplicationQueueStats) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 2
+ // write "Nodes"
+ err = en.Append(0x82, 0xa5, 0x4e, 0x6f, 0x64, 0x65, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(len(z.Nodes)))
+ if err != nil {
+ err = msgp.WrapError(err, "Nodes")
+ return
+ }
+ for za0001 := range z.Nodes {
+ err = z.Nodes[za0001].EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Nodes", za0001)
+ return
+ }
+ }
+ // write "Uptime"
+ err = en.Append(0xa6, 0x55, 0x70, 0x74, 0x69, 0x6d, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.Uptime)
+ if err != nil {
+ err = msgp.WrapError(err, "Uptime")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *ReplicationQueueStats) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 2
+ // string "Nodes"
+ o = append(o, 0x82, 0xa5, 0x4e, 0x6f, 0x64, 0x65, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.Nodes)))
+ for za0001 := range z.Nodes {
+ o, err = z.Nodes[za0001].MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Nodes", za0001)
+ return
+ }
+ }
+ // string "Uptime"
+ o = append(o, 0xa6, 0x55, 0x70, 0x74, 0x69, 0x6d, 0x65)
+ o = msgp.AppendInt64(o, z.Uptime)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ReplicationQueueStats) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Nodes":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Nodes")
+ return
+ }
+ if cap(z.Nodes) >= int(zb0002) {
+ z.Nodes = (z.Nodes)[:zb0002]
+ } else {
+ z.Nodes = make([]ReplQNodeStats, zb0002)
+ }
+ for za0001 := range z.Nodes {
+ bts, err = z.Nodes[za0001].UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Nodes", za0001)
+ return
+ }
+ }
+ case "Uptime":
+ z.Uptime, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Uptime")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *ReplicationQueueStats) Msgsize() (s int) {
+ s = 1 + 6 + msgp.ArrayHeaderSize
+ for za0001 := range z.Nodes {
+ s += z.Nodes[za0001].Msgsize()
+ }
+ s += 7 + msgp.Int64Size
+ return
+}
diff --git a/cmd/bucket-stats_gen_test.go b/cmd/bucket-stats_gen_test.go
new file mode 100644
index 00000000..18344b43
--- /dev/null
+++ b/cmd/bucket-stats_gen_test.go
@@ -0,0 +1,1027 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+func TestMarshalUnmarshalBucketReplicationStat(t *testing.T) {
+ v := BucketReplicationStat{}
+ 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 BenchmarkMarshalMsgBucketReplicationStat(b *testing.B) {
+ v := BucketReplicationStat{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBucketReplicationStat(b *testing.B) {
+ v := BucketReplicationStat{}
+ 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 BenchmarkUnmarshalBucketReplicationStat(b *testing.B) {
+ v := BucketReplicationStat{}
+ 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 TestEncodeDecodeBucketReplicationStat(t *testing.T) {
+ v := BucketReplicationStat{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBucketReplicationStat Msgsize() is inaccurate")
+ }
+
+ vn := BucketReplicationStat{}
+ 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 BenchmarkEncodeBucketReplicationStat(b *testing.B) {
+ v := BucketReplicationStat{}
+ 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 BenchmarkDecodeBucketReplicationStat(b *testing.B) {
+ v := BucketReplicationStat{}
+ 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 TestMarshalUnmarshalBucketReplicationStats(t *testing.T) {
+ v := BucketReplicationStats{}
+ 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 BenchmarkMarshalMsgBucketReplicationStats(b *testing.B) {
+ v := BucketReplicationStats{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBucketReplicationStats(b *testing.B) {
+ v := BucketReplicationStats{}
+ 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 BenchmarkUnmarshalBucketReplicationStats(b *testing.B) {
+ v := BucketReplicationStats{}
+ 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 TestEncodeDecodeBucketReplicationStats(t *testing.T) {
+ v := BucketReplicationStats{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBucketReplicationStats Msgsize() is inaccurate")
+ }
+
+ vn := BucketReplicationStats{}
+ 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 BenchmarkEncodeBucketReplicationStats(b *testing.B) {
+ v := BucketReplicationStats{}
+ 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 BenchmarkDecodeBucketReplicationStats(b *testing.B) {
+ v := BucketReplicationStats{}
+ 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 TestMarshalUnmarshalBucketStats(t *testing.T) {
+ v := BucketStats{}
+ 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 BenchmarkMarshalMsgBucketStats(b *testing.B) {
+ v := BucketStats{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBucketStats(b *testing.B) {
+ v := BucketStats{}
+ 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 BenchmarkUnmarshalBucketStats(b *testing.B) {
+ v := BucketStats{}
+ 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 TestEncodeDecodeBucketStats(t *testing.T) {
+ v := BucketStats{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBucketStats Msgsize() is inaccurate")
+ }
+
+ vn := BucketStats{}
+ 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 BenchmarkEncodeBucketStats(b *testing.B) {
+ v := BucketStats{}
+ 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 BenchmarkDecodeBucketStats(b *testing.B) {
+ v := BucketStats{}
+ 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 TestMarshalUnmarshalBucketStatsMap(t *testing.T) {
+ v := BucketStatsMap{}
+ bts, err := v.MarshalMsg(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ left, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
+ }
+
+ left, err = msgp.Skip(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
+ }
+}
+
+func BenchmarkMarshalMsgBucketStatsMap(b *testing.B) {
+ v := BucketStatsMap{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgBucketStatsMap(b *testing.B) {
+ v := BucketStatsMap{}
+ bts := make([]byte, 0, v.Msgsize())
+ bts, _ = v.MarshalMsg(bts[0:0])
+ b.SetBytes(int64(len(bts)))
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ bts, _ = v.MarshalMsg(bts[0:0])
+ }
+}
+
+func BenchmarkUnmarshalBucketStatsMap(b *testing.B) {
+ v := BucketStatsMap{}
+ bts, _ := v.MarshalMsg(nil)
+ b.ReportAllocs()
+ b.SetBytes(int64(len(bts)))
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func TestEncodeDecodeBucketStatsMap(t *testing.T) {
+ v := BucketStatsMap{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeBucketStatsMap Msgsize() is inaccurate")
+ }
+
+ vn := BucketStatsMap{}
+ err := msgp.Decode(&buf, &vn)
+ if err != nil {
+ t.Error(err)
+ }
+
+ buf.Reset()
+ msgp.Encode(&buf, &v)
+ err = msgp.NewReader(&buf).Skip()
+ if err != nil {
+ t.Error(err)
+ }
+}
+
+func BenchmarkEncodeBucketStatsMap(b *testing.B) {
+ v := BucketStatsMap{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+ b.SetBytes(int64(buf.Len()))
+ en := msgp.NewWriter(msgp.Nowhere)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.EncodeMsg(en)
+ }
+ en.Flush()
+}
+
+func BenchmarkDecodeBucketStatsMap(b *testing.B) {
+ v := BucketStatsMap{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+ b.SetBytes(int64(buf.Len()))
+ rd := msgp.NewEndlessReader(buf.Bytes(), b)
+ dc := msgp.NewReader(rd)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ err := v.DecodeMsg(dc)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func TestMarshalUnmarshalReplQNodeStats(t *testing.T) {
+ v := ReplQNodeStats{}
+ 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 BenchmarkMarshalMsgReplQNodeStats(b *testing.B) {
+ v := ReplQNodeStats{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgReplQNodeStats(b *testing.B) {
+ v := ReplQNodeStats{}
+ 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 BenchmarkUnmarshalReplQNodeStats(b *testing.B) {
+ v := ReplQNodeStats{}
+ 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 TestEncodeDecodeReplQNodeStats(t *testing.T) {
+ v := ReplQNodeStats{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeReplQNodeStats Msgsize() is inaccurate")
+ }
+
+ vn := ReplQNodeStats{}
+ 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 BenchmarkEncodeReplQNodeStats(b *testing.B) {
+ v := ReplQNodeStats{}
+ 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 BenchmarkDecodeReplQNodeStats(b *testing.B) {
+ v := ReplQNodeStats{}
+ 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 TestMarshalUnmarshalReplicationLastHour(t *testing.T) {
+ v := ReplicationLastHour{}
+ 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 BenchmarkMarshalMsgReplicationLastHour(b *testing.B) {
+ v := ReplicationLastHour{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgReplicationLastHour(b *testing.B) {
+ v := ReplicationLastHour{}
+ 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 BenchmarkUnmarshalReplicationLastHour(b *testing.B) {
+ v := ReplicationLastHour{}
+ 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 TestEncodeDecodeReplicationLastHour(t *testing.T) {
+ v := ReplicationLastHour{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeReplicationLastHour Msgsize() is inaccurate")
+ }
+
+ vn := ReplicationLastHour{}
+ 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 BenchmarkEncodeReplicationLastHour(b *testing.B) {
+ v := ReplicationLastHour{}
+ 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 BenchmarkDecodeReplicationLastHour(b *testing.B) {
+ v := ReplicationLastHour{}
+ 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 TestMarshalUnmarshalReplicationLastMinute(t *testing.T) {
+ v := ReplicationLastMinute{}
+ 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 BenchmarkMarshalMsgReplicationLastMinute(b *testing.B) {
+ v := ReplicationLastMinute{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgReplicationLastMinute(b *testing.B) {
+ v := ReplicationLastMinute{}
+ 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 BenchmarkUnmarshalReplicationLastMinute(b *testing.B) {
+ v := ReplicationLastMinute{}
+ 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 TestEncodeDecodeReplicationLastMinute(t *testing.T) {
+ v := ReplicationLastMinute{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeReplicationLastMinute Msgsize() is inaccurate")
+ }
+
+ vn := ReplicationLastMinute{}
+ 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 BenchmarkEncodeReplicationLastMinute(b *testing.B) {
+ v := ReplicationLastMinute{}
+ 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 BenchmarkDecodeReplicationLastMinute(b *testing.B) {
+ v := ReplicationLastMinute{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+ b.SetBytes(int64(buf.Len()))
+ rd := msgp.NewEndlessReader(buf.Bytes(), b)
+ dc := msgp.NewReader(rd)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ err := v.DecodeMsg(dc)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func TestMarshalUnmarshalReplicationLatency(t *testing.T) {
+ v := ReplicationLatency{}
+ bts, err := v.MarshalMsg(nil)
+ 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 BenchmarkMarshalMsgReplicationLatency(b *testing.B) {
+ v := ReplicationLatency{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgReplicationLatency(b *testing.B) {
+ v := ReplicationLatency{}
+ 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 BenchmarkUnmarshalReplicationLatency(b *testing.B) {
+ v := ReplicationLatency{}
+ 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 TestEncodeDecodeReplicationLatency(t *testing.T) {
+ v := ReplicationLatency{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeReplicationLatency Msgsize() is inaccurate")
+ }
+
+ vn := ReplicationLatency{}
+ 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 BenchmarkEncodeReplicationLatency(b *testing.B) {
+ v := ReplicationLatency{}
+ 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 BenchmarkDecodeReplicationLatency(b *testing.B) {
+ v := ReplicationLatency{}
+ 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 TestMarshalUnmarshalReplicationQueueStats(t *testing.T) {
+ v := ReplicationQueueStats{}
+ 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 BenchmarkMarshalMsgReplicationQueueStats(b *testing.B) {
+ v := ReplicationQueueStats{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgReplicationQueueStats(b *testing.B) {
+ v := ReplicationQueueStats{}
+ 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 BenchmarkUnmarshalReplicationQueueStats(b *testing.B) {
+ v := ReplicationQueueStats{}
+ 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 TestEncodeDecodeReplicationQueueStats(t *testing.T) {
+ v := ReplicationQueueStats{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeReplicationQueueStats Msgsize() is inaccurate")
+ }
+
+ vn := ReplicationQueueStats{}
+ 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 BenchmarkEncodeReplicationQueueStats(b *testing.B) {
+ v := ReplicationQueueStats{}
+ 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 BenchmarkDecodeReplicationQueueStats(b *testing.B) {
+ v := ReplicationQueueStats{}
+ 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)
+ }
+ }
+}
diff --git a/cmd/bucket-targets.go b/cmd/bucket-targets.go
new file mode 100644
index 00000000..9bd67a05
--- /dev/null
+++ b/cmd/bucket-targets.go
@@ -0,0 +1,768 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "errors"
+ "maps"
+ "net/url"
+ "sync"
+ "time"
+
+ jsoniter "github.com/json-iterator/go"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio-go/v7"
+ "github.com/minio/minio-go/v7/pkg/credentials"
+ "github.com/minio/minio/internal/bucket/replication"
+ "github.com/minio/minio/internal/crypto"
+ "github.com/minio/minio/internal/kms"
+)
+
+const (
+ defaultHealthCheckDuration = 5 * time.Second
+ // default interval for reload of all remote target endpoints
+ defaultHealthCheckReloadDuration = 30 * time.Minute
+)
+
+type arnTarget struct {
+ Client *TargetClient
+ lastRefresh time.Time
+}
+
+// arnErrs represents number of errors seen for a ARN and if update is in progress
+// to refresh remote targets from bucket metadata.
+type arnErrs struct {
+ count int64
+ updateInProgress bool
+ bucket string
+}
+
+// BucketTargetSys represents bucket targets subsystem
+type BucketTargetSys struct {
+ sync.RWMutex
+ arnRemotesMap map[string]arnTarget
+ targetsMap map[string][]madmin.BucketTarget
+ hMutex sync.RWMutex
+ hc map[string]epHealth
+ hcClient *madmin.AnonymousClient
+ aMutex sync.RWMutex
+ arnErrsMap map[string]arnErrs // map of ARN to error count of failures to get target
+}
+
+type latencyStat struct {
+ lastmin lastMinuteLatency
+ curr time.Duration
+ avg time.Duration
+ peak time.Duration
+ N int64
+}
+
+func (l *latencyStat) update(d time.Duration) {
+ l.lastmin.add(d)
+ l.N++
+ if d > l.peak {
+ l.peak = d
+ }
+ l.curr = l.lastmin.getTotal().avg()
+ l.avg = time.Duration((int64(l.avg)*(l.N-1) + int64(l.curr)) / l.N)
+}
+
+// epHealth struct represents health of a replication target endpoint.
+type epHealth struct {
+ Endpoint string
+ Scheme string
+ Online bool
+ lastOnline time.Time
+ lastHCAt time.Time
+ offlineDuration time.Duration
+ latency latencyStat
+}
+
+// isOffline returns current liveness result of remote target. Add endpoint to
+// healthCheck map if missing and default to online status
+func (sys *BucketTargetSys) isOffline(ep *url.URL) bool {
+ sys.hMutex.RLock()
+ defer sys.hMutex.RUnlock()
+ if h, ok := sys.hc[ep.Host]; ok {
+ return !h.Online
+ }
+ go sys.initHC(ep)
+ return false
+}
+
+// markOffline sets endpoint to offline if network i/o timeout seen.
+func (sys *BucketTargetSys) markOffline(ep *url.URL) {
+ sys.hMutex.Lock()
+ defer sys.hMutex.Unlock()
+ if h, ok := sys.hc[ep.Host]; ok {
+ h.Online = false
+ sys.hc[ep.Host] = h
+ }
+}
+
+func (sys *BucketTargetSys) initHC(ep *url.URL) {
+ sys.hMutex.Lock()
+ sys.hc[ep.Host] = epHealth{
+ Endpoint: ep.Host,
+ Scheme: ep.Scheme,
+ Online: true,
+ }
+ sys.hMutex.Unlock()
+}
+
+// newHCClient initializes an anonymous client for performing health check on the remote endpoints
+func newHCClient() *madmin.AnonymousClient {
+ clnt, e := madmin.NewAnonymousClientNoEndpoint()
+ if e != nil {
+ bugLogIf(GlobalContext, errors.New("Unable to initialize health check client"))
+ return nil
+ }
+ clnt.SetCustomTransport(globalRemoteTargetTransport)
+ return clnt
+}
+
+// heartBeat performs liveness check on remote endpoints.
+func (sys *BucketTargetSys) heartBeat(ctx context.Context) {
+ hcTimer := time.NewTimer(defaultHealthCheckDuration)
+ defer hcTimer.Stop()
+ for {
+ select {
+ case <-hcTimer.C:
+ sys.hMutex.RLock()
+ eps := make([]madmin.ServerProperties, 0, len(sys.hc))
+ for _, ep := range sys.hc {
+ eps = append(eps, madmin.ServerProperties{Endpoint: ep.Endpoint, Scheme: ep.Scheme})
+ }
+ sys.hMutex.RUnlock()
+
+ if len(eps) > 0 {
+ cctx, cancel := context.WithTimeout(ctx, 30*time.Second)
+ m := make(map[string]epHealth, len(eps))
+ start := time.Now()
+
+ for result := range sys.hcClient.Alive(cctx, madmin.AliveOpts{}, eps...) {
+ var lastOnline time.Time
+ var offline time.Duration
+ // var deploymentID string
+ sys.hMutex.RLock()
+ prev, ok := sys.hc[result.Endpoint.Host]
+ sys.hMutex.RUnlock()
+ if ok {
+ if prev.Online != result.Online || !result.Online {
+ if !prev.lastHCAt.IsZero() {
+ offline = time.Since(prev.lastHCAt) + prev.offlineDuration
+ } else {
+ offline = prev.offlineDuration
+ }
+ } else if result.Online {
+ offline = prev.offlineDuration
+ }
+ }
+ lastOnline = prev.lastOnline
+ if result.Online {
+ lastOnline = time.Now()
+ }
+ l := prev.latency
+ l.update(time.Since(start))
+ m[result.Endpoint.Host] = epHealth{
+ Endpoint: result.Endpoint.Host,
+ Scheme: result.Endpoint.Scheme,
+ Online: result.Online,
+ lastOnline: lastOnline,
+ offlineDuration: offline,
+ lastHCAt: time.Now(),
+ latency: l,
+ }
+ }
+ cancel()
+ sys.hMutex.Lock()
+ sys.hc = m
+ sys.hMutex.Unlock()
+ }
+ hcTimer.Reset(defaultHealthCheckDuration)
+ case <-ctx.Done():
+ return
+ }
+ }
+}
+
+// periodically rebuild the healthCheck map from list of targets to clear
+// out stale endpoints
+func (sys *BucketTargetSys) reloadHealthCheckers(ctx context.Context) {
+ m := make(map[string]epHealth)
+ tgts := sys.ListTargets(ctx, "", "")
+ sys.hMutex.Lock()
+ for _, t := range tgts {
+ if _, ok := m[t.Endpoint]; !ok {
+ scheme := "http"
+ if t.Secure {
+ scheme = "https"
+ }
+ epHealth := epHealth{
+ Online: true,
+ Endpoint: t.Endpoint,
+ Scheme: scheme,
+ }
+ if prev, ok := sys.hc[t.Endpoint]; ok {
+ epHealth.lastOnline = prev.lastOnline
+ epHealth.offlineDuration = prev.offlineDuration
+ epHealth.lastHCAt = prev.lastHCAt
+ epHealth.latency = prev.latency
+ }
+ m[t.Endpoint] = epHealth
+ }
+ }
+ // swap out the map
+ sys.hc = m
+ sys.hMutex.Unlock()
+}
+
+func (sys *BucketTargetSys) healthStats() map[string]epHealth {
+ sys.hMutex.RLock()
+ defer sys.hMutex.RUnlock()
+ m := make(map[string]epHealth, len(sys.hc))
+ maps.Copy(m, sys.hc)
+ return m
+}
+
+// ListTargets lists bucket targets across tenant or for individual bucket, and returns
+// results filtered by arnType
+func (sys *BucketTargetSys) ListTargets(ctx context.Context, bucket, arnType string) (targets []madmin.BucketTarget) {
+ h := sys.healthStats()
+
+ if bucket != "" {
+ if ts, err := sys.ListBucketTargets(ctx, bucket); err == nil {
+ for _, t := range ts.Targets {
+ if string(t.Type) == arnType || arnType == "" {
+ if hs, ok := h[t.URL().Host]; ok {
+ t.TotalDowntime = hs.offlineDuration
+ t.Online = hs.Online
+ t.LastOnline = hs.lastOnline
+ t.Latency = madmin.LatencyStat{
+ Curr: hs.latency.curr,
+ Avg: hs.latency.avg,
+ Max: hs.latency.peak,
+ }
+ }
+ targets = append(targets, t.Clone())
+ }
+ }
+ }
+ return targets
+ }
+ sys.RLock()
+ defer sys.RUnlock()
+ for _, tgts := range sys.targetsMap {
+ for _, t := range tgts {
+ if string(t.Type) == arnType || arnType == "" {
+ if hs, ok := h[t.URL().Host]; ok {
+ t.TotalDowntime = hs.offlineDuration
+ t.Online = hs.Online
+ t.LastOnline = hs.lastOnline
+ t.Latency = madmin.LatencyStat{
+ Curr: hs.latency.curr,
+ Avg: hs.latency.avg,
+ Max: hs.latency.peak,
+ }
+ }
+ targets = append(targets, t.Clone())
+ }
+ }
+ }
+ return targets
+}
+
+// ListBucketTargets - gets list of bucket targets for this bucket.
+func (sys *BucketTargetSys) ListBucketTargets(ctx context.Context, bucket string) (*madmin.BucketTargets, error) {
+ sys.RLock()
+ defer sys.RUnlock()
+
+ tgts, ok := sys.targetsMap[bucket]
+ if ok {
+ return &madmin.BucketTargets{Targets: tgts}, nil
+ }
+ return nil, BucketRemoteTargetNotFound{Bucket: bucket}
+}
+
+// Delete clears targets present for a bucket
+func (sys *BucketTargetSys) Delete(bucket string) {
+ sys.Lock()
+ defer sys.Unlock()
+ tgts, ok := sys.targetsMap[bucket]
+ if !ok {
+ return
+ }
+ for _, t := range tgts {
+ delete(sys.arnRemotesMap, t.Arn)
+ }
+ delete(sys.targetsMap, bucket)
+}
+
+// SetTarget - sets a new minio-go client target for this bucket.
+func (sys *BucketTargetSys) SetTarget(ctx context.Context, bucket string, tgt *madmin.BucketTarget, update bool) error {
+ if !tgt.Type.IsValid() && !update {
+ return BucketRemoteArnTypeInvalid{Bucket: bucket}
+ }
+ clnt, err := sys.getRemoteTargetClient(tgt)
+ if err != nil {
+ return BucketRemoteTargetNotFound{Bucket: tgt.TargetBucket, Err: err}
+ }
+ // validate if target credentials are ok
+ exists, err := clnt.BucketExists(ctx, tgt.TargetBucket)
+ if err != nil {
+ switch minio.ToErrorResponse(err).Code {
+ case "NoSuchBucket":
+ return BucketRemoteTargetNotFound{Bucket: tgt.TargetBucket, Err: err}
+ case "AccessDenied":
+ return RemoteTargetConnectionErr{Bucket: tgt.TargetBucket, AccessKey: tgt.Credentials.AccessKey, Err: err}
+ }
+ return RemoteTargetConnectionErr{Bucket: tgt.TargetBucket, AccessKey: tgt.Credentials.AccessKey, Err: err}
+ }
+ if !exists {
+ return BucketRemoteTargetNotFound{Bucket: tgt.TargetBucket}
+ }
+ if tgt.Type == madmin.ReplicationService {
+ if !globalBucketVersioningSys.Enabled(bucket) {
+ return BucketReplicationSourceNotVersioned{Bucket: bucket}
+ }
+ vcfg, err := clnt.GetBucketVersioning(ctx, tgt.TargetBucket)
+ if err != nil {
+ return RemoteTargetConnectionErr{Bucket: tgt.TargetBucket, Err: err, AccessKey: tgt.Credentials.AccessKey}
+ }
+ if !vcfg.Enabled() {
+ return BucketRemoteTargetNotVersioned{Bucket: tgt.TargetBucket}
+ }
+ }
+
+ // Check if target is a MinIO server and alive
+ hcCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
+ scheme := "http"
+ if tgt.Secure {
+ scheme = "https"
+ }
+ result := <-sys.hcClient.Alive(hcCtx, madmin.AliveOpts{}, madmin.ServerProperties{
+ Endpoint: tgt.Endpoint,
+ Scheme: scheme,
+ })
+
+ cancel()
+ if result.Error != nil {
+ return RemoteTargetConnectionErr{Bucket: tgt.TargetBucket, Err: result.Error, AccessKey: tgt.Credentials.AccessKey}
+ }
+ if !result.Online {
+ err := errors.New("Health check timed out after 3 seconds")
+ return RemoteTargetConnectionErr{Err: err}
+ }
+
+ sys.Lock()
+ defer sys.Unlock()
+
+ tgts := sys.targetsMap[bucket]
+ newtgts := make([]madmin.BucketTarget, len(tgts))
+ found := false
+ for idx, t := range tgts {
+ if t.Type == tgt.Type {
+ if t.Arn == tgt.Arn {
+ if !update {
+ return BucketRemoteAlreadyExists{Bucket: t.TargetBucket}
+ }
+ newtgts[idx] = *tgt
+ found = true
+ continue
+ }
+ // fail if endpoint is already present in list of targets and not a matching ARN
+ if t.Endpoint == tgt.Endpoint {
+ return BucketRemoteAlreadyExists{Bucket: t.TargetBucket}
+ }
+ }
+ newtgts[idx] = t
+ }
+ if !found && !update {
+ newtgts = append(newtgts, *tgt)
+ }
+
+ sys.targetsMap[bucket] = newtgts
+ sys.arnRemotesMap[tgt.Arn] = arnTarget{Client: clnt}
+ sys.updateBandwidthLimit(bucket, tgt.Arn, tgt.BandwidthLimit)
+ return nil
+}
+
+func (sys *BucketTargetSys) updateBandwidthLimit(bucket, arn string, limit int64) {
+ if limit == 0 {
+ globalBucketMonitor.DeleteBucketThrottle(bucket, arn)
+ return
+ }
+ // Setup bandwidth throttling
+
+ globalBucketMonitor.SetBandwidthLimit(bucket, arn, limit)
+}
+
+// RemoveTarget - removes a remote bucket target for this source bucket.
+func (sys *BucketTargetSys) RemoveTarget(ctx context.Context, bucket, arnStr string) error {
+ if arnStr == "" {
+ return BucketRemoteArnInvalid{Bucket: bucket}
+ }
+
+ arn, err := madmin.ParseARN(arnStr)
+ if err != nil {
+ return BucketRemoteArnInvalid{Bucket: bucket}
+ }
+
+ if arn.Type == madmin.ReplicationService {
+ // reject removal of remote target if replication configuration is present
+ rcfg, err := getReplicationConfig(ctx, bucket)
+ if err == nil && rcfg != nil {
+ for _, tgtArn := range rcfg.FilterTargetArns(replication.ObjectOpts{OpType: replication.AllReplicationType}) {
+ if err == nil && (tgtArn == arnStr || rcfg.RoleArn == arnStr) {
+ sys.RLock()
+ _, ok := sys.arnRemotesMap[arnStr]
+ sys.RUnlock()
+ if ok {
+ return BucketRemoteRemoveDisallowed{Bucket: bucket}
+ }
+ }
+ }
+ }
+ }
+
+ // delete ARN type from list of matching targets
+ sys.Lock()
+ defer sys.Unlock()
+ found := false
+ tgts, ok := sys.targetsMap[bucket]
+ if !ok {
+ return BucketRemoteTargetNotFound{Bucket: bucket}
+ }
+ targets := make([]madmin.BucketTarget, 0, len(tgts))
+ for _, tgt := range tgts {
+ if tgt.Arn != arnStr {
+ targets = append(targets, tgt)
+ continue
+ }
+ found = true
+ }
+ if !found {
+ return BucketRemoteTargetNotFound{Bucket: bucket}
+ }
+ sys.targetsMap[bucket] = targets
+ delete(sys.arnRemotesMap, arnStr)
+ sys.updateBandwidthLimit(bucket, arnStr, 0)
+ return nil
+}
+
+func (sys *BucketTargetSys) markRefreshInProgress(bucket, arn string) {
+ sys.aMutex.Lock()
+ defer sys.aMutex.Unlock()
+ if v, ok := sys.arnErrsMap[arn]; !ok {
+ sys.arnErrsMap[arn] = arnErrs{
+ updateInProgress: true,
+ count: v.count + 1,
+ bucket: bucket,
+ }
+ }
+}
+
+func (sys *BucketTargetSys) markRefreshDone(bucket, arn string) {
+ sys.aMutex.Lock()
+ defer sys.aMutex.Unlock()
+ if v, ok := sys.arnErrsMap[arn]; ok {
+ sys.arnErrsMap[arn] = arnErrs{
+ updateInProgress: false,
+ count: v.count,
+ bucket: bucket,
+ }
+ }
+}
+
+func (sys *BucketTargetSys) isReloadingTarget(bucket, arn string) bool {
+ sys.aMutex.RLock()
+ defer sys.aMutex.RUnlock()
+ if v, ok := sys.arnErrsMap[arn]; ok {
+ return v.updateInProgress
+ }
+ return false
+}
+
+func (sys *BucketTargetSys) incTargetErr(bucket, arn string) {
+ sys.aMutex.Lock()
+ defer sys.aMutex.Unlock()
+ if v, ok := sys.arnErrsMap[arn]; ok {
+ sys.arnErrsMap[arn] = arnErrs{
+ updateInProgress: v.updateInProgress,
+ count: v.count + 1,
+ }
+ }
+}
+
+// GetRemoteTargetClient returns minio-go client for replication target instance
+func (sys *BucketTargetSys) GetRemoteTargetClient(bucket, arn string) *TargetClient {
+ sys.RLock()
+ tgt := sys.arnRemotesMap[arn]
+ sys.RUnlock()
+
+ if tgt.Client != nil {
+ return tgt.Client
+ }
+ defer func() { // lazy refresh remote targets
+ if tgt.Client == nil && !sys.isReloadingTarget(bucket, arn) && (tgt.lastRefresh.Equal(timeSentinel) || tgt.lastRefresh.Before(UTCNow().Add(-5*time.Minute))) {
+ tgts, err := globalBucketMetadataSys.GetBucketTargetsConfig(bucket)
+ if err == nil {
+ sys.markRefreshInProgress(bucket, arn)
+ sys.UpdateAllTargets(bucket, tgts)
+ sys.markRefreshDone(bucket, arn)
+ }
+ }
+ sys.incTargetErr(bucket, arn)
+ }()
+ return nil
+}
+
+// GetRemoteBucketTargetByArn returns BucketTarget for a ARN
+func (sys *BucketTargetSys) GetRemoteBucketTargetByArn(ctx context.Context, bucket, arn string) madmin.BucketTarget {
+ sys.RLock()
+ defer sys.RUnlock()
+ var tgt madmin.BucketTarget
+ for _, t := range sys.targetsMap[bucket] {
+ if t.Arn == arn {
+ tgt = t.Clone()
+ tgt.Credentials = t.Credentials
+ return tgt
+ }
+ }
+ return tgt
+}
+
+// NewBucketTargetSys - creates new replication system.
+func NewBucketTargetSys(ctx context.Context) *BucketTargetSys {
+ sys := &BucketTargetSys{
+ arnRemotesMap: make(map[string]arnTarget),
+ targetsMap: make(map[string][]madmin.BucketTarget),
+ arnErrsMap: make(map[string]arnErrs),
+ hc: make(map[string]epHealth),
+ hcClient: newHCClient(),
+ }
+ // reload healthCheck endpoints map periodically to remove stale endpoints from the map.
+ go func() {
+ rTimer := time.NewTimer(defaultHealthCheckReloadDuration)
+ defer rTimer.Stop()
+ for {
+ select {
+ case <-rTimer.C:
+ sys.reloadHealthCheckers(ctx)
+ rTimer.Reset(defaultHealthCheckReloadDuration)
+ case <-ctx.Done():
+ return
+ }
+ }
+ }()
+ go sys.heartBeat(ctx)
+ return sys
+}
+
+// UpdateAllTargets updates target to reflect metadata updates
+func (sys *BucketTargetSys) UpdateAllTargets(bucket string, tgts *madmin.BucketTargets) {
+ if sys == nil {
+ return
+ }
+ sys.Lock()
+ defer sys.Unlock()
+
+ // Remove existingtarget and arn association
+ if stgts, ok := sys.targetsMap[bucket]; ok {
+ for _, t := range stgts {
+ delete(sys.arnRemotesMap, t.Arn)
+ }
+ delete(sys.targetsMap, bucket)
+ }
+
+ if tgts != nil {
+ for _, tgt := range tgts.Targets {
+ tgtClient, err := sys.getRemoteTargetClient(&tgt)
+ if err != nil {
+ continue
+ }
+ sys.arnRemotesMap[tgt.Arn] = arnTarget{
+ Client: tgtClient,
+ lastRefresh: UTCNow(),
+ }
+ sys.updateBandwidthLimit(bucket, tgt.Arn, tgt.BandwidthLimit)
+ }
+
+ if !tgts.Empty() {
+ sys.targetsMap[bucket] = tgts.Targets
+ }
+ }
+}
+
+// create minio-go clients for buckets having remote targets
+func (sys *BucketTargetSys) set(bucket string, meta BucketMetadata) {
+ cfg := meta.bucketTargetConfig
+ if cfg == nil || cfg.Empty() {
+ return
+ }
+ sys.Lock()
+ defer sys.Unlock()
+ for _, tgt := range cfg.Targets {
+ tgtClient, err := sys.getRemoteTargetClient(&tgt)
+ if err != nil {
+ replLogIf(GlobalContext, err)
+ continue
+ }
+ sys.arnRemotesMap[tgt.Arn] = arnTarget{Client: tgtClient}
+ sys.updateBandwidthLimit(bucket, tgt.Arn, tgt.BandwidthLimit)
+ }
+ sys.targetsMap[bucket] = cfg.Targets
+}
+
+// Returns a minio-go Client configured to access remote host described in replication target config.
+func (sys *BucketTargetSys) getRemoteTargetClient(tcfg *madmin.BucketTarget) (*TargetClient, error) {
+ config := tcfg.Credentials
+ creds := credentials.NewStaticV4(config.AccessKey, config.SecretKey, "")
+
+ api, err := minio.New(tcfg.Endpoint, &minio.Options{
+ Creds: creds,
+ Secure: tcfg.Secure,
+ Region: tcfg.Region,
+ Transport: globalRemoteTargetTransport,
+ })
+ if err != nil {
+ return nil, err
+ }
+ api.SetAppInfo("minio-replication-target", ReleaseTag+" "+tcfg.Arn)
+
+ hcDuration := defaultHealthCheckDuration
+ if tcfg.HealthCheckDuration >= 1 { // require minimum health check duration of 1 sec.
+ hcDuration = tcfg.HealthCheckDuration
+ }
+ tc := &TargetClient{
+ Client: api,
+ healthCheckDuration: hcDuration,
+ replicateSync: tcfg.ReplicationSync,
+ Bucket: tcfg.TargetBucket,
+ StorageClass: tcfg.StorageClass,
+ disableProxy: tcfg.DisableProxy,
+ ARN: tcfg.Arn,
+ ResetID: tcfg.ResetID,
+ Endpoint: tcfg.Endpoint,
+ Secure: tcfg.Secure,
+ }
+ return tc, nil
+}
+
+// getRemoteARN gets existing ARN for an endpoint or generates a new one.
+func (sys *BucketTargetSys) getRemoteARN(bucket string, target *madmin.BucketTarget, deplID string) (arn string, exists bool) {
+ if target == nil {
+ return arn, exists
+ }
+ sys.RLock()
+ defer sys.RUnlock()
+ tgts := sys.targetsMap[bucket]
+ for _, tgt := range tgts {
+ if tgt.Type == target.Type &&
+ tgt.TargetBucket == target.TargetBucket &&
+ target.URL().String() == tgt.URL().String() &&
+ tgt.Credentials.AccessKey == target.Credentials.AccessKey {
+ return tgt.Arn, true
+ }
+ }
+ if !target.Type.IsValid() {
+ return arn, exists
+ }
+ return generateARN(target, deplID), false
+}
+
+// getRemoteARNForPeer returns the remote target for a peer site in site replication
+func (sys *BucketTargetSys) getRemoteARNForPeer(bucket string, peer madmin.PeerInfo) string {
+ sys.RLock()
+ defer sys.RUnlock()
+ tgts := sys.targetsMap[bucket]
+ for _, target := range tgts {
+ ep, _ := url.Parse(peer.Endpoint)
+ if target.SourceBucket == bucket &&
+ target.TargetBucket == bucket &&
+ target.Endpoint == ep.Host &&
+ target.Secure == (ep.Scheme == "https") &&
+ target.Type == madmin.ReplicationService {
+ return target.Arn
+ }
+ }
+ return ""
+}
+
+// generate ARN that is unique to this target type
+func generateARN(t *madmin.BucketTarget, deplID string) string {
+ uuid := deplID
+ if uuid == "" {
+ uuid = mustGetUUID()
+ }
+ arn := madmin.ARN{
+ Type: t.Type,
+ ID: uuid,
+ Region: t.Region,
+ Bucket: t.TargetBucket,
+ }
+ return arn.String()
+}
+
+// Returns parsed target config. If KMS is configured, remote target is decrypted
+func parseBucketTargetConfig(bucket string, cdata, cmetadata []byte) (*madmin.BucketTargets, error) {
+ var (
+ data []byte
+ err error
+ t madmin.BucketTargets
+ meta map[string]string
+ )
+ if len(cdata) == 0 {
+ return nil, nil
+ }
+ data = cdata
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ if len(cmetadata) != 0 {
+ if err := json.Unmarshal(cmetadata, &meta); err != nil {
+ return nil, err
+ }
+ if crypto.S3.IsEncrypted(meta) {
+ if data, err = decryptBucketMetadata(cdata, bucket, meta, kms.Context{
+ bucket: bucket,
+ bucketTargetsFile: bucketTargetsFile,
+ }); err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ if err = json.Unmarshal(data, &t); err != nil {
+ return nil, err
+ }
+ return &t, nil
+}
+
+// TargetClient is the struct for remote target client.
+type TargetClient struct {
+ *minio.Client
+ healthCheckDuration time.Duration
+ Bucket string // remote bucket target
+ replicateSync bool
+ StorageClass string // storage class on remote
+ disableProxy bool
+ ARN string // ARN to uniquely identify remote target
+ ResetID string
+ Endpoint string
+ Secure bool
+}
diff --git a/cmd/bucket-versioning-handler.go b/cmd/bucket-versioning-handler.go
new file mode 100644
index 00000000..92b2c146
--- /dev/null
+++ b/cmd/bucket-versioning-handler.go
@@ -0,0 +1,162 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "encoding/base64"
+ "encoding/xml"
+ "io"
+ "net/http"
+
+ humanize "github.com/dustin/go-humanize"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/bucket/versioning"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/mux"
+ "github.com/minio/pkg/v3/policy"
+)
+
+const (
+ bucketVersioningConfig = "versioning.xml"
+
+ // Maximum size of bucket versioning configuration payload sent to the PutBucketVersioningHandler.
+ maxBucketVersioningConfigSize = 1 * humanize.MiByte
+)
+
+// PutBucketVersioningHandler - PUT Bucket Versioning.
+// ----------
+func (api objectAPIHandlers) PutBucketVersioningHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "PutBucketVersioning")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketVersioningAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ v, err := versioning.ParseConfig(io.LimitReader(r.Body, maxBucketVersioningConfigSize))
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if globalSiteReplicationSys.isEnabled() && !v.Enabled() {
+ writeErrorResponse(ctx, w, APIError{
+ Code: "InvalidBucketState",
+ Description: "Cluster replication is enabled on this site, versioning cannot be suspended on bucket.",
+ HTTPStatusCode: http.StatusBadRequest,
+ }, r.URL)
+ return
+ }
+
+ if rcfg, _ := globalBucketObjectLockSys.Get(bucket); rcfg.LockEnabled && (v.Suspended() || v.PrefixesExcluded()) {
+ writeErrorResponse(ctx, w, APIError{
+ Code: "InvalidBucketState",
+ Description: "An Object Lock configuration is present on this bucket, versioning cannot be suspended.",
+ HTTPStatusCode: http.StatusBadRequest,
+ }, r.URL)
+ return
+ }
+ if rc, _ := getReplicationConfig(ctx, bucket); rc != nil && v.Suspended() {
+ writeErrorResponse(ctx, w, APIError{
+ Code: "InvalidBucketState",
+ Description: "A replication configuration is present on this bucket, bucket wide versioning cannot be suspended.",
+ HTTPStatusCode: http.StatusBadRequest,
+ }, r.URL)
+ return
+ }
+
+ configData, err := xml.Marshal(v)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketVersioningConfig, configData)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Call site replication hook.
+ //
+ // We encode the xml bytes as base64 to ensure there are no encoding
+ // errors.
+ cfgStr := base64.StdEncoding.EncodeToString(configData)
+ replLogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
+ Type: madmin.SRBucketMetaTypeVersionConfig,
+ Bucket: bucket,
+ Versioning: &cfgStr,
+ UpdatedAt: updatedAt,
+ }))
+
+ writeSuccessResponseHeadersOnly(w)
+}
+
+// GetBucketVersioningHandler - GET Bucket Versioning.
+// ----------
+func (api objectAPIHandlers) GetBucketVersioningHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetBucketVersioning")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketVersioningAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Check if bucket exists.
+ if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ config, err := globalBucketVersioningSys.Get(bucket)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ configData, err := xml.Marshal(config)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Write bucket versioning configuration to client
+ writeSuccessResponseXML(w, configData)
+}
diff --git a/cmd/bucket-versioning.go b/cmd/bucket-versioning.go
new file mode 100644
index 00000000..5bfd44a4
--- /dev/null
+++ b/cmd/bucket-versioning.go
@@ -0,0 +1,83 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "strings"
+
+ "github.com/minio/minio/internal/bucket/versioning"
+ "github.com/minio/minio/internal/logger"
+)
+
+// BucketVersioningSys - policy subsystem.
+type BucketVersioningSys struct{}
+
+// Enabled enabled versioning?
+func (sys *BucketVersioningSys) Enabled(bucket string) bool {
+ vc, err := sys.Get(bucket)
+ if err != nil {
+ logger.CriticalIf(GlobalContext, err)
+ }
+ return vc.Enabled()
+}
+
+// PrefixEnabled returns true is versioning is enabled at bucket level and if
+// the given prefix doesn't match any excluded prefixes pattern. This is
+// part of a MinIO versioning configuration extension.
+func (sys *BucketVersioningSys) PrefixEnabled(bucket, prefix string) bool {
+ vc, err := sys.Get(bucket)
+ if err != nil {
+ logger.CriticalIf(GlobalContext, err)
+ }
+ return vc.PrefixEnabled(prefix)
+}
+
+// Suspended suspended versioning?
+func (sys *BucketVersioningSys) Suspended(bucket string) bool {
+ vc, err := sys.Get(bucket)
+ if err != nil {
+ logger.CriticalIf(GlobalContext, err)
+ }
+ return vc.Suspended()
+}
+
+// PrefixSuspended returns true if the given prefix matches an excluded prefix
+// pattern. This is part of a MinIO versioning configuration extension.
+func (sys *BucketVersioningSys) PrefixSuspended(bucket, prefix string) bool {
+ vc, err := sys.Get(bucket)
+ if err != nil {
+ logger.CriticalIf(GlobalContext, err)
+ }
+
+ return vc.PrefixSuspended(prefix)
+}
+
+// Get returns stored bucket policy
+func (sys *BucketVersioningSys) Get(bucket string) (*versioning.Versioning, error) {
+ if bucket == minioMetaBucket || strings.HasPrefix(bucket, minioMetaBucket) {
+ return &versioning.Versioning{XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/"}, nil
+ }
+
+ vcfg, _, err := globalBucketMetadataSys.GetVersioningConfig(bucket)
+ return vcfg, err
+}
+
+// NewBucketVersioningSys - creates new versioning system.
+func NewBucketVersioningSys() *BucketVersioningSys {
+ return &BucketVersioningSys{}
+}
diff --git a/cmd/build-constants.go b/cmd/build-constants.go
new file mode 100644
index 00000000..7f46baff
--- /dev/null
+++ b/cmd/build-constants.go
@@ -0,0 +1,69 @@
+// Copyright (c) 2015-2022 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import "runtime"
+
+// DO NOT EDIT THIS FILE DIRECTLY. These are build-time constants
+// set through ‘buildscripts/gen-ldflags.go’.
+var (
+ // GOPATH - GOPATH value at the time of build.
+ GOPATH = ""
+
+ // GOROOT - GOROOT value at the time of build.
+ GOROOT = ""
+
+ // Version - version time.RFC3339.
+ Version = "DEVELOPMENT.GOGET"
+
+ // ReleaseTag - release tag in TAG.%Y-%m-%dT%H-%M-%SZ.
+ ReleaseTag = "DEVELOPMENT.GOGET"
+
+ // CommitID - latest commit id.
+ CommitID = "DEVELOPMENT.GOGET"
+
+ // ShortCommitID - first 12 characters from CommitID.
+ ShortCommitID = "DEVELOPMENT.GOGET"
+
+ // CopyrightYear - dynamic value of the copyright end year
+ CopyrightYear = "0000"
+
+ // MinioReleaseTagTimeLayout - release tag time layout.
+ MinioReleaseTagTimeLayout = "2006-01-02T15-04-05Z"
+
+ // MinioOSARCH - OS and ARCH.
+ minioOSARCH = runtime.GOOS + "-" + runtime.GOARCH
+
+ // MinioReleaseBaseURL - release url without os and arch.
+ MinioReleaseBaseURL = "https://dl.min.io/server/minio/release/"
+
+ // MinioReleaseURL - release URL.
+ MinioReleaseURL = MinioReleaseBaseURL + minioOSARCH + SlashSeparator
+
+ // MinioStoreName - MinIO store name.
+ MinioStoreName = "MinIO"
+
+ // MinioUAName - MinIO user agent name.
+ MinioUAName = "MinIO"
+
+ // MinioBannerName - MinIO banner name for startup message.
+ MinioBannerName = "MinIO Object Storage Server"
+
+ // MinioLicense - MinIO server license.
+ MinioLicense = "GNU AGPLv3 - https://www.gnu.org/licenses/agpl-3.0.html"
+)
diff --git a/cmd/callhome.go b/cmd/callhome.go
new file mode 100644
index 00000000..2a6d6695
--- /dev/null
+++ b/cmd/callhome.go
@@ -0,0 +1,199 @@
+// Copyright (c) 2015-2022 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "compress/gzip"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "math/rand"
+ "net/url"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+)
+
+var callhomeLeaderLockTimeout = newDynamicTimeout(30*time.Second, 10*time.Second)
+
+// initCallhome will start the callhome task in the background.
+func initCallhome(ctx context.Context, objAPI ObjectLayer) {
+ if !globalCallhomeConfig.Enabled() {
+ return
+ }
+
+ go func() {
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+ // Leader node (that successfully acquires the lock inside runCallhome)
+ // will keep performing the callhome. If the leader goes down for some reason,
+ // the lock will be released and another node will acquire it and take over
+ // because of this loop.
+ for {
+ if !globalCallhomeConfig.Enabled() {
+ return
+ }
+
+ if !runCallhome(ctx, objAPI) {
+ // callhome was disabled or context was canceled
+ return
+ }
+
+ // callhome running on a different node.
+ // sleep for some time and try again.
+ duration := max(time.Duration(r.Float64()*float64(globalCallhomeConfig.FrequencyDur())),
+ // Make sure to sleep at least a second to avoid high CPU ticks.
+ time.Second)
+ time.Sleep(duration)
+ }
+ }()
+}
+
+func runCallhome(ctx context.Context, objAPI ObjectLayer) bool {
+ // Make sure only 1 callhome is running on the cluster.
+ locker := objAPI.NewNSLock(minioMetaBucket, "callhome/runCallhome.lock")
+ lkctx, err := locker.GetLock(ctx, callhomeLeaderLockTimeout)
+ if err != nil {
+ // lock timedout means some other node is the leader,
+ // cycle back return 'true'
+ return true
+ }
+
+ ctx = lkctx.Context()
+ defer locker.Unlock(lkctx)
+
+ // Perform callhome once and then keep running it at regular intervals.
+ performCallhome(ctx)
+
+ callhomeTimer := time.NewTimer(globalCallhomeConfig.FrequencyDur())
+ defer callhomeTimer.Stop()
+
+ for {
+ if !globalCallhomeConfig.Enabled() {
+ // Stop the processing as callhome got disabled
+ return false
+ }
+
+ select {
+ case <-ctx.Done():
+ // indicates that we do not need to run callhome anymore
+ return false
+ case <-callhomeTimer.C:
+ if !globalCallhomeConfig.Enabled() {
+ // Stop the processing as callhome got disabled
+ return false
+ }
+
+ performCallhome(ctx)
+
+ // Reset the timer for next cycle.
+ callhomeTimer.Reset(globalCallhomeConfig.FrequencyDur())
+ }
+ }
+}
+
+func performCallhome(ctx context.Context) {
+ deadline := 10 * time.Second // Default deadline is 10secs for callhome
+ objectAPI := newObjectLayerFn()
+ if objectAPI == nil {
+ internalLogIf(ctx, errors.New("Callhome: object layer not ready"))
+ return
+ }
+
+ healthCtx, healthCancel := context.WithTimeout(ctx, deadline)
+ defer healthCancel()
+
+ healthInfoCh := make(chan madmin.HealthInfo)
+
+ query := url.Values{}
+ for _, k := range madmin.HealthDataTypesList {
+ query.Set(string(k), "true")
+ }
+
+ healthInfo := madmin.HealthInfo{
+ TimeStamp: time.Now().UTC(),
+ Version: madmin.HealthInfoVersion,
+ Minio: madmin.MinioHealthInfo{
+ Info: madmin.MinioInfo{
+ DeploymentID: globalDeploymentID(),
+ },
+ },
+ }
+
+ go fetchHealthInfo(healthCtx, objectAPI, &query, healthInfoCh, healthInfo)
+
+ for {
+ select {
+ case hi, hasMore := <-healthInfoCh:
+ if !hasMore {
+ auditOptions := AuditLogOptions{Event: "callhome:diagnostics"}
+ // Received all data. Send to SUBNET and return
+ err := sendHealthInfo(ctx, healthInfo)
+ if err != nil {
+ internalLogIf(ctx, fmt.Errorf("Unable to perform callhome: %w", err))
+ auditOptions.Error = err.Error()
+ }
+ auditLogInternal(ctx, auditOptions)
+ return
+ }
+ healthInfo = hi
+ case <-healthCtx.Done():
+ return
+ }
+ }
+}
+
+const (
+ subnetHealthPath = "/api/health/upload"
+)
+
+func sendHealthInfo(ctx context.Context, healthInfo madmin.HealthInfo) error {
+ url := globalSubnetConfig.BaseURL + subnetHealthPath
+
+ filename := fmt.Sprintf("health_%s.json.gz", UTCNow().Format("20060102150405"))
+ url += "?filename=" + filename
+
+ _, err := globalSubnetConfig.Upload(url, filename, createHealthJSONGzip(ctx, healthInfo))
+ return err
+}
+
+func createHealthJSONGzip(ctx context.Context, healthInfo madmin.HealthInfo) []byte {
+ var b bytes.Buffer
+ gzWriter := gzip.NewWriter(&b)
+
+ header := struct {
+ Version string `json:"version"`
+ }{Version: healthInfo.Version}
+
+ enc := json.NewEncoder(gzWriter)
+ if e := enc.Encode(header); e != nil {
+ internalLogIf(ctx, fmt.Errorf("Could not encode health info header: %w", e))
+ return nil
+ }
+
+ if e := enc.Encode(healthInfo); e != nil {
+ internalLogIf(ctx, fmt.Errorf("Could not encode health info: %w", e))
+ return nil
+ }
+
+ gzWriter.Flush()
+ gzWriter.Close()
+
+ return b.Bytes()
+}
diff --git a/cmd/common-main.go b/cmd/common-main.go
new file mode 100644
index 00000000..e57e190f
--- /dev/null
+++ b/cmd/common-main.go
@@ -0,0 +1,1064 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bufio"
+ "bytes"
+ "context"
+ "crypto/x509"
+ "encoding/gob"
+ "errors"
+ "fmt"
+ "net"
+ "net/url"
+ "os"
+ "path"
+ "path/filepath"
+ "runtime"
+ "sort"
+ "strconv"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/dustin/go-humanize"
+ fcolor "github.com/fatih/color"
+ "github.com/go-openapi/loads"
+ "github.com/inconshreveable/mousetrap"
+ dns2 "github.com/miekg/dns"
+ "github.com/minio/cli"
+ consoleapi "github.com/minio/console/api"
+ "github.com/minio/console/api/operations"
+ consoleoauth2 "github.com/minio/console/pkg/auth/idp/oauth2"
+ consoleCerts "github.com/minio/console/pkg/certs"
+ "github.com/minio/kms-go/kes"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio-go/v7"
+ "github.com/minio/minio-go/v7/pkg/set"
+ "github.com/minio/minio/internal/auth"
+ "github.com/minio/minio/internal/color"
+ "github.com/minio/minio/internal/config"
+ "github.com/minio/minio/internal/kms"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/certs"
+ "github.com/minio/pkg/v3/console"
+ "github.com/minio/pkg/v3/env"
+ xnet "github.com/minio/pkg/v3/net"
+ "golang.org/x/term"
+)
+
+// serverDebugLog will enable debug printing
+var (
+ serverDebugLog = env.Get("_MINIO_SERVER_DEBUG", config.EnableOff) == config.EnableOn
+ currentReleaseTime time.Time
+ orchestrated = IsKubernetes() || IsDocker()
+)
+
+func init() {
+ if !term.IsTerminal(int(os.Stdout.Fd())) || !term.IsTerminal(int(os.Stderr.Fd())) {
+ color.TurnOff()
+ }
+ if env.Get("NO_COLOR", "") != "" || env.Get("TERM", "") == "dumb" {
+ color.TurnOff()
+ }
+
+ if runtime.GOOS == "windows" {
+ if mousetrap.StartedByExplorer() {
+ fmt.Printf("Don't double-click %s\n", os.Args[0])
+ fmt.Println("You need to open cmd.exe/PowerShell and run it from the command line")
+ fmt.Println("Refer to the docs here on how to run it as a Windows Service https://github.com/minio/minio-service/tree/master/windows")
+ fmt.Println("Press the Enter Key to Exit")
+ fmt.Scanln()
+ os.Exit(1)
+ }
+ }
+
+ logger.Init(GOPATH, GOROOT)
+ logger.RegisterError(config.FmtError)
+
+ t, _ := minioVersionToReleaseTime(Version)
+ if !t.IsZero() {
+ globalVersionUnix = uint64(t.Unix())
+ }
+
+ globalIsCICD = env.Get("MINIO_CI_CD", "") != "" || env.Get("CI", "") != ""
+
+ console.SetColor("Debug", fcolor.New())
+
+ gob.Register(StorageErr(""))
+ gob.Register(madmin.TimeInfo{})
+ gob.Register(madmin.XFSErrorConfigs{})
+ gob.Register(map[string]string{})
+ gob.Register(map[string]any{})
+
+ // All minio-go and madmin-go API operations shall be performed only once,
+ // another way to look at this is we are turning off retries.
+ minio.MaxRetry = 1
+ madmin.MaxRetry = 1
+
+ currentReleaseTime, _ = GetCurrentReleaseTime()
+}
+
+const consolePrefix = "CONSOLE_"
+
+func minioConfigToConsoleFeatures() {
+ os.Setenv("CONSOLE_PBKDF_SALT", globalDeploymentID())
+ os.Setenv("CONSOLE_PBKDF_PASSPHRASE", globalDeploymentID())
+ if globalMinioEndpoint != "" {
+ os.Setenv("CONSOLE_MINIO_SERVER", globalMinioEndpoint)
+ } else {
+ // Explicitly set 127.0.0.1 so Console will automatically bypass TLS verification to the local S3 API.
+ // This will save users from providing a certificate with IP or FQDN SAN that points to the local host.
+ os.Setenv("CONSOLE_MINIO_SERVER", fmt.Sprintf("%s://127.0.0.1:%s", getURLScheme(globalIsTLS), globalMinioPort))
+ }
+ if value := env.Get(config.EnvMinIOLogQueryURL, ""); value != "" {
+ os.Setenv("CONSOLE_LOG_QUERY_URL", value)
+ if value := env.Get(config.EnvMinIOLogQueryAuthToken, ""); value != "" {
+ os.Setenv("CONSOLE_LOG_QUERY_AUTH_TOKEN", value)
+ }
+ }
+ if value := env.Get(config.EnvBrowserRedirectURL, ""); value != "" {
+ os.Setenv("CONSOLE_BROWSER_REDIRECT_URL", value)
+ }
+ if value := env.Get(config.EnvConsoleDebugLogLevel, ""); value != "" {
+ os.Setenv("CONSOLE_DEBUG_LOGLEVEL", value)
+ }
+ // pass the console subpath configuration
+ if globalBrowserRedirectURL != nil {
+ subPath := path.Clean(pathJoin(strings.TrimSpace(globalBrowserRedirectURL.Path), SlashSeparator))
+ if subPath != SlashSeparator {
+ os.Setenv("CONSOLE_SUBPATH", subPath)
+ }
+ }
+ // Enable if prometheus URL is set.
+ if value := env.Get(config.EnvMinIOPrometheusURL, ""); value != "" {
+ os.Setenv("CONSOLE_PROMETHEUS_URL", value)
+ if value := env.Get(config.EnvMinIOPrometheusJobID, "minio-job"); value != "" {
+ os.Setenv("CONSOLE_PROMETHEUS_JOB_ID", value)
+ // Support additional labels for more granular filtering.
+ if value := env.Get(config.EnvMinIOPrometheusExtraLabels, ""); value != "" {
+ os.Setenv("CONSOLE_PROMETHEUS_EXTRA_LABELS", value)
+ }
+ }
+ // Support Prometheus Auth Token
+ if value := env.Get(config.EnvMinIOPrometheusAuthToken, ""); value != "" {
+ os.Setenv("CONSOLE_PROMETHEUS_AUTH_TOKEN", value)
+ }
+ }
+ // Enable if LDAP is enabled.
+ if globalIAMSys.LDAPConfig.Enabled() {
+ os.Setenv("CONSOLE_LDAP_ENABLED", config.EnableOn)
+ }
+ // Handle animation in welcome page
+ if value := env.Get(config.EnvBrowserLoginAnimation, "on"); value != "" {
+ os.Setenv("CONSOLE_ANIMATED_LOGIN", value)
+ }
+
+ // Pass on the session duration environment variable, else we will default to 12 hours
+ if valueSts := env.Get(config.EnvMinioStsDuration, ""); valueSts != "" {
+ os.Setenv("CONSOLE_STS_DURATION", valueSts)
+ } else if valueSession := env.Get(config.EnvBrowserSessionDuration, ""); valueSession != "" {
+ os.Setenv("CONSOLE_STS_DURATION", valueSession)
+ }
+
+ os.Setenv("CONSOLE_MINIO_SITE_NAME", globalSite.Name())
+ os.Setenv("CONSOLE_MINIO_SITE_REGION", globalSite.Region())
+ os.Setenv("CONSOLE_MINIO_REGION", globalSite.Region())
+
+ os.Setenv("CONSOLE_CERT_PASSWD", env.Get("MINIO_CERT_PASSWD", ""))
+
+ // This section sets Browser (console) stored config
+ if valueSCP := globalBrowserConfig.GetCSPolicy(); valueSCP != "" {
+ os.Setenv("CONSOLE_SECURE_CONTENT_SECURITY_POLICY", valueSCP)
+ }
+
+ if hstsSeconds := globalBrowserConfig.GetHSTSSeconds(); hstsSeconds > 0 {
+ isubdom := globalBrowserConfig.IsHSTSIncludeSubdomains()
+ isprel := globalBrowserConfig.IsHSTSPreload()
+ os.Setenv("CONSOLE_SECURE_STS_SECONDS", strconv.Itoa(hstsSeconds))
+ os.Setenv("CONSOLE_SECURE_STS_INCLUDE_SUB_DOMAINS", isubdom)
+ os.Setenv("CONSOLE_SECURE_STS_PRELOAD", isprel)
+ }
+
+ if valueRefer := globalBrowserConfig.GetReferPolicy(); valueRefer != "" {
+ os.Setenv("CONSOLE_SECURE_REFERRER_POLICY", valueRefer)
+ }
+
+ globalSubnetConfig.ApplyEnv()
+}
+
+func buildOpenIDConsoleConfig() consoleoauth2.OpenIDPCfg {
+ pcfgs := globalIAMSys.OpenIDConfig.ProviderCfgs
+ m := make(map[string]consoleoauth2.ProviderConfig, len(pcfgs))
+ for name, cfg := range pcfgs {
+ callback := getConsoleEndpoints()[0] + "/oauth_callback"
+ if cfg.RedirectURI != "" {
+ callback = cfg.RedirectURI
+ }
+ m[name] = consoleoauth2.ProviderConfig{
+ URL: cfg.URL.String(),
+ DisplayName: cfg.DisplayName,
+ ClientID: cfg.ClientID,
+ ClientSecret: cfg.ClientSecret,
+ HMACSalt: globalDeploymentID(),
+ HMACPassphrase: cfg.ClientID,
+ Scopes: strings.Join(cfg.DiscoveryDoc.ScopesSupported, ","),
+ Userinfo: cfg.ClaimUserinfo,
+ RedirectCallbackDynamic: cfg.RedirectURIDynamic,
+ RedirectCallback: callback,
+ EndSessionEndpoint: cfg.DiscoveryDoc.EndSessionEndpoint,
+ RoleArn: cfg.GetRoleArn(),
+ }
+ }
+ return m
+}
+
+func initConsoleServer() (*consoleapi.Server, error) {
+ // unset all console_ environment variables.
+ for _, cenv := range env.List(consolePrefix) {
+ os.Unsetenv(cenv)
+ }
+
+ // enable all console environment variables
+ minioConfigToConsoleFeatures()
+
+ // set certs dir to minio directory
+ consoleCerts.GlobalCertsDir = &consoleCerts.ConfigDir{
+ Path: globalCertsDir.Get(),
+ }
+ consoleCerts.GlobalCertsCADir = &consoleCerts.ConfigDir{
+ Path: globalCertsCADir.Get(),
+ }
+
+ // set certs before other console initialization
+ consoleapi.GlobalRootCAs, consoleapi.GlobalPublicCerts, consoleapi.GlobalTLSCertsManager = globalRootCAs, globalPublicCerts, globalTLSCerts
+
+ swaggerSpec, err := loads.Embedded(consoleapi.SwaggerJSON, consoleapi.FlatSwaggerJSON)
+ if err != nil {
+ return nil, err
+ }
+
+ api := operations.NewConsoleAPI(swaggerSpec)
+
+ if !serverDebugLog {
+ // Disable console logging if server debug log is not enabled
+ noLog := func(string, ...any) {}
+
+ consoleapi.LogInfo = noLog
+ consoleapi.LogError = noLog
+ api.Logger = noLog
+ }
+
+ // Pass in console application config. This needs to happen before the
+ // ConfigureAPI() call.
+ consoleapi.GlobalMinIOConfig = consoleapi.MinIOConfig{
+ OpenIDProviders: buildOpenIDConsoleConfig(),
+ }
+
+ server := consoleapi.NewServer(api)
+ // register all APIs
+ server.ConfigureAPI()
+
+ consolePort, _ := strconv.Atoi(globalMinioConsolePort)
+
+ server.Host = globalMinioConsoleHost
+ server.Port = consolePort
+ consoleapi.Port = globalMinioConsolePort
+ consoleapi.Hostname = globalMinioConsoleHost
+
+ if globalIsTLS {
+ // If TLS certificates are provided enforce the HTTPS.
+ server.EnabledListeners = []string{"https"}
+ server.TLSPort = consolePort
+ // Need to store tls-port, tls-host un config variables so secure.middleware can read from there
+ consoleapi.TLSPort = globalMinioConsolePort
+ consoleapi.Hostname = globalMinioConsoleHost
+ }
+
+ return server, nil
+}
+
+// Check for updates and print a notification message
+func checkUpdate(mode string) {
+ updateURL := minioReleaseInfoURL
+ if runtime.GOOS == globalWindowsOSName {
+ updateURL = minioReleaseWindowsInfoURL
+ }
+
+ u, err := url.Parse(updateURL)
+ if err != nil {
+ return
+ }
+
+ if currentReleaseTime.IsZero() {
+ return
+ }
+
+ _, lrTime, err := getLatestReleaseTime(u, 2*time.Second, mode)
+ if err != nil {
+ return
+ }
+
+ var older time.Duration
+ var downloadURL string
+ if lrTime.After(currentReleaseTime) {
+ older = lrTime.Sub(currentReleaseTime)
+ downloadURL = getDownloadURL(releaseTimeToReleaseTag(lrTime))
+ }
+
+ updateMsg := prepareUpdateMessage(downloadURL, older)
+ if updateMsg == "" {
+ return
+ }
+
+ logger.Info(prepareUpdateMessage("Run `mc admin update ALIAS`", lrTime.Sub(currentReleaseTime)))
+}
+
+func newConfigDir(dir string, dirSet bool, getDefaultDir func() string) (*ConfigDir, error) {
+ if dir == "" {
+ dir = getDefaultDir()
+ }
+
+ if dir == "" {
+ if !dirSet {
+ return nil, fmt.Errorf("missing option must be provided")
+ }
+ return nil, fmt.Errorf("provided option cannot be empty")
+ }
+
+ // Disallow relative paths, figure out absolute paths.
+ dirAbs, err := filepath.Abs(dir)
+ if err != nil {
+ return nil, err
+ }
+ err = mkdirAllIgnorePerm(dirAbs)
+ if err != nil {
+ return nil, fmt.Errorf("unable to create the directory `%s`: %w", dirAbs, err)
+ }
+
+ return &ConfigDir{path: dirAbs}, nil
+}
+
+func buildServerCtxt(ctx *cli.Context, ctxt *serverCtxt) (err error) {
+ // Get "json" flag from command line argument and
+ ctxt.JSON = ctx.IsSet("json") || ctx.GlobalIsSet("json")
+ // Get quiet flag from command line argument.
+ ctxt.Quiet = ctx.IsSet("quiet") || ctx.GlobalIsSet("quiet")
+ // Get anonymous flag from command line argument.
+ ctxt.Anonymous = ctx.IsSet("anonymous") || ctx.GlobalIsSet("anonymous")
+ // Fetch address option
+ ctxt.Addr = ctx.GlobalString("address")
+ if ctxt.Addr == "" || ctxt.Addr == ":"+GlobalMinioDefaultPort {
+ ctxt.Addr = ctx.String("address")
+ }
+
+ // Fetch console address option
+ ctxt.ConsoleAddr = ctx.GlobalString("console-address")
+ if ctxt.ConsoleAddr == "" {
+ ctxt.ConsoleAddr = ctx.String("console-address")
+ }
+
+ if cxml := ctx.String("crossdomain-xml"); cxml != "" {
+ buf, err := os.ReadFile(cxml)
+ if err != nil {
+ return err
+ }
+ ctxt.CrossDomainXML = string(buf)
+ }
+
+ // Check "no-compat" flag from command line argument.
+ ctxt.StrictS3Compat = !ctx.IsSet("no-compat") && !ctx.GlobalIsSet("no-compat")
+
+ switch {
+ case ctx.IsSet("config-dir"):
+ ctxt.ConfigDir = ctx.String("config-dir")
+ ctxt.configDirSet = true
+ case ctx.GlobalIsSet("config-dir"):
+ ctxt.ConfigDir = ctx.GlobalString("config-dir")
+ ctxt.configDirSet = true
+ }
+
+ switch {
+ case ctx.IsSet("certs-dir"):
+ ctxt.CertsDir = ctx.String("certs-dir")
+ ctxt.certsDirSet = true
+ case ctx.GlobalIsSet("certs-dir"):
+ ctxt.CertsDir = ctx.GlobalString("certs-dir")
+ ctxt.certsDirSet = true
+ }
+
+ memAvailable := availableMemory()
+ if ctx.IsSet("memlimit") || ctx.GlobalIsSet("memlimit") {
+ memlimit := ctx.String("memlimit")
+ if memlimit == "" {
+ memlimit = ctx.GlobalString("memlimit")
+ }
+ mlimit, err := humanize.ParseBytes(memlimit)
+ if err != nil {
+ return err
+ }
+ if mlimit > memAvailable {
+ logger.Info("WARNING: maximum memory available (%s) smaller than specified --memlimit=%s, ignoring --memlimit value",
+ humanize.IBytes(memAvailable), memlimit)
+ }
+ ctxt.MemLimit = mlimit
+ } else {
+ ctxt.MemLimit = memAvailable
+ }
+
+ if memAvailable < ctxt.MemLimit {
+ ctxt.MemLimit = memAvailable
+ }
+
+ ctxt.FTP = ctx.StringSlice("ftp")
+ ctxt.SFTP = ctx.StringSlice("sftp")
+ ctxt.Interface = ctx.String("interface")
+ ctxt.UserTimeout = ctx.Duration("conn-user-timeout")
+ ctxt.SendBufSize = ctx.Int("send-buf-size")
+ ctxt.RecvBufSize = ctx.Int("recv-buf-size")
+ ctxt.IdleTimeout = ctx.Duration("idle-timeout")
+ ctxt.UserTimeout = ctx.Duration("conn-user-timeout")
+
+ if conf := ctx.String("config"); len(conf) > 0 {
+ err = mergeServerCtxtFromConfigFile(conf, ctxt)
+ } else {
+ err = mergeDisksLayoutFromArgs(serverCmdArgs(ctx), ctxt)
+ }
+
+ return err
+}
+
+func handleCommonArgs(ctxt serverCtxt) {
+ if ctxt.JSON {
+ logger.EnableJSON()
+ }
+ if ctxt.Quiet {
+ logger.EnableQuiet()
+ }
+ if ctxt.Anonymous {
+ logger.EnableAnonymous()
+ }
+
+ consoleAddr := ctxt.ConsoleAddr
+ addr := ctxt.Addr
+ configDir := ctxt.ConfigDir
+ configSet := ctxt.configDirSet
+ certsDir := ctxt.CertsDir
+ certsSet := ctxt.certsDirSet
+
+ if globalBrowserEnabled {
+ if consoleAddr == "" {
+ p, err := xnet.GetFreePort()
+ if err != nil {
+ logger.FatalIf(err, "Unable to get free port for Console UI on the host")
+ }
+ // hold the port
+ l, err := net.Listen("TCP", fmt.Sprintf(":%s", p.String()))
+ if err == nil {
+ defer l.Close()
+ }
+ consoleAddr = net.JoinHostPort("", p.String())
+ }
+
+ if _, _, err := net.SplitHostPort(consoleAddr); err != nil {
+ logger.FatalIf(err, "Unable to start listening on console port")
+ }
+
+ if consoleAddr == addr {
+ logger.FatalIf(errors.New("--console-address cannot be same as --address"), "Unable to start the server")
+ }
+ }
+
+ globalMinioHost, globalMinioPort = mustSplitHostPort(addr)
+ if globalMinioPort == "0" {
+ p, err := xnet.GetFreePort()
+ if err != nil {
+ logger.FatalIf(err, "Unable to get free port for S3 API on the host")
+ }
+ globalMinioPort = p.String()
+ globalDynamicAPIPort = true
+ }
+
+ if globalBrowserEnabled {
+ globalMinioConsoleHost, globalMinioConsolePort = mustSplitHostPort(consoleAddr)
+ }
+
+ if globalMinioPort == globalMinioConsolePort {
+ logger.FatalIf(errors.New("--console-address port cannot be same as --address port"), "Unable to start the server")
+ }
+
+ globalMinioAddr = addr
+
+ // Set all config, certs and CAs directories.
+ var err error
+ globalConfigDir, err = newConfigDir(configDir, configSet, defaultConfigDir.Get)
+ logger.FatalIf(err, "Unable to initialize the (deprecated) config directory")
+ globalCertsDir, err = newConfigDir(certsDir, certsSet, defaultCertsDir.Get)
+ logger.FatalIf(err, "Unable to initialize the certs directory")
+
+ // Remove this code when we deprecate and remove config-dir.
+ // This code is to make sure we inherit from the config-dir
+ // option if certs-dir is not provided.
+ if !certsSet && configSet {
+ globalCertsDir = &ConfigDir{path: filepath.Join(globalConfigDir.Get(), certsDir)}
+ }
+
+ globalCertsCADir = &ConfigDir{path: filepath.Join(globalCertsDir.Get(), certsCADir)}
+
+ logger.FatalIf(mkdirAllIgnorePerm(globalCertsCADir.Get()), "Unable to create certs CA directory at %s", globalCertsCADir.Get())
+}
+
+func runDNSCache(ctx *cli.Context) {
+ dnsTTL := ctx.Duration("dns-cache-ttl")
+ // Check if we have configured a custom DNS cache TTL.
+ if dnsTTL <= 0 {
+ if orchestrated {
+ dnsTTL = 30 * time.Second
+ } else {
+ dnsTTL = 10 * time.Minute
+ }
+ }
+
+ // Call to refresh will refresh names in cache.
+ go func() {
+ // Baremetal setups set DNS refresh window up to dnsTTL duration.
+ t := time.NewTicker(dnsTTL)
+ defer t.Stop()
+ for {
+ select {
+ case <-t.C:
+ globalDNSCache.Refresh()
+
+ case <-GlobalContext.Done():
+ return
+ }
+ }
+ }()
+}
+
+type envKV struct {
+ Key string
+ Value string
+ Skip bool
+}
+
+func (e envKV) String() string {
+ if e.Skip {
+ return ""
+ }
+ return fmt.Sprintf("%s=%s", e.Key, e.Value)
+}
+
+func parsEnvEntry(envEntry string) (envKV, error) {
+ envEntry = strings.TrimSpace(envEntry)
+ if envEntry == "" {
+ // Skip all empty lines
+ return envKV{
+ Skip: true,
+ }, nil
+ }
+ if strings.HasPrefix(envEntry, "#") {
+ // Skip commented lines
+ return envKV{
+ Skip: true,
+ }, nil
+ }
+ envTokens := strings.SplitN(strings.TrimSpace(strings.TrimPrefix(envEntry, "export")), config.EnvSeparator, 2)
+ if len(envTokens) != 2 {
+ return envKV{}, fmt.Errorf("envEntry malformed; %s, expected to be of form 'KEY=value'", envEntry)
+ }
+
+ key := envTokens[0]
+ val := envTokens[1]
+
+ // Remove quotes from the value if found
+ if len(val) >= 2 {
+ quote := val[0]
+ if (quote == '"' || quote == '\'') && val[len(val)-1] == quote {
+ val = val[1 : len(val)-1]
+ }
+ }
+
+ return envKV{
+ Key: key,
+ Value: val,
+ }, nil
+}
+
+// Similar to os.Environ returns a copy of strings representing
+// the environment values from a file, in the form "key, value".
+// in a structured form.
+func minioEnvironFromFile(envConfigFile string) ([]envKV, error) {
+ f, err := Open(envConfigFile)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ var ekvs []envKV
+ scanner := bufio.NewScanner(f)
+ for scanner.Scan() {
+ ekv, err := parsEnvEntry(scanner.Text())
+ if err != nil {
+ return nil, err
+ }
+ if ekv.Skip {
+ // Skips empty lines
+ continue
+ }
+ ekvs = append(ekvs, ekv)
+ }
+ if err = scanner.Err(); err != nil {
+ return nil, err
+ }
+ return ekvs, nil
+}
+
+func readFromSecret(sp string) (string, error) {
+ // Supports reading path from docker secrets, filename is
+ // relative to /run/secrets/ position.
+ if isFile(pathJoin("/run/secrets/", sp)) {
+ sp = pathJoin("/run/secrets/", sp)
+ }
+ credBuf, err := os.ReadFile(sp)
+ if err != nil {
+ if os.IsNotExist(err) { // ignore if file doesn't exist.
+ return "", nil
+ }
+ return "", err
+ }
+ return string(bytes.TrimSpace(credBuf)), nil
+}
+
+func loadEnvVarsFromFiles() {
+ if env.IsSet(config.EnvAccessKeyFile) {
+ accessKey, err := readFromSecret(env.Get(config.EnvAccessKeyFile, ""))
+ if err != nil {
+ logger.Fatal(config.ErrInvalidCredentials(err),
+ "Unable to validate credentials inherited from the secret file(s)")
+ }
+ if accessKey != "" {
+ os.Setenv(config.EnvRootUser, accessKey)
+ }
+ }
+
+ if env.IsSet(config.EnvSecretKeyFile) {
+ secretKey, err := readFromSecret(env.Get(config.EnvSecretKeyFile, ""))
+ if err != nil {
+ logger.Fatal(config.ErrInvalidCredentials(err),
+ "Unable to validate credentials inherited from the secret file(s)")
+ }
+ if secretKey != "" {
+ os.Setenv(config.EnvRootPassword, secretKey)
+ }
+ }
+
+ if env.IsSet(config.EnvRootUserFile) {
+ rootUser, err := readFromSecret(env.Get(config.EnvRootUserFile, ""))
+ if err != nil {
+ logger.Fatal(config.ErrInvalidCredentials(err),
+ "Unable to validate credentials inherited from the secret file(s)")
+ }
+ if rootUser != "" {
+ os.Setenv(config.EnvRootUser, rootUser)
+ }
+ }
+
+ if env.IsSet(config.EnvRootPasswordFile) {
+ rootPassword, err := readFromSecret(env.Get(config.EnvRootPasswordFile, ""))
+ if err != nil {
+ logger.Fatal(config.ErrInvalidCredentials(err),
+ "Unable to validate credentials inherited from the secret file(s)")
+ }
+ if rootPassword != "" {
+ os.Setenv(config.EnvRootPassword, rootPassword)
+ }
+ }
+
+ if env.IsSet(config.EnvConfigEnvFile) {
+ ekvs, err := minioEnvironFromFile(env.Get(config.EnvConfigEnvFile, ""))
+ if err != nil && !os.IsNotExist(err) {
+ logger.Fatal(err, "Unable to read the config environment file")
+ }
+ for _, ekv := range ekvs {
+ os.Setenv(ekv.Key, ekv.Value)
+ }
+ }
+}
+
+func serverHandleEarlyEnvVars() {
+ var err error
+ globalBrowserEnabled, err = config.ParseBool(env.Get(config.EnvBrowser, config.EnableOn))
+ if err != nil {
+ logger.Fatal(config.ErrInvalidBrowserValue(err), "Invalid MINIO_BROWSER value in environment variable")
+ }
+}
+
+func serverHandleEnvVars() {
+ var err error
+ if globalBrowserEnabled {
+ if redirectURL := env.Get(config.EnvBrowserRedirectURL, ""); redirectURL != "" {
+ u, err := xnet.ParseHTTPURL(redirectURL)
+ if err != nil {
+ logger.Fatal(err, "Invalid MINIO_BROWSER_REDIRECT_URL value in environment variable")
+ }
+ // Look for if URL has invalid values and return error.
+ if !isValidURLEndpoint((*url.URL)(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")
+ }
+ globalBrowserRedirectURL = u
+ }
+ globalBrowserRedirect = env.Get(config.EnvBrowserRedirect, config.EnableOn) == config.EnableOn
+ }
+
+ if serverURL := env.Get(config.EnvMinIOServerURL, ""); serverURL != "" {
+ u, err := xnet.ParseHTTPURL(serverURL)
+ if err != nil {
+ logger.Fatal(err, "Invalid MINIO_SERVER_URL value in environment variable")
+ }
+ // Look for if URL has invalid values and return error.
+ if !isValidURLEndpoint((*url.URL)(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")
+ }
+ u.Path = "" // remove any path component such as `/`
+ globalMinioEndpoint = u.String()
+ globalMinioEndpointURL = u
+ }
+
+ globalFSOSync, err = config.ParseBool(env.Get(config.EnvFSOSync, config.EnableOff))
+ if err != nil {
+ logger.Fatal(config.ErrInvalidFSOSyncValue(err), "Invalid MINIO_FS_OSYNC value in environment variable")
+ }
+
+ rootDiskSize := env.Get(config.EnvRootDriveThresholdSize, "")
+ if rootDiskSize == "" {
+ rootDiskSize = env.Get(config.EnvRootDiskThresholdSize, "")
+ }
+ if rootDiskSize != "" {
+ size, err := humanize.ParseBytes(rootDiskSize)
+ if err != nil {
+ logger.Fatal(err, fmt.Sprintf("Invalid %s value in root drive threshold environment variable", rootDiskSize))
+ }
+ globalRootDiskThreshold = size
+ }
+
+ domains := env.Get(config.EnvDomain, "")
+ if len(domains) != 0 {
+ for domainName := range strings.SplitSeq(domains, config.ValueSeparator) {
+ if _, ok := dns2.IsDomainName(domainName); !ok {
+ logger.Fatal(config.ErrInvalidDomainValue(nil).Msgf("Unknown value `%s`", domainName),
+ "Invalid MINIO_DOMAIN value in environment variable")
+ }
+ globalDomainNames = append(globalDomainNames, domainName)
+ }
+ sort.Strings(globalDomainNames)
+ lcpSuf := lcpSuffix(globalDomainNames)
+ for _, domainName := range globalDomainNames {
+ if domainName == lcpSuf && len(globalDomainNames) > 1 {
+ logger.Fatal(config.ErrOverlappingDomainValue(nil).Msgf("Overlapping domains `%s` not allowed", globalDomainNames),
+ "Invalid MINIO_DOMAIN value in environment variable")
+ }
+ }
+ }
+
+ publicIPs := env.Get(config.EnvPublicIPs, "")
+ if len(publicIPs) != 0 {
+ minioEndpoints := strings.Split(publicIPs, config.ValueSeparator)
+ domainIPs := set.NewStringSet()
+ for _, endpoint := range minioEndpoints {
+ if net.ParseIP(endpoint) == nil {
+ // Checking if the IP is a DNS entry.
+ addrs, err := globalDNSCache.LookupHost(GlobalContext, endpoint)
+ if err != nil {
+ logger.FatalIf(err, "Unable to initialize MinIO server with [%s] invalid entry found in MINIO_PUBLIC_IPS", endpoint)
+ }
+ for _, addr := range addrs {
+ domainIPs.Add(addr)
+ }
+ }
+ domainIPs.Add(endpoint)
+ }
+ updateDomainIPs(domainIPs)
+ } else {
+ // Add found interfaces IP address to global domain IPS,
+ // loopback addresses will be naturally dropped.
+ domainIPs := mustGetLocalIP4()
+ for _, host := range globalEndpoints.Hostnames() {
+ domainIPs.Add(host)
+ }
+ updateDomainIPs(domainIPs)
+ }
+
+ // In place update is true by default if the MINIO_UPDATE is not set
+ // or is not set to 'off', if MINIO_UPDATE is set to 'off' then
+ // in-place update is off.
+ globalInplaceUpdateDisabled = strings.EqualFold(env.Get(config.EnvUpdate, config.EnableOn), config.EnableOff)
+
+ // Check if the supported credential env vars,
+ // "MINIO_ROOT_USER" and "MINIO_ROOT_PASSWORD" are provided
+ // Warn user if deprecated environment variables,
+ // "MINIO_ACCESS_KEY" and "MINIO_SECRET_KEY", are defined
+ // Check all error conditions first
+ //nolint:gocritic
+ if !env.IsSet(config.EnvRootUser) && env.IsSet(config.EnvRootPassword) {
+ logger.Fatal(config.ErrMissingEnvCredentialRootUser(nil), "Unable to start MinIO")
+ } else if env.IsSet(config.EnvRootUser) && !env.IsSet(config.EnvRootPassword) {
+ logger.Fatal(config.ErrMissingEnvCredentialRootPassword(nil), "Unable to start MinIO")
+ } else if !env.IsSet(config.EnvRootUser) && !env.IsSet(config.EnvRootPassword) {
+ if !env.IsSet(config.EnvAccessKey) && env.IsSet(config.EnvSecretKey) {
+ logger.Fatal(config.ErrMissingEnvCredentialAccessKey(nil), "Unable to start MinIO")
+ } else if env.IsSet(config.EnvAccessKey) && !env.IsSet(config.EnvSecretKey) {
+ logger.Fatal(config.ErrMissingEnvCredentialSecretKey(nil), "Unable to start MinIO")
+ }
+ }
+
+ globalEnableSyncBoot = env.Get("MINIO_SYNC_BOOT", config.EnableOff) == config.EnableOn
+}
+
+func loadRootCredentials() auth.Credentials {
+ // At this point, either both environment variables
+ // are defined or both are not defined.
+ // Check both cases and authenticate them if correctly defined
+ var user, password string
+ var legacyCredentials bool
+ //nolint:gocritic
+ if env.IsSet(config.EnvRootUser) && env.IsSet(config.EnvRootPassword) {
+ user = env.Get(config.EnvRootUser, "")
+ password = env.Get(config.EnvRootPassword, "")
+ } else if env.IsSet(config.EnvAccessKey) && env.IsSet(config.EnvSecretKey) {
+ user = env.Get(config.EnvAccessKey, "")
+ password = env.Get(config.EnvSecretKey, "")
+ legacyCredentials = true
+ } else if globalServerCtxt.RootUser != "" && globalServerCtxt.RootPwd != "" {
+ user, password = globalServerCtxt.RootUser, globalServerCtxt.RootPwd
+ }
+ if user == "" || password == "" {
+ return auth.Credentials{}
+ }
+ cred, err := auth.CreateCredentials(user, password)
+ if err != nil {
+ if legacyCredentials {
+ logger.Fatal(config.ErrInvalidCredentials(err),
+ "Unable to validate credentials inherited from the shell environment")
+ } else {
+ logger.Fatal(config.ErrInvalidRootUserCredentials(err),
+ "Unable to validate credentials inherited from the shell environment")
+ }
+ }
+ if env.IsSet(config.EnvAccessKey) && env.IsSet(config.EnvSecretKey) {
+ msg := fmt.Sprintf("WARNING: %s and %s are deprecated.\n"+
+ " Please use %s and %s",
+ config.EnvAccessKey, config.EnvSecretKey,
+ config.EnvRootUser, config.EnvRootPassword)
+ logger.Info(color.RedBold(msg))
+ }
+ globalCredViaEnv = true
+ return cred
+}
+
+// autoGenerateRootCredentials generates root credentials deterministically if
+// a KMS is configured, no manual credentials have been specified and if root
+// access is disabled.
+func autoGenerateRootCredentials() auth.Credentials {
+ if GlobalKMS == nil {
+ return globalActiveCred
+ }
+
+ aKey, err := GlobalKMS.MAC(GlobalContext, &kms.MACRequest{Message: []byte("root access key")})
+ if IsErrIgnored(err, kes.ErrNotAllowed, kms.ErrNotSupported, errors.ErrUnsupported, kms.ErrPermission) {
+ // If we don't have permission to compute the HMAC, don't change the cred.
+ return globalActiveCred
+ }
+ if err != nil {
+ logger.Fatal(err, "Unable to generate root access key using KMS")
+ }
+
+ sKey, err := GlobalKMS.MAC(GlobalContext, &kms.MACRequest{Message: []byte("root secret key")})
+ if err != nil {
+ // Here, we must have permission. Otherwise, we would have failed earlier.
+ logger.Fatal(err, "Unable to generate root secret key using KMS")
+ }
+
+ accessKey, err := auth.GenerateAccessKey(20, bytes.NewReader(aKey))
+ if err != nil {
+ logger.Fatal(err, "Unable to generate root access key")
+ }
+ secretKey, err := auth.GenerateSecretKey(32, bytes.NewReader(sKey))
+ if err != nil {
+ logger.Fatal(err, "Unable to generate root secret key")
+ }
+
+ logger.Info("Automatically generated root access key and secret key with the KMS")
+ return auth.Credentials{
+ AccessKey: accessKey,
+ SecretKey: secretKey,
+ }
+}
+
+// Initialize KMS global variable after valiadating and loading the configuration.
+// It depends on KMS env variables and global cli flags.
+func handleKMSConfig() {
+ present, err := kms.IsPresent()
+ if err != nil {
+ logger.Fatal(err, "Invalid KMS configuration specified")
+ }
+ if !present {
+ return
+ }
+
+ KMS, err := kms.Connect(GlobalContext, &kms.ConnectionOptions{
+ CADir: globalCertsCADir.Get(),
+ })
+ if err != nil {
+ logger.Fatal(err, "Failed to connect to KMS")
+ }
+
+ if _, err = KMS.GenerateKey(GlobalContext, &kms.GenerateKeyRequest{}); errors.Is(err, kms.ErrKeyNotFound) {
+ err = KMS.CreateKey(GlobalContext, &kms.CreateKeyRequest{Name: KMS.DefaultKey})
+ }
+ if err != nil && !errors.Is(err, kms.ErrKeyExists) && !errors.Is(err, kms.ErrPermission) {
+ logger.Fatal(err, "Failed to connect to KMS")
+ }
+ GlobalKMS = KMS
+}
+
+func getTLSConfig() (x509Certs []*x509.Certificate, manager *certs.Manager, secureConn bool, err error) {
+ if !isFile(getPublicCertFile()) || !isFile(getPrivateKeyFile()) {
+ return nil, nil, false, nil
+ }
+
+ if x509Certs, err = config.ParsePublicCertFile(getPublicCertFile()); err != nil {
+ return nil, nil, false, err
+ }
+
+ manager, err = certs.NewManager(GlobalContext, getPublicCertFile(), getPrivateKeyFile(), config.LoadX509KeyPair)
+ if err != nil {
+ return nil, nil, false, err
+ }
+
+ // MinIO has support for multiple certificates. It expects the following structure:
+ // certs/
+ // │
+ // ├─ public.crt
+ // ├─ private.key
+ // │
+ // ├─ example.com/
+ // │ │
+ // │ ├─ public.crt
+ // │ └─ private.key
+ // └─ foobar.org/
+ // │
+ // ├─ public.crt
+ // └─ private.key
+ // ...
+ //
+ // Therefore, we read all filenames in the cert directory and check
+ // for each directory whether it contains a public.crt and private.key.
+ // If so, we try to add it to certificate manager.
+ root, err := Open(globalCertsDir.Get())
+ if err != nil {
+ return nil, nil, false, err
+ }
+ defer root.Close()
+
+ files, err := root.Readdir(-1)
+ if err != nil {
+ return nil, nil, false, err
+ }
+ for _, file := range files {
+ // Ignore all
+ // - regular files
+ // - "CAs" directory
+ // - any directory which starts with ".."
+ if file.Mode().IsRegular() || file.Name() == "CAs" || strings.HasPrefix(file.Name(), "..") {
+ continue
+ }
+ if file.Mode()&os.ModeSymlink == os.ModeSymlink {
+ file, err = Stat(filepath.Join(root.Name(), file.Name()))
+ if err != nil {
+ // not accessible ignore
+ continue
+ }
+ if !file.IsDir() {
+ continue
+ }
+ }
+
+ var (
+ certFile = filepath.Join(root.Name(), file.Name(), publicCertFile)
+ keyFile = filepath.Join(root.Name(), file.Name(), privateKeyFile)
+ )
+ if !isFile(certFile) || !isFile(keyFile) {
+ continue
+ }
+ if err = manager.AddCertificate(certFile, keyFile); err != nil {
+ err = fmt.Errorf("Unable to load TLS certificate '%s,%s': %w", certFile, keyFile, err)
+ bootLogIf(GlobalContext, err, logger.ErrorKind)
+ }
+ }
+ secureConn = true
+
+ // Certs might be symlinks, reload them every 10 seconds.
+ manager.UpdateReloadDuration(10 * time.Second)
+
+ // syscall.SIGHUP to reload the certs.
+ manager.ReloadOnSignal(syscall.SIGHUP)
+
+ return x509Certs, manager, secureConn, nil
+}
+
+// contextCanceled returns whether a context is canceled.
+func contextCanceled(ctx context.Context) bool {
+ select {
+ case <-ctx.Done():
+ return true
+ default:
+ return false
+ }
+}
+
+// bgContext returns a context that can be used for async operations.
+// Cancellation/timeouts are removed, so parent cancellations/timeout will
+// not propagate from parent.
+// Context values are preserved.
+// This can be used for goroutines that live beyond the parent context.
+func bgContext(parent context.Context) context.Context {
+ return bgCtx{parent: parent}
+}
+
+type bgCtx struct {
+ parent context.Context
+}
+
+func (a bgCtx) Done() <-chan struct{} {
+ return nil
+}
+
+func (a bgCtx) Err() error {
+ return nil
+}
+
+func (a bgCtx) Deadline() (deadline time.Time, ok bool) {
+ return time.Time{}, false
+}
+
+func (a bgCtx) Value(key any) any {
+ return a.parent.Value(key)
+}
diff --git a/cmd/common-main_test.go b/cmd/common-main_test.go
new file mode 100644
index 00000000..9757267d
--- /dev/null
+++ b/cmd/common-main_test.go
@@ -0,0 +1,183 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "errors"
+ "os"
+ "reflect"
+ "testing"
+)
+
+func Test_readFromSecret(t *testing.T) {
+ testCases := []struct {
+ content string
+ expectedErr bool
+ expectedValue string
+ }{
+ {
+ "value\n",
+ false,
+ "value",
+ },
+ {
+ " \t\n Hello, Gophers \n\t\r\n",
+ false,
+ "Hello, Gophers",
+ },
+ }
+
+ for _, testCase := range testCases {
+ t.Run("", func(t *testing.T) {
+ tmpfile, err := os.CreateTemp(t.TempDir(), "testfile")
+ if err != nil {
+ t.Error(err)
+ }
+ tmpfile.WriteString(testCase.content)
+ tmpfile.Sync()
+ tmpfile.Close()
+
+ value, err := readFromSecret(tmpfile.Name())
+ if err != nil && !testCase.expectedErr {
+ t.Error(err)
+ }
+ if err == nil && testCase.expectedErr {
+ t.Error(errors.New("expected error, found success"))
+ }
+ if value != testCase.expectedValue {
+ t.Errorf("Expected %s, got %s", testCase.expectedValue, value)
+ }
+ })
+ }
+}
+
+func Test_minioEnvironFromFile(t *testing.T) {
+ testCases := []struct {
+ content string
+ expectedErr bool
+ expectedEkvs []envKV
+ }{
+ {
+ `
+export MINIO_ROOT_USER=minio
+export MINIO_ROOT_PASSWORD=minio123`,
+ false,
+ []envKV{
+ {
+ Key: "MINIO_ROOT_USER",
+ Value: "minio",
+ },
+ {
+ Key: "MINIO_ROOT_PASSWORD",
+ Value: "minio123",
+ },
+ },
+ },
+ // Value with double quotes
+ {
+ `export MINIO_ROOT_USER="minio"`,
+ false,
+ []envKV{
+ {
+ Key: "MINIO_ROOT_USER",
+ Value: "minio",
+ },
+ },
+ },
+ // Value with single quotes
+ {
+ `export MINIO_ROOT_USER='minio'`,
+ false,
+ []envKV{
+ {
+ Key: "MINIO_ROOT_USER",
+ Value: "minio",
+ },
+ },
+ },
+ {
+ `
+MINIO_ROOT_USER=minio
+MINIO_ROOT_PASSWORD=minio123`,
+ false,
+ []envKV{
+ {
+ Key: "MINIO_ROOT_USER",
+ Value: "minio",
+ },
+ {
+ Key: "MINIO_ROOT_PASSWORD",
+ Value: "minio123",
+ },
+ },
+ },
+ {
+ `
+export MINIO_ROOT_USERminio
+export MINIO_ROOT_PASSWORD=minio123`,
+ true,
+ nil,
+ },
+ {
+ `
+# simple comment
+# MINIO_ROOT_USER=minioadmin
+# MINIO_ROOT_PASSWORD=minioadmin
+MINIO_ROOT_USER=minio
+MINIO_ROOT_PASSWORD=minio123`,
+ false,
+ []envKV{
+ {
+ Key: "MINIO_ROOT_USER",
+ Value: "minio",
+ },
+ {
+ Key: "MINIO_ROOT_PASSWORD",
+ Value: "minio123",
+ },
+ },
+ },
+ }
+ for _, testCase := range testCases {
+ t.Run("", func(t *testing.T) {
+ tmpfile, err := os.CreateTemp(t.TempDir(), "testfile")
+ if err != nil {
+ t.Error(err)
+ }
+ tmpfile.WriteString(testCase.content)
+ tmpfile.Sync()
+ tmpfile.Close()
+
+ ekvs, err := minioEnvironFromFile(tmpfile.Name())
+ if err != nil && !testCase.expectedErr {
+ t.Error(err)
+ }
+ if err == nil && testCase.expectedErr {
+ t.Error(errors.New("expected error, found success"))
+ }
+
+ if len(ekvs) != len(testCase.expectedEkvs) {
+ t.Errorf("expected %v keys, got %v keys", len(testCase.expectedEkvs), len(ekvs))
+ }
+
+ if !reflect.DeepEqual(ekvs, testCase.expectedEkvs) {
+ t.Errorf("expected %v, got %v", testCase.expectedEkvs, ekvs)
+ }
+ })
+ }
+}
diff --git a/cmd/config-common.go b/cmd/config-common.go
new file mode 100644
index 00000000..71140aa3
--- /dev/null
+++ b/cmd/config-common.go
@@ -0,0 +1,97 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "io"
+ "net/http"
+
+ "github.com/minio/minio/internal/hash"
+)
+
+var errConfigNotFound = errors.New("config file not found")
+
+func readConfigWithMetadata(ctx context.Context, store objectIO, configFile string, opts ObjectOptions) ([]byte, ObjectInfo, error) {
+ r, err := store.GetObjectNInfo(ctx, minioMetaBucket, configFile, nil, http.Header{}, opts)
+ if err != nil {
+ if isErrObjectNotFound(err) {
+ return nil, ObjectInfo{}, errConfigNotFound
+ }
+
+ return nil, ObjectInfo{}, err
+ }
+ defer r.Close()
+
+ buf, err := io.ReadAll(r)
+ if err != nil {
+ return nil, ObjectInfo{}, err
+ }
+ if len(buf) == 0 {
+ return nil, ObjectInfo{}, errConfigNotFound
+ }
+ return buf, r.ObjInfo, nil
+}
+
+func readConfig(ctx context.Context, store objectIO, configFile string) ([]byte, error) {
+ buf, _, err := readConfigWithMetadata(ctx, store, configFile, ObjectOptions{})
+ return buf, err
+}
+
+type objectDeleter interface {
+ DeleteObject(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error)
+}
+
+func deleteConfig(ctx context.Context, objAPI objectDeleter, configFile string) error {
+ _, err := objAPI.DeleteObject(ctx, minioMetaBucket, configFile, ObjectOptions{
+ DeletePrefix: true,
+ DeletePrefixObject: true, // use prefix delete on exact object (this is an optimization to avoid fan-out calls)
+ })
+ if err != nil && isErrObjectNotFound(err) {
+ return errConfigNotFound
+ }
+ return err
+}
+
+func saveConfigWithOpts(ctx context.Context, store objectIO, configFile string, data []byte, opts ObjectOptions) error {
+ hashReader, err := hash.NewReader(ctx, bytes.NewReader(data), int64(len(data)), "", getSHA256Hash(data), int64(len(data)))
+ if err != nil {
+ return err
+ }
+
+ _, err = store.PutObject(ctx, minioMetaBucket, configFile, NewPutObjReader(hashReader), opts)
+ return err
+}
+
+func saveConfig(ctx context.Context, store objectIO, configFile string, data []byte) error {
+ return saveConfigWithOpts(ctx, store, configFile, data, ObjectOptions{MaxParity: true})
+}
+
+func checkConfig(ctx context.Context, objAPI ObjectLayer, configFile string) error {
+ if _, err := objAPI.GetObjectInfo(ctx, minioMetaBucket, configFile, ObjectOptions{}); err != nil {
+ // Treat object not found as config not found.
+ if isErrObjectNotFound(err) {
+ return errConfigNotFound
+ }
+
+ return err
+ }
+ return nil
+}
diff --git a/cmd/config-current.go b/cmd/config-current.go
new file mode 100644
index 00000000..a87e2876
--- /dev/null
+++ b/cmd/config-current.go
@@ -0,0 +1,861 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "maps"
+ "strings"
+ "sync"
+
+ "github.com/minio/minio/internal/config/browser"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/config"
+ "github.com/minio/minio/internal/config/api"
+ "github.com/minio/minio/internal/config/batch"
+ "github.com/minio/minio/internal/config/callhome"
+ "github.com/minio/minio/internal/config/compress"
+ "github.com/minio/minio/internal/config/dns"
+ "github.com/minio/minio/internal/config/drive"
+ "github.com/minio/minio/internal/config/etcd"
+ "github.com/minio/minio/internal/config/heal"
+ xldap "github.com/minio/minio/internal/config/identity/ldap"
+ "github.com/minio/minio/internal/config/identity/openid"
+ idplugin "github.com/minio/minio/internal/config/identity/plugin"
+ xtls "github.com/minio/minio/internal/config/identity/tls"
+ "github.com/minio/minio/internal/config/ilm"
+ "github.com/minio/minio/internal/config/lambda"
+ "github.com/minio/minio/internal/config/notify"
+ "github.com/minio/minio/internal/config/policy/opa"
+ polplugin "github.com/minio/minio/internal/config/policy/plugin"
+ "github.com/minio/minio/internal/config/scanner"
+ "github.com/minio/minio/internal/config/storageclass"
+ "github.com/minio/minio/internal/config/subnet"
+ "github.com/minio/minio/internal/crypto"
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/env"
+)
+
+func initHelp() {
+ kvs := map[string]config.KVS{
+ config.EtcdSubSys: etcd.DefaultKVS,
+ config.CompressionSubSys: compress.DefaultKVS,
+ config.IdentityLDAPSubSys: xldap.DefaultKVS,
+ config.IdentityOpenIDSubSys: openid.DefaultKVS,
+ config.IdentityTLSSubSys: xtls.DefaultKVS,
+ config.IdentityPluginSubSys: idplugin.DefaultKVS,
+ config.PolicyOPASubSys: opa.DefaultKVS,
+ config.PolicyPluginSubSys: polplugin.DefaultKVS,
+ config.SiteSubSys: config.DefaultSiteKVS,
+ config.RegionSubSys: config.DefaultRegionKVS,
+ config.APISubSys: api.DefaultKVS,
+ config.LoggerWebhookSubSys: logger.DefaultLoggerWebhookKVS,
+ config.AuditWebhookSubSys: logger.DefaultAuditWebhookKVS,
+ config.AuditKafkaSubSys: logger.DefaultAuditKafkaKVS,
+ config.ScannerSubSys: scanner.DefaultKVS,
+ config.SubnetSubSys: subnet.DefaultKVS,
+ config.CallhomeSubSys: callhome.DefaultKVS,
+ config.DriveSubSys: drive.DefaultKVS,
+ config.ILMSubSys: ilm.DefaultKVS,
+ config.BatchSubSys: batch.DefaultKVS,
+ config.BrowserSubSys: browser.DefaultKVS,
+ }
+ maps.Copy(kvs, notify.DefaultNotificationKVS)
+ maps.Copy(kvs, lambda.DefaultLambdaKVS)
+ if globalIsErasure {
+ kvs[config.StorageClassSubSys] = storageclass.DefaultKVS
+ kvs[config.HealSubSys] = heal.DefaultKVS
+ }
+ config.RegisterDefaultKVS(kvs)
+
+ // Captures help for each sub-system
+ helpSubSys := config.HelpKVS{
+ config.HelpKV{
+ Key: config.SubnetSubSys,
+ Type: "string",
+ Description: "register Enterprise license for the cluster",
+ Optional: true,
+ },
+ config.HelpKV{
+ Key: config.CallhomeSubSys,
+ Type: "string",
+ Description: "enable callhome to MinIO SUBNET",
+ Optional: true,
+ },
+ config.HelpKV{
+ Key: config.DriveSubSys,
+ Description: "enable drive specific settings",
+ },
+ config.HelpKV{
+ Key: config.SiteSubSys,
+ Description: "label the server and its location",
+ },
+ config.HelpKV{
+ Key: config.APISubSys,
+ Description: "manage global HTTP API call specific features, such as throttling, authentication types, etc.",
+ },
+ config.HelpKV{
+ Key: config.ScannerSubSys,
+ Description: "manage namespace scanning for usage calculation, lifecycle, healing and more",
+ },
+ config.HelpKV{
+ Key: config.BatchSubSys,
+ Description: "manage batch job workers and wait times",
+ },
+ config.HelpKV{
+ Key: config.CompressionSubSys,
+ Description: "enable server side compression of objects",
+ },
+ config.HelpKV{
+ Key: config.IdentityOpenIDSubSys,
+ Description: "enable OpenID SSO support",
+ MultipleTargets: true,
+ },
+ config.HelpKV{
+ Key: config.IdentityLDAPSubSys,
+ Description: "enable LDAP SSO support",
+ },
+ config.HelpKV{
+ Key: config.IdentityTLSSubSys,
+ Description: "enable X.509 TLS certificate SSO support",
+ },
+ config.HelpKV{
+ Key: config.IdentityPluginSubSys,
+ Description: "enable Identity Plugin via external hook",
+ },
+ config.HelpKV{
+ Key: config.PolicyPluginSubSys,
+ Description: "enable Access Management Plugin for policy enforcement",
+ },
+ config.HelpKV{
+ Key: config.LoggerWebhookSubSys,
+ Description: "send server logs to webhook endpoints",
+ MultipleTargets: true,
+ },
+ config.HelpKV{
+ Key: config.AuditWebhookSubSys,
+ Description: "send audit logs to webhook endpoints",
+ MultipleTargets: true,
+ },
+ config.HelpKV{
+ Key: config.AuditKafkaSubSys,
+ Description: "send audit logs to kafka endpoints",
+ MultipleTargets: true,
+ },
+ config.HelpKV{
+ Key: config.NotifyWebhookSubSys,
+ Description: "publish bucket notifications to webhook endpoints",
+ MultipleTargets: true,
+ },
+ config.HelpKV{
+ Key: config.NotifyAMQPSubSys,
+ Description: "publish bucket notifications to AMQP endpoints",
+ MultipleTargets: true,
+ },
+ config.HelpKV{
+ Key: config.NotifyKafkaSubSys,
+ Description: "publish bucket notifications to Kafka endpoints",
+ MultipleTargets: true,
+ },
+ config.HelpKV{
+ Key: config.NotifyMQTTSubSys,
+ Description: "publish bucket notifications to MQTT endpoints",
+ MultipleTargets: true,
+ },
+ config.HelpKV{
+ Key: config.NotifyNATSSubSys,
+ Description: "publish bucket notifications to NATS endpoints",
+ MultipleTargets: true,
+ },
+ config.HelpKV{
+ Key: config.NotifyNSQSubSys,
+ Description: "publish bucket notifications to NSQ endpoints",
+ MultipleTargets: true,
+ },
+ config.HelpKV{
+ Key: config.NotifyMySQLSubSys,
+ Description: "publish bucket notifications to MySQL databases",
+ MultipleTargets: true,
+ },
+ config.HelpKV{
+ Key: config.NotifyPostgresSubSys,
+ Description: "publish bucket notifications to Postgres databases",
+ MultipleTargets: true,
+ },
+ config.HelpKV{
+ Key: config.NotifyESSubSys,
+ Description: "publish bucket notifications to Elasticsearch endpoints",
+ MultipleTargets: true,
+ },
+ config.HelpKV{
+ Key: config.NotifyRedisSubSys,
+ Description: "publish bucket notifications to Redis datastores",
+ MultipleTargets: true,
+ },
+ config.HelpKV{
+ Key: config.LambdaWebhookSubSys,
+ Description: "manage remote lambda functions",
+ MultipleTargets: true,
+ },
+ config.HelpKV{
+ Key: config.EtcdSubSys,
+ Description: "persist IAM assets externally to etcd",
+ },
+ config.HelpKV{
+ Key: config.BrowserSubSys,
+ Description: "manage Browser HTTP specific features, such as Security headers, etc.",
+ Optional: true,
+ },
+ config.HelpKV{
+ Key: config.ILMSubSys,
+ Description: "manage ILM settings for expiration and transition workers",
+ Optional: true,
+ },
+ }
+
+ if globalIsErasure {
+ helpSubSys = append(helpSubSys, config.HelpKV{
+ Key: config.StorageClassSubSys,
+ Description: "define object level redundancy",
+ }, config.HelpKV{
+ Key: config.HealSubSys,
+ Description: "manage object healing frequency and bitrot verification checks",
+ })
+ }
+
+ helpMap := map[string]config.HelpKVS{
+ "": helpSubSys, // Help for all sub-systems.
+ config.SiteSubSys: config.SiteHelp,
+ config.RegionSubSys: config.RegionHelp,
+ config.APISubSys: api.Help,
+ config.StorageClassSubSys: storageclass.Help,
+ config.EtcdSubSys: etcd.Help,
+ config.CompressionSubSys: compress.Help,
+ config.HealSubSys: heal.Help,
+ config.BatchSubSys: batch.Help,
+ config.ScannerSubSys: scanner.Help,
+ config.IdentityOpenIDSubSys: openid.Help,
+ config.IdentityLDAPSubSys: xldap.Help,
+ config.IdentityTLSSubSys: xtls.Help,
+ config.IdentityPluginSubSys: idplugin.Help,
+ config.PolicyOPASubSys: opa.Help,
+ config.PolicyPluginSubSys: polplugin.Help,
+ config.LoggerWebhookSubSys: logger.Help,
+ config.AuditWebhookSubSys: logger.HelpWebhook,
+ config.AuditKafkaSubSys: logger.HelpKafka,
+ config.NotifyAMQPSubSys: notify.HelpAMQP,
+ config.NotifyKafkaSubSys: notify.HelpKafka,
+ config.NotifyMQTTSubSys: notify.HelpMQTT,
+ config.NotifyNATSSubSys: notify.HelpNATS,
+ config.NotifyNSQSubSys: notify.HelpNSQ,
+ config.NotifyMySQLSubSys: notify.HelpMySQL,
+ config.NotifyPostgresSubSys: notify.HelpPostgres,
+ config.NotifyRedisSubSys: notify.HelpRedis,
+ config.NotifyWebhookSubSys: notify.HelpWebhook,
+ config.NotifyESSubSys: notify.HelpES,
+ config.LambdaWebhookSubSys: lambda.HelpWebhook,
+ config.SubnetSubSys: subnet.HelpSubnet,
+ config.CallhomeSubSys: callhome.HelpCallhome,
+ config.DriveSubSys: drive.HelpDrive,
+ config.BrowserSubSys: browser.Help,
+ config.ILMSubSys: ilm.Help,
+ }
+
+ config.RegisterHelpSubSys(helpMap)
+
+ // save top-level help for deprecated sub-systems in a separate map.
+ deprecatedHelpKVMap := map[string]config.HelpKV{
+ config.RegionSubSys: {
+ Key: config.RegionSubSys,
+ Description: "[DEPRECATED - use `site` instead] label the location of the server",
+ },
+ config.PolicyOPASubSys: {
+ Key: config.PolicyOPASubSys,
+ Description: "[DEPRECATED - use `policy_plugin` instead] enable external OPA for policy enforcement",
+ },
+ }
+
+ config.RegisterHelpDeprecatedSubSys(deprecatedHelpKVMap)
+}
+
+var (
+ // globalServerConfig server config.
+ globalServerConfig config.Config
+ globalServerConfigMu sync.RWMutex
+)
+
+func validateSubSysConfig(ctx context.Context, s config.Config, subSys string, objAPI ObjectLayer) error {
+ switch subSys {
+ case config.SiteSubSys:
+ if _, err := config.LookupSite(s[config.SiteSubSys][config.Default], s[config.RegionSubSys][config.Default]); err != nil {
+ return err
+ }
+ case config.APISubSys:
+ if _, err := api.LookupConfig(s[config.APISubSys][config.Default]); err != nil {
+ return err
+ }
+ case config.BatchSubSys:
+ if _, err := batch.LookupConfig(s[config.BatchSubSys][config.Default]); err != nil {
+ return err
+ }
+ case config.StorageClassSubSys:
+ if objAPI == nil {
+ return errServerNotInitialized
+ }
+ for _, setDriveCount := range objAPI.SetDriveCounts() {
+ if _, err := storageclass.LookupConfig(s[config.StorageClassSubSys][config.Default], setDriveCount); err != nil {
+ return err
+ }
+ }
+ case config.CompressionSubSys:
+ if _, err := compress.LookupConfig(s[config.CompressionSubSys][config.Default]); err != nil {
+ return err
+ }
+ case config.HealSubSys:
+ if _, err := heal.LookupConfig(s[config.HealSubSys][config.Default]); err != nil {
+ return err
+ }
+ case config.ScannerSubSys:
+ if _, err := scanner.LookupConfig(s[config.ScannerSubSys][config.Default]); err != nil {
+ return err
+ }
+ case config.EtcdSubSys:
+ etcdCfg, err := etcd.LookupConfig(s[config.EtcdSubSys][config.Default], globalRootCAs)
+ if err != nil {
+ return err
+ }
+ if etcdCfg.Enabled {
+ etcdClnt, err := etcd.New(etcdCfg)
+ if err != nil {
+ return err
+ }
+ etcdClnt.Close()
+ }
+ case config.IdentityOpenIDSubSys:
+ if _, err := openid.LookupConfig(s,
+ xhttp.WithUserAgent(NewHTTPTransport(), func() string {
+ return getUserAgent(getMinioMode())
+ }), xhttp.DrainBody, globalSite.Region()); err != nil {
+ return err
+ }
+ case config.IdentityLDAPSubSys:
+ cfg, err := xldap.Lookup(s, globalRootCAs)
+ if err != nil {
+ return err
+ }
+ if cfg.Enabled() {
+ conn, cerr := cfg.LDAP.Connect()
+ if cerr != nil {
+ return cerr
+ }
+ conn.Close()
+ }
+ case config.IdentityTLSSubSys:
+ if _, err := xtls.Lookup(s[config.IdentityTLSSubSys][config.Default]); err != nil {
+ return err
+ }
+ case config.IdentityPluginSubSys:
+ if _, err := idplugin.LookupConfig(s[config.IdentityPluginSubSys][config.Default],
+ NewHTTPTransport(), xhttp.DrainBody, globalSite.Region()); err != nil {
+ return err
+ }
+ case config.SubnetSubSys:
+ if _, err := subnet.LookupConfig(s[config.SubnetSubSys][config.Default], nil); err != nil {
+ return err
+ }
+ case config.CallhomeSubSys:
+ cfg, err := callhome.LookupConfig(s[config.CallhomeSubSys][config.Default])
+ if err != nil {
+ return err
+ }
+ // callhome cannot be enabled if license is not registered yet, throw an error.
+ if cfg.Enabled() && !globalSubnetConfig.Registered() {
+ return errors.New("Deployment is not registered with SUBNET. Please register the deployment via 'mc license register ALIAS'")
+ }
+ case config.DriveSubSys:
+ if _, err := drive.LookupConfig(s[config.DriveSubSys][config.Default]); err != nil {
+ return err
+ }
+ case config.PolicyOPASubSys:
+ // In case legacy OPA config is being set, we treat it as if the
+ // AuthZPlugin is being set.
+ subSys = config.PolicyPluginSubSys
+ fallthrough
+ case config.PolicyPluginSubSys:
+ if ppargs, err := polplugin.LookupConfig(s, GetDefaultConnSettings(), xhttp.DrainBody); err != nil {
+ return err
+ } else if ppargs.URL == nil {
+ // Check if legacy opa is configured.
+ if _, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default],
+ NewHTTPTransport(), xhttp.DrainBody); err != nil {
+ return err
+ }
+ }
+ case config.BrowserSubSys:
+ if _, err := browser.LookupConfig(s[config.BrowserSubSys][config.Default]); err != nil {
+ return err
+ }
+ default:
+ if config.LoggerSubSystems.Contains(subSys) {
+ if err := logger.ValidateSubSysConfig(ctx, s, subSys); err != nil {
+ return err
+ }
+ }
+ }
+
+ if config.NotifySubSystems.Contains(subSys) {
+ if err := notify.TestSubSysNotificationTargets(ctx, s, subSys, NewHTTPTransport()); err != nil {
+ return err
+ }
+ }
+
+ if config.LambdaSubSystems.Contains(subSys) {
+ if err := lambda.TestSubSysLambdaTargets(GlobalContext, s, subSys, NewHTTPTransport()); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func validateConfig(ctx context.Context, s config.Config, subSys string) error {
+ objAPI := newObjectLayerFn()
+
+ // We must have a global lock for this so nobody else modifies env while we do.
+ defer env.LockSetEnv()()
+
+ // Disable merging env values with config for validation.
+ env.SetEnvOff()
+
+ // Enable env values to validate KMS.
+ defer env.SetEnvOn()
+ if subSys != "" {
+ return validateSubSysConfig(ctx, s, subSys, objAPI)
+ }
+
+ // No sub-system passed. Validate all of them.
+ for _, ss := range config.SubSystems.ToSlice() {
+ if err := validateSubSysConfig(ctx, s, ss, objAPI); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func lookupConfigs(s config.Config, objAPI ObjectLayer) {
+ ctx := GlobalContext
+
+ dnsURL, dnsUser, dnsPass, err := env.LookupEnv(config.EnvDNSWebhook)
+ if err != nil {
+ configLogIf(ctx, fmt.Errorf("Unable to initialize remote webhook DNS config %w", err))
+ }
+ if err == nil && dnsURL != "" {
+ bootstrapTraceMsg("initialize remote bucket DNS store")
+ globalDNSConfig, err = dns.NewOperatorDNS(dnsURL,
+ dns.Authentication(dnsUser, dnsPass),
+ dns.RootCAs(globalRootCAs))
+ if err != nil {
+ configLogIf(ctx, fmt.Errorf("Unable to initialize remote webhook DNS config %w", err))
+ }
+ }
+
+ etcdCfg, err := etcd.LookupConfig(s[config.EtcdSubSys][config.Default], globalRootCAs)
+ if err != nil {
+ configLogIf(ctx, fmt.Errorf("Unable to initialize etcd config: %w", err))
+ }
+
+ if etcdCfg.Enabled {
+ bootstrapTraceMsg("initialize etcd store")
+ globalEtcdClient, err = etcd.New(etcdCfg)
+ if err != nil {
+ configLogIf(ctx, fmt.Errorf("Unable to initialize etcd config: %w", err))
+ }
+
+ if len(globalDomainNames) != 0 && !globalDomainIPs.IsEmpty() && globalEtcdClient != nil {
+ if globalDNSConfig != nil {
+ // if global DNS is already configured, indicate with a warning, in case
+ // users are confused.
+ configLogIf(ctx, fmt.Errorf("DNS store is already configured with %s, etcd is not used for DNS store", globalDNSConfig))
+ } else {
+ globalDNSConfig, err = dns.NewCoreDNS(etcdCfg.Config,
+ dns.DomainNames(globalDomainNames),
+ dns.DomainIPs(globalDomainIPs),
+ dns.DomainPort(globalMinioPort),
+ dns.CoreDNSPath(etcdCfg.CoreDNSPath),
+ )
+ if err != nil {
+ configLogIf(ctx, fmt.Errorf("Unable to initialize DNS config for %s: %w",
+ globalDomainNames, err))
+ }
+ }
+ }
+ }
+
+ // Bucket federation is 'true' only when IAM assets are not namespaced
+ // per tenant and all tenants interested in globally available users
+ // if namespace was requested such as specifying etcdPathPrefix then
+ // we assume that users are interested in global bucket support
+ // but not federation.
+ globalBucketFederation = etcdCfg.PathPrefix == "" && etcdCfg.Enabled
+
+ siteCfg, err := config.LookupSite(s[config.SiteSubSys][config.Default], s[config.RegionSubSys][config.Default])
+ if err != nil {
+ configLogIf(ctx, fmt.Errorf("Invalid site configuration: %w", err))
+ }
+ globalSite.Update(siteCfg)
+
+ globalAutoEncryption = crypto.LookupAutoEncryption() // Enable auto-encryption if enabled
+ if globalAutoEncryption && GlobalKMS == nil {
+ logger.Fatal(errors.New("no KMS configured"), "MINIO_KMS_AUTO_ENCRYPTION requires a valid KMS configuration")
+ }
+
+ transport := NewHTTPTransport()
+
+ bootstrapTraceMsg("initialize the event notification targets")
+ globalNotifyTargetList, err = notify.FetchEnabledTargets(GlobalContext, s, transport)
+ if err != nil {
+ configLogIf(ctx, fmt.Errorf("Unable to initialize notification target(s): %w", err))
+ }
+
+ bootstrapTraceMsg("initialize the lambda targets")
+ globalLambdaTargetList, err = lambda.FetchEnabledTargets(GlobalContext, s, transport)
+ if err != nil {
+ configLogIf(ctx, fmt.Errorf("Unable to initialize lambda target(s): %w", err))
+ }
+
+ bootstrapTraceMsg("applying the dynamic configuration")
+ // Apply dynamic config values
+ if err := applyDynamicConfig(ctx, objAPI, s); err != nil {
+ configLogIf(ctx, err)
+ }
+}
+
+func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s config.Config, subSys string) error {
+ if objAPI == nil {
+ return errServerNotInitialized
+ }
+
+ var errs []error
+ setDriveCounts := objAPI.SetDriveCounts()
+ switch subSys {
+ case config.APISubSys:
+ apiConfig, err := api.LookupConfig(s[config.APISubSys][config.Default])
+ if err != nil {
+ configLogIf(ctx, fmt.Errorf("Invalid api configuration: %w", err))
+ }
+
+ globalAPIConfig.init(apiConfig, setDriveCounts, objAPI.Legacy())
+ setRemoteInstanceTransport(NewHTTPTransportWithTimeout(apiConfig.RemoteTransportDeadline))
+ case config.CompressionSubSys:
+ cmpCfg, err := compress.LookupConfig(s[config.CompressionSubSys][config.Default])
+ if err != nil {
+ return fmt.Errorf("Unable to setup Compression: %w", err)
+ }
+ globalCompressConfigMu.Lock()
+ globalCompressConfig = cmpCfg
+ globalCompressConfigMu.Unlock()
+ case config.HealSubSys:
+ healCfg, err := heal.LookupConfig(s[config.HealSubSys][config.Default])
+ if err != nil {
+ errs = append(errs, fmt.Errorf("Unable to apply heal config: %w", err))
+ } else {
+ globalHealConfig.Update(healCfg)
+ }
+ case config.BatchSubSys:
+ batchCfg, err := batch.LookupConfig(s[config.BatchSubSys][config.Default])
+ if err != nil {
+ errs = append(errs, fmt.Errorf("Unable to apply batch config: %w", err))
+ } else {
+ globalBatchConfig.Update(batchCfg)
+ }
+ case config.ScannerSubSys:
+ scannerCfg, err := scanner.LookupConfig(s[config.ScannerSubSys][config.Default])
+ if err != nil {
+ errs = append(errs, fmt.Errorf("Unable to apply scanner config: %w", err))
+ } else {
+ // update dynamic scanner values.
+ scannerIdleMode.Store(scannerCfg.IdleMode)
+ scannerCycle.Store(scannerCfg.Cycle)
+ scannerExcessObjectVersions.Store(scannerCfg.ExcessVersions)
+ scannerExcessFolders.Store(scannerCfg.ExcessFolders)
+ configLogIf(ctx, scannerSleeper.Update(scannerCfg.Delay, scannerCfg.MaxWait))
+ }
+ case config.LoggerWebhookSubSys:
+ loggerCfg, err := logger.LookupConfigForSubSys(ctx, s, config.LoggerWebhookSubSys)
+ if err != nil {
+ configLogIf(ctx, fmt.Errorf("Unable to load logger webhook config: %w", err))
+ }
+ userAgent := getUserAgent(getMinioMode())
+ for n, l := range loggerCfg.HTTP {
+ if l.Enabled {
+ l.LogOnceIf = configLogOnceConsoleIf
+ l.UserAgent = userAgent
+ l.Transport = NewHTTPTransportWithClientCerts(l.ClientCert, l.ClientKey)
+ }
+ loggerCfg.HTTP[n] = l
+ }
+ if errs := logger.UpdateHTTPWebhooks(ctx, loggerCfg.HTTP); len(errs) > 0 {
+ configLogIf(ctx, fmt.Errorf("Unable to update logger webhook config: %v", errs))
+ }
+ case config.AuditWebhookSubSys:
+ loggerCfg, err := logger.LookupConfigForSubSys(ctx, s, config.AuditWebhookSubSys)
+ if err != nil {
+ configLogIf(ctx, fmt.Errorf("Unable to load audit webhook config: %w", err))
+ }
+ userAgent := getUserAgent(getMinioMode())
+ for n, l := range loggerCfg.AuditWebhook {
+ if l.Enabled {
+ l.LogOnceIf = configLogOnceConsoleIf
+ l.UserAgent = userAgent
+ l.Transport = NewHTTPTransportWithClientCerts(l.ClientCert, l.ClientKey)
+ }
+ loggerCfg.AuditWebhook[n] = l
+ }
+
+ if errs := logger.UpdateAuditWebhooks(ctx, loggerCfg.AuditWebhook); len(errs) > 0 {
+ configLogIf(ctx, fmt.Errorf("Unable to update audit webhook targets: %v", errs))
+ }
+ case config.AuditKafkaSubSys:
+ loggerCfg, err := logger.LookupConfigForSubSys(ctx, s, config.AuditKafkaSubSys)
+ if err != nil {
+ configLogIf(ctx, fmt.Errorf("Unable to load audit kafka config: %w", err))
+ }
+ for n, l := range loggerCfg.AuditKafka {
+ if l.Enabled {
+ if l.TLS.Enable {
+ l.TLS.RootCAs = globalRootCAs
+ }
+ l.LogOnce = configLogOnceIf
+ loggerCfg.AuditKafka[n] = l
+ }
+ }
+ if errs := logger.UpdateAuditKafkaTargets(ctx, loggerCfg); len(errs) > 0 {
+ configLogIf(ctx, fmt.Errorf("Unable to update audit kafka targets: %v", errs))
+ }
+ case config.StorageClassSubSys:
+ for i, setDriveCount := range setDriveCounts {
+ sc, err := storageclass.LookupConfig(s[config.StorageClassSubSys][config.Default], setDriveCount)
+ if err != nil {
+ configLogIf(ctx, fmt.Errorf("Unable to initialize storage class config: %w", err))
+ break
+ }
+ if i == 0 {
+ globalStorageClass.Update(sc)
+ }
+ }
+ case config.SubnetSubSys:
+ subnetConfig, err := subnet.LookupConfig(s[config.SubnetSubSys][config.Default], globalRemoteTargetTransport)
+ if err != nil {
+ configLogIf(ctx, fmt.Errorf("Unable to parse subnet configuration: %w", err))
+ } else {
+ globalSubnetConfig.Update(subnetConfig, globalIsCICD)
+ globalSubnetConfig.ApplyEnv() // update environment settings for Console UI
+ }
+ case config.CallhomeSubSys:
+ callhomeCfg, err := callhome.LookupConfig(s[config.CallhomeSubSys][config.Default])
+ if err != nil {
+ configLogIf(ctx, fmt.Errorf("Unable to load callhome config: %w", err))
+ } else {
+ enable := callhomeCfg.Enable && !globalCallhomeConfig.Enabled()
+ globalCallhomeConfig.Update(callhomeCfg)
+ if enable {
+ initCallhome(ctx, objAPI)
+ }
+ }
+ case config.DriveSubSys:
+ driveConfig, err := drive.LookupConfig(s[config.DriveSubSys][config.Default])
+ if err != nil {
+ configLogIf(ctx, fmt.Errorf("Unable to load drive config: %w", err))
+ } else {
+ if err = globalDriveConfig.Update(driveConfig); err != nil {
+ configLogIf(ctx, fmt.Errorf("Unable to update drive config: %v", err))
+ }
+ }
+ case config.BrowserSubSys:
+ browserCfg, err := browser.LookupConfig(s[config.BrowserSubSys][config.Default])
+ if err != nil {
+ errs = append(errs, fmt.Errorf("Unable to apply browser config: %w", err))
+ } else {
+ globalBrowserConfig.Update(browserCfg)
+ }
+ case config.ILMSubSys:
+ ilmCfg, err := ilm.LookupConfig(s[config.ILMSubSys][config.Default])
+ if err != nil {
+ errs = append(errs, fmt.Errorf("Unable to apply ilm config: %w", err))
+ } else {
+ if globalTransitionState != nil {
+ globalTransitionState.UpdateWorkers(ilmCfg.TransitionWorkers)
+ }
+ if globalExpiryState != nil {
+ globalExpiryState.ResizeWorkers(ilmCfg.ExpirationWorkers)
+ }
+ globalILMConfig.update(ilmCfg)
+ }
+ }
+ globalServerConfigMu.Lock()
+ defer globalServerConfigMu.Unlock()
+ if globalServerConfig != nil {
+ globalServerConfig[subSys] = s[subSys]
+ }
+ if len(errs) > 0 {
+ return errors.Join(errs...)
+ }
+ return nil
+}
+
+// applyDynamicConfig will apply dynamic config values.
+// Dynamic systems should be in config.SubSystemsDynamic as well.
+func applyDynamicConfig(ctx context.Context, objAPI ObjectLayer, s config.Config) error {
+ for subSys := range config.SubSystemsDynamic {
+ err := applyDynamicConfigForSubSys(ctx, objAPI, s, subSys)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// Help - return sub-system level help
+type Help struct {
+ SubSys string `json:"subSys"`
+ Description string `json:"description"`
+ MultipleTargets bool `json:"multipleTargets"`
+ KeysHelp config.HelpKVS `json:"keysHelp"`
+}
+
+// GetHelp - returns help for sub-sys, a key for a sub-system or all the help.
+func GetHelp(subSys, key string, envOnly bool) (Help, error) {
+ if len(subSys) == 0 {
+ return Help{KeysHelp: config.HelpSubSysMap[subSys]}, nil
+ }
+ subSystemValue := strings.SplitN(subSys, config.SubSystemSeparator, 2)
+ if len(subSystemValue) == 0 {
+ return Help{}, config.Errorf("invalid number of arguments %s", subSys)
+ }
+
+ subSys = subSystemValue[0]
+
+ subSysHelp, ok := config.HelpSubSysMap[""].Lookup(subSys)
+ if !ok {
+ subSysHelp, ok = config.HelpDeprecatedSubSysMap[subSys]
+ if !ok {
+ return Help{}, config.Errorf("unknown sub-system %s", subSys)
+ }
+ }
+
+ h, ok := config.HelpSubSysMap[subSys]
+ if !ok {
+ return Help{}, config.Errorf("unknown sub-system %s", subSys)
+ }
+ if key != "" {
+ value, ok := h.Lookup(key)
+ if !ok {
+ return Help{}, config.Errorf("unknown key %s for sub-system %s",
+ key, subSys)
+ }
+ h = config.HelpKVS{value}
+ }
+
+ help := config.HelpKVS{}
+
+ // Only for multiple targets, make sure
+ // to list the ENV, for regular k/v EnableKey is
+ // implicit, for ENVs we cannot make it implicit.
+ if subSysHelp.MultipleTargets {
+ key := madmin.EnableKey
+ if envOnly {
+ key = config.EnvPrefix + strings.ToTitle(subSys) + config.EnvWordDelimiter + strings.ToTitle(madmin.EnableKey)
+ }
+ help = append(help, config.HelpKV{
+ Key: key,
+ Description: fmt.Sprintf("enable %s target, default is 'off'", subSys),
+ Optional: false,
+ Type: "on|off",
+ })
+ }
+
+ for _, hkv := range h {
+ key := hkv.Key
+ if envOnly {
+ key = config.EnvPrefix + strings.ToTitle(subSys) + config.EnvWordDelimiter + strings.ToTitle(hkv.Key)
+ }
+ help = append(help, config.HelpKV{
+ Key: key,
+ Description: hkv.Description,
+ Optional: hkv.Optional,
+ Type: hkv.Type,
+ })
+ }
+
+ return Help{
+ SubSys: subSys,
+ Description: subSysHelp.Description,
+ MultipleTargets: subSysHelp.MultipleTargets,
+ KeysHelp: help,
+ }, nil
+}
+
+func newServerConfig() config.Config {
+ return config.New()
+}
+
+// newSrvConfig - initialize a new server config, saves env parameters if
+// found, otherwise use default parameters
+func newSrvConfig(objAPI ObjectLayer) error {
+ // Initialize server config.
+ srvCfg := newServerConfig()
+
+ // hold the mutex lock before a new config is assigned.
+ globalServerConfigMu.Lock()
+ globalServerConfig = srvCfg
+ globalServerConfigMu.Unlock()
+
+ // Save config into file.
+ return saveServerConfig(GlobalContext, objAPI, srvCfg)
+}
+
+func getValidConfig(objAPI ObjectLayer) (config.Config, error) {
+ return readServerConfig(GlobalContext, objAPI, nil)
+}
+
+// loadConfig - loads a new config from disk, overrides params
+// from env if found and valid
+// data is optional. If nil it will be loaded from backend.
+func loadConfig(objAPI ObjectLayer, data []byte) error {
+ bootstrapTraceMsg("load the configuration")
+ srvCfg, err := readServerConfig(GlobalContext, objAPI, data)
+ if err != nil {
+ return err
+ }
+
+ bootstrapTraceMsg("lookup the configuration")
+ // Override any values from ENVs.
+ lookupConfigs(srvCfg, objAPI)
+
+ // hold the mutex lock before a new config is assigned.
+ globalServerConfigMu.Lock()
+ globalServerConfig = srvCfg
+ globalServerConfigMu.Unlock()
+
+ return nil
+}
diff --git a/cmd/config-current_test.go b/cmd/config-current_test.go
new file mode 100644
index 00000000..338cebbe
--- /dev/null
+++ b/cmd/config-current_test.go
@@ -0,0 +1,67 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "os"
+ "testing"
+
+ "github.com/minio/minio/internal/config"
+)
+
+func TestServerConfig(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ objLayer, fsDir, err := prepareFS(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(fsDir)
+
+ if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
+ t.Fatalf("Init Test config failed")
+ }
+
+ if globalSite.Region() != globalMinioDefaultRegion {
+ t.Errorf("Expecting region `us-east-1` found %s", globalSite.Region())
+ }
+
+ // Set new region and verify.
+ config.SetRegion(globalServerConfig, "us-west-1")
+ site, err := config.LookupSite(
+ globalServerConfig[config.SiteSubSys][config.Default],
+ globalServerConfig[config.RegionSubSys][config.Default],
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+ if site.Region() != "us-west-1" {
+ t.Errorf("Expecting region `us-west-1` found %s", globalSite.Region())
+ }
+
+ if err := saveServerConfig(t.Context(), objLayer, globalServerConfig); err != nil {
+ t.Fatalf("Unable to save updated config file %s", err)
+ }
+
+ // Initialize server config.
+ if err := loadConfig(objLayer, nil); err != nil {
+ t.Fatalf("Unable to initialize from updated config file %s", err)
+ }
+}
diff --git a/cmd/config-dir.go b/cmd/config-dir.go
new file mode 100644
index 00000000..b03b175b
--- /dev/null
+++ b/cmd/config-dir.go
@@ -0,0 +1,108 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "os"
+ "path/filepath"
+
+ homedir "github.com/mitchellh/go-homedir"
+)
+
+const (
+ // Default minio configuration directory where below configuration files/directories are stored.
+ defaultMinioConfigDir = ".minio"
+
+ // Directory contains below files/directories for HTTPS configuration.
+ certsDir = "certs"
+
+ // Directory contains all CA certificates other than system defaults for HTTPS.
+ certsCADir = "CAs"
+
+ // Public certificate file for HTTPS.
+ publicCertFile = "public.crt"
+
+ // Private key file for HTTPS.
+ privateKeyFile = "private.key"
+)
+
+// ConfigDir - points to a user set directory.
+type ConfigDir struct {
+ path string
+}
+
+func getDefaultConfigDir() string {
+ homeDir, err := homedir.Dir()
+ if err != nil {
+ return ""
+ }
+
+ return filepath.Join(homeDir, defaultMinioConfigDir)
+}
+
+func getDefaultCertsDir() string {
+ return filepath.Join(getDefaultConfigDir(), certsDir)
+}
+
+func getDefaultCertsCADir() string {
+ return filepath.Join(getDefaultCertsDir(), certsCADir)
+}
+
+var (
+ // Default config, certs and CA directories.
+ defaultConfigDir = &ConfigDir{path: getDefaultConfigDir()}
+ defaultCertsDir = &ConfigDir{path: getDefaultCertsDir()}
+ defaultCertsCADir = &ConfigDir{path: getDefaultCertsCADir()}
+
+ // Points to current configuration directory -- deprecated, to be removed in future.
+ globalConfigDir = defaultConfigDir
+ // Points to current certs directory set by user with --certs-dir
+ globalCertsDir = defaultCertsDir
+ // Points to relative path to certs directory and is /CAs
+ globalCertsCADir = defaultCertsCADir
+)
+
+// Get - returns current directory.
+func (dir *ConfigDir) Get() string {
+ return dir.path
+}
+
+// Attempts to create all directories, ignores any permission denied errors.
+func mkdirAllIgnorePerm(path string) error {
+ err := os.MkdirAll(path, 0o700)
+ if err != nil {
+ // It is possible in kubernetes like deployments this directory
+ // is already mounted and is not writable, ignore any write errors.
+ if osIsPermission(err) {
+ err = nil
+ }
+ }
+ return err
+}
+
+func getConfigFile() string {
+ return filepath.Join(globalConfigDir.Get(), minioConfigFile)
+}
+
+func getPublicCertFile() string {
+ return filepath.Join(globalCertsDir.Get(), publicCertFile)
+}
+
+func getPrivateKeyFile() string {
+ return filepath.Join(globalCertsDir.Get(), privateKeyFile)
+}
diff --git a/cmd/config-encrypted_test.go b/cmd/config-encrypted_test.go
new file mode 100644
index 00000000..7b59ae73
--- /dev/null
+++ b/cmd/config-encrypted_test.go
@@ -0,0 +1,76 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/auth"
+)
+
+func TestDecryptData(t *testing.T) {
+ cred1 := auth.Credentials{
+ AccessKey: "minio",
+ SecretKey: "minio123",
+ }
+
+ cred2 := auth.Credentials{
+ AccessKey: "minio",
+ SecretKey: "minio1234",
+ }
+
+ data := []byte(`config data`)
+ edata1, err := madmin.EncryptData(cred1.String(), data)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ edata2, err := madmin.EncryptData(cred2.String(), data)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ tests := []struct {
+ edata []byte
+ cred auth.Credentials
+ success bool
+ }{
+ {edata1, cred1, true},
+ {edata2, cred2, true},
+ {data, cred1, false},
+ }
+
+ for _, test := range tests {
+ t.Run("", func(t *testing.T) {
+ ddata, err := madmin.DecryptData(test.cred.String(), bytes.NewReader(test.edata))
+ if err != nil && test.success {
+ t.Errorf("Expected success, saw failure %v", err)
+ }
+ if err == nil && !test.success {
+ t.Error("Expected failure, saw success")
+ }
+ if test.success {
+ if !bytes.Equal(ddata, data) {
+ t.Errorf("Expected %s, got %s", string(data), string(ddata))
+ }
+ }
+ })
+ }
+}
diff --git a/cmd/config-migrate.go b/cmd/config-migrate.go
new file mode 100644
index 00000000..0cceb185
--- /dev/null
+++ b/cmd/config-migrate.go
@@ -0,0 +1,189 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "path"
+ "strings"
+
+ "github.com/minio/minio/internal/config"
+ "github.com/minio/minio/internal/config/compress"
+ xldap "github.com/minio/minio/internal/config/identity/ldap"
+ "github.com/minio/minio/internal/config/identity/openid"
+ "github.com/minio/minio/internal/config/notify"
+ "github.com/minio/minio/internal/config/policy/opa"
+ "github.com/minio/minio/internal/config/storageclass"
+ "github.com/minio/minio/internal/event/target"
+ "github.com/minio/minio/internal/logger"
+ xnet "github.com/minio/pkg/v3/net"
+ "github.com/minio/pkg/v3/quick"
+)
+
+// Save config file to corresponding backend
+func Save(configFile string, data any) error {
+ return quick.SaveConfig(data, configFile, globalEtcdClient)
+}
+
+// Load config from backend
+func Load(configFile string, data any) (quick.Config, error) {
+ return quick.LoadConfig(configFile, globalEtcdClient, data)
+}
+
+func readConfigWithoutMigrate(ctx context.Context, objAPI ObjectLayer) (config.Config, error) {
+ // Construct path to config.json for the given bucket.
+ configFile := path.Join(minioConfigPrefix, minioConfigFile)
+
+ configFiles := []string{
+ getConfigFile(),
+ getConfigFile() + ".deprecated",
+ configFile,
+ }
+
+ newServerCfg := func() (config.Config, error) {
+ // Initialize server config.
+ srvCfg := newServerConfig()
+
+ return srvCfg, saveServerConfig(ctx, objAPI, srvCfg)
+ }
+
+ var data []byte
+ var err error
+
+ cfg := &serverConfigV33{}
+ for _, cfgFile := range configFiles {
+ if _, err = Load(cfgFile, cfg); err != nil {
+ if !osIsNotExist(err) && !osIsPermission(err) {
+ return nil, err
+ }
+ continue
+ }
+ data, _ = json.Marshal(cfg)
+ break
+ }
+ if osIsPermission(err) {
+ logger.Info("Older config found but is not readable %s, proceeding to read config from other places", err)
+ }
+ if osIsNotExist(err) || osIsPermission(err) || len(data) == 0 {
+ data, err = readConfig(GlobalContext, objAPI, configFile)
+ if err != nil {
+ // when config.json is not found, then we freshly initialize.
+ if errors.Is(err, errConfigNotFound) {
+ return newServerCfg()
+ }
+ return nil, err
+ }
+
+ data, err = decryptData(data, configFile)
+ if err != nil {
+ return nil, err
+ }
+
+ newCfg, err := readServerConfig(GlobalContext, objAPI, data)
+ if err == nil {
+ return newCfg, nil
+ }
+
+ // Read older `.minio.sys/config/config.json`, if not
+ // possible just fail.
+ if err = json.Unmarshal(data, cfg); err != nil {
+ // Unable to parse old JSON simply re-initialize a new one.
+ return newServerCfg()
+ }
+ }
+
+ if !globalCredViaEnv && cfg.Credential.IsValid() {
+ // Preserve older credential if we do not have
+ // root credentials set via environment variable.
+ globalActiveCred = cfg.Credential
+ }
+
+ // Init compression config. For future migration, Compression config needs to be copied over from previous version.
+ switch cfg.Version {
+ case "29":
+ // V29 -> V30
+ cfg.Compression.Enabled = false
+ cfg.Compression.Extensions = strings.Split(compress.DefaultExtensions, config.ValueSeparator)
+ cfg.Compression.MimeTypes = strings.Split(compress.DefaultMimeTypes, config.ValueSeparator)
+ case "30":
+ // V30 -> V31
+ cfg.OpenID = openid.Config{}
+ cfg.Policy.OPA = opa.Args{
+ URL: &xnet.URL{},
+ AuthToken: "",
+ }
+ case "31":
+ // V31 -> V32
+ cfg.Notify.NSQ = make(map[string]target.NSQArgs)
+ cfg.Notify.NSQ["1"] = target.NSQArgs{}
+ }
+
+ // Move to latest.
+ cfg.Version = "33"
+
+ newCfg := newServerConfig()
+
+ config.SetRegion(newCfg, cfg.Region)
+ storageclass.SetStorageClass(newCfg, cfg.StorageClass)
+
+ for k, loggerArgs := range cfg.Logger.HTTP {
+ logger.SetLoggerHTTP(newCfg, k, loggerArgs)
+ }
+ for k, auditArgs := range cfg.Logger.AuditWebhook {
+ logger.SetLoggerHTTPAudit(newCfg, k, auditArgs)
+ }
+
+ xldap.SetIdentityLDAP(newCfg, cfg.LDAPServerConfig)
+ opa.SetPolicyOPAConfig(newCfg, cfg.Policy.OPA)
+ compress.SetCompressionConfig(newCfg, cfg.Compression)
+
+ for k, args := range cfg.Notify.AMQP {
+ notify.SetNotifyAMQP(newCfg, k, args)
+ }
+ for k, args := range cfg.Notify.Elasticsearch {
+ notify.SetNotifyES(newCfg, k, args)
+ }
+ for k, args := range cfg.Notify.Kafka {
+ notify.SetNotifyKafka(newCfg, k, args)
+ }
+ for k, args := range cfg.Notify.MQTT {
+ notify.SetNotifyMQTT(newCfg, k, args)
+ }
+ for k, args := range cfg.Notify.MySQL {
+ notify.SetNotifyMySQL(newCfg, k, args)
+ }
+ for k, args := range cfg.Notify.NATS {
+ notify.SetNotifyNATS(newCfg, k, args)
+ }
+ for k, args := range cfg.Notify.NSQ {
+ notify.SetNotifyNSQ(newCfg, k, args)
+ }
+ for k, args := range cfg.Notify.PostgreSQL {
+ notify.SetNotifyPostgres(newCfg, k, args)
+ }
+ for k, args := range cfg.Notify.Redis {
+ notify.SetNotifyRedis(newCfg, k, args)
+ }
+ for k, args := range cfg.Notify.Webhook {
+ notify.SetNotifyWebhook(newCfg, k, args)
+ }
+
+ return newCfg, nil
+}
diff --git a/cmd/config-versions.go b/cmd/config-versions.go
new file mode 100644
index 00000000..020bfa44
--- /dev/null
+++ b/cmd/config-versions.go
@@ -0,0 +1,79 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "github.com/minio/minio/internal/auth"
+ "github.com/minio/minio/internal/config"
+ "github.com/minio/minio/internal/config/compress"
+ xldap "github.com/minio/minio/internal/config/identity/ldap"
+ "github.com/minio/minio/internal/config/identity/openid"
+ "github.com/minio/minio/internal/config/notify"
+ "github.com/minio/minio/internal/config/policy/opa"
+ "github.com/minio/minio/internal/config/storageclass"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/quick"
+)
+
+// FileLogger is introduced to workaround the dependency about logrus
+type FileLogger struct {
+ Enable bool `json:"enable"`
+ Filename string `json:"filename"`
+}
+
+// ConsoleLogger is introduced to workaround the dependency about logrus
+type ConsoleLogger struct {
+ Enable bool `json:"enable"`
+}
+
+// serverConfigV33 is just like version '32', removes clientID from NATS and MQTT, and adds queueDir, queueLimit in all notification targets.
+type serverConfigV33 struct {
+ quick.Config `json:"-"` // ignore interfaces
+
+ Version string `json:"version"`
+
+ // S3 API configuration.
+ Credential auth.Credentials `json:"credential"`
+ Region string `json:"region"`
+ Worm config.BoolFlag `json:"worm"`
+
+ // Storage class configuration
+ StorageClass storageclass.Config `json:"storageclass"`
+
+ // Notification queue configuration.
+ Notify notify.Config `json:"notify"`
+
+ // Logger configuration
+ Logger logger.Config `json:"logger"`
+
+ // Compression configuration
+ Compression compress.Config `json:"compress"`
+
+ // OpenID configuration
+ OpenID openid.Config `json:"openid"`
+
+ // External policy enforcements.
+ Policy struct {
+ // OPA configuration.
+ OPA opa.Args `json:"opa"`
+
+ // Add new external policy enforcements here.
+ } `json:"policy"`
+
+ LDAPServerConfig xldap.LegacyConfig `json:"ldapserverconfig"`
+}
diff --git a/cmd/config.go b/cmd/config.go
new file mode 100644
index 00000000..7311d628
--- /dev/null
+++ b/cmd/config.go
@@ -0,0 +1,227 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "path"
+ "sort"
+ "strings"
+
+ jsoniter "github.com/json-iterator/go"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/config"
+ "github.com/minio/minio/internal/kms"
+)
+
+const (
+ minioConfigPrefix = "config"
+ minioConfigBucket = minioMetaBucket + SlashSeparator + minioConfigPrefix
+ kvPrefix = ".kv"
+
+ // Captures all the previous SetKV operations and allows rollback.
+ minioConfigHistoryPrefix = minioConfigPrefix + "/history"
+
+ // MinIO configuration file.
+ minioConfigFile = "config.json"
+)
+
+func listServerConfigHistory(ctx context.Context, objAPI ObjectLayer, withData bool, count int) (
+ []madmin.ConfigHistoryEntry, error,
+) {
+ var configHistory []madmin.ConfigHistoryEntry
+
+ // List all kvs
+ marker := ""
+ for {
+ res, err := objAPI.ListObjects(ctx, minioMetaBucket, minioConfigHistoryPrefix, marker, "", maxObjectList)
+ if err != nil {
+ return nil, err
+ }
+ for _, obj := range res.Objects {
+ cfgEntry := madmin.ConfigHistoryEntry{
+ RestoreID: strings.TrimSuffix(path.Base(obj.Name), kvPrefix),
+ CreateTime: obj.ModTime, // ModTime is createTime for config history entries.
+ }
+ if withData {
+ data, err := readConfig(ctx, objAPI, obj.Name)
+ if err != nil {
+ // ignore history file if not readable.
+ continue
+ }
+
+ data, err = decryptData(data, obj.Name)
+ if err != nil {
+ // ignore history file that cannot be loaded.
+ continue
+ }
+
+ cfgEntry.Data = string(data)
+ }
+ configHistory = append(configHistory, cfgEntry)
+ count--
+ if count == 0 {
+ break
+ }
+ }
+ if !res.IsTruncated {
+ // We are done here
+ break
+ }
+ marker = res.NextMarker
+ }
+ sort.Slice(configHistory, func(i, j int) bool {
+ return configHistory[i].CreateTime.Before(configHistory[j].CreateTime)
+ })
+ return configHistory, nil
+}
+
+func delServerConfigHistory(ctx context.Context, objAPI ObjectLayer, uuidKV string) error {
+ historyFile := pathJoin(minioConfigHistoryPrefix, uuidKV+kvPrefix)
+ _, err := objAPI.DeleteObject(ctx, minioMetaBucket, historyFile, ObjectOptions{
+ DeletePrefix: true,
+ DeletePrefixObject: true, // use prefix delete on exact object (this is an optimization to avoid fan-out calls)
+ })
+ return err
+}
+
+func readServerConfigHistory(ctx context.Context, objAPI ObjectLayer, uuidKV string) ([]byte, error) {
+ historyFile := pathJoin(minioConfigHistoryPrefix, uuidKV+kvPrefix)
+ data, err := readConfig(ctx, objAPI, historyFile)
+ if err != nil {
+ return nil, err
+ }
+
+ return decryptData(data, historyFile)
+}
+
+func saveServerConfigHistory(ctx context.Context, objAPI ObjectLayer, kv []byte) error {
+ uuidKV := mustGetUUID() + kvPrefix
+ historyFile := pathJoin(minioConfigHistoryPrefix, uuidKV)
+
+ if GlobalKMS != nil {
+ var err error
+ kv, err = config.EncryptBytes(GlobalKMS, kv, kms.Context{
+ minioMetaBucket: path.Join(minioMetaBucket, historyFile),
+ })
+ if err != nil {
+ return err
+ }
+ }
+ return saveConfig(ctx, objAPI, historyFile, kv)
+}
+
+func saveServerConfig(ctx context.Context, objAPI ObjectLayer, cfg any) error {
+ data, err := json.Marshal(cfg)
+ if err != nil {
+ return err
+ }
+
+ configFile := path.Join(minioConfigPrefix, minioConfigFile)
+ if GlobalKMS != nil {
+ data, err = config.EncryptBytes(GlobalKMS, data, kms.Context{
+ minioMetaBucket: path.Join(minioMetaBucket, configFile),
+ })
+ if err != nil {
+ return err
+ }
+ }
+ return saveConfig(ctx, objAPI, configFile, data)
+}
+
+// data is optional. If nil it will be loaded from backend.
+func readServerConfig(ctx context.Context, objAPI ObjectLayer, data []byte) (config.Config, error) {
+ srvCfg := config.New()
+ var err error
+ if len(data) == 0 {
+ configFile := path.Join(minioConfigPrefix, minioConfigFile)
+ data, err = readConfig(ctx, objAPI, configFile)
+ if err != nil {
+ if errors.Is(err, errConfigNotFound) {
+ lookupConfigs(srvCfg, objAPI)
+ return srvCfg, nil
+ }
+ return nil, err
+ }
+
+ data, err = decryptData(data, configFile)
+ if err != nil {
+ lookupConfigs(srvCfg, objAPI)
+ return nil, err
+ }
+ }
+
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ if err := json.Unmarshal(data, &srvCfg); err != nil {
+ return nil, err
+ }
+
+ // Add any missing entries
+ return srvCfg.Merge(), nil
+}
+
+// ConfigSys - config system.
+type ConfigSys struct{}
+
+// Init - initializes config system from config.json.
+func (sys *ConfigSys) Init(objAPI ObjectLayer) error {
+ if objAPI == nil {
+ return errInvalidArgument
+ }
+
+ return initConfig(objAPI)
+}
+
+// NewConfigSys - creates new config system object.
+func NewConfigSys() *ConfigSys {
+ return &ConfigSys{}
+}
+
+// Initialize and load config from remote etcd or local config directory
+func initConfig(objAPI ObjectLayer) (err error) {
+ bootstrapTraceMsg("load the configuration")
+ defer func() {
+ if err != nil {
+ bootstrapTraceMsg(fmt.Sprintf("loading configuration failed: %v", err))
+ }
+ }()
+
+ if objAPI == nil {
+ return errServerNotInitialized
+ }
+
+ srvCfg, err := readConfigWithoutMigrate(GlobalContext, objAPI)
+ if err != nil {
+ return err
+ }
+
+ bootstrapTraceMsg("lookup the configuration")
+
+ // Override any values from ENVs.
+ lookupConfigs(srvCfg, objAPI)
+
+ // hold the mutex lock before a new config is assigned.
+ globalServerConfigMu.Lock()
+ globalServerConfig = srvCfg
+ globalServerConfigMu.Unlock()
+
+ return nil
+}
diff --git a/cmd/consolelogger.go b/cmd/consolelogger.go
new file mode 100644
index 00000000..18488e19
--- /dev/null
+++ b/cmd/consolelogger.go
@@ -0,0 +1,205 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "container/ring"
+ "context"
+ "io"
+ "sync"
+ "sync/atomic"
+
+ "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/target/console"
+ types "github.com/minio/minio/internal/logger/target/loggertypes"
+ "github.com/minio/minio/internal/pubsub"
+ xnet "github.com/minio/pkg/v3/net"
+)
+
+// number of log messages to buffer
+const defaultLogBufferCount = 10000
+
+// HTTPConsoleLoggerSys holds global console logger state
+type HTTPConsoleLoggerSys struct {
+ totalMessages int64
+ failedMessages int64
+
+ sync.RWMutex
+ pubsub *pubsub.PubSub[log.Info, madmin.LogMask]
+ console *console.Target
+ nodeName string
+ logBuf *ring.Ring
+}
+
+// NewConsoleLogger - creates new HTTPConsoleLoggerSys with all nodes subscribed to
+// the console logging pub sub system
+func NewConsoleLogger(ctx context.Context, w io.Writer) *HTTPConsoleLoggerSys {
+ return &HTTPConsoleLoggerSys{
+ pubsub: pubsub.New[log.Info, madmin.LogMask](8),
+ console: console.New(w),
+ logBuf: ring.New(defaultLogBufferCount),
+ }
+}
+
+// IsOnline always true in case of console logger
+func (sys *HTTPConsoleLoggerSys) IsOnline(_ context.Context) bool {
+ return true
+}
+
+// SetNodeName - sets the node name if any after distributed setup has initialized
+func (sys *HTTPConsoleLoggerSys) SetNodeName(nodeName string) {
+ if !globalIsDistErasure {
+ sys.nodeName = ""
+ return
+ }
+
+ host, err := xnet.ParseHost(globalLocalNodeName)
+ if err != nil {
+ logger.FatalIf(err, "Unable to start console logging subsystem")
+ }
+
+ sys.nodeName = host.Name
+}
+
+// HasLogListeners returns true if console log listeners are registered
+// for this node or peers
+func (sys *HTTPConsoleLoggerSys) HasLogListeners() bool {
+ return sys != nil && sys.pubsub.Subscribers() > 0
+}
+
+// Subscribe starts console logging for this node.
+func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan log.Info, doneCh <-chan struct{}, node string, last int, logKind madmin.LogMask, filter func(entry log.Info) bool) error {
+ // Enable console logging for remote client.
+ if !sys.HasLogListeners() {
+ logger.AddSystemTarget(GlobalContext, sys)
+ }
+
+ cnt := 0
+ // by default send all console logs in the ring buffer unless node or limit query parameters
+ // are set.
+ var lastN []log.Info
+ if last > defaultLogBufferCount || last <= 0 {
+ last = defaultLogBufferCount
+ }
+
+ lastN = make([]log.Info, last)
+ sys.RLock()
+ sys.logBuf.Do(func(p any) {
+ if p != nil {
+ lg, ok := p.(log.Info)
+ if ok && lg.SendLog(node, logKind) {
+ lastN[cnt%last] = lg
+ cnt++
+ }
+ }
+ })
+ sys.RUnlock()
+ // send last n console log messages in order filtered by node
+ if cnt > 0 {
+ for i := range last {
+ entry := lastN[(cnt+i)%last]
+ if (entry == log.Info{}) {
+ continue
+ }
+ select {
+ case subCh <- entry:
+ case <-doneCh:
+ return nil
+ }
+ }
+ }
+ return sys.pubsub.Subscribe(madmin.LogMaskAll, subCh, doneCh, filter)
+}
+
+// Init if HTTPConsoleLoggerSys is valid, always returns nil right now
+func (sys *HTTPConsoleLoggerSys) Init(_ context.Context) error {
+ return nil
+}
+
+// Endpoint - dummy function for interface compatibility
+func (sys *HTTPConsoleLoggerSys) Endpoint() string {
+ return sys.console.Endpoint()
+}
+
+// String - stringer function for interface compatibility
+func (sys *HTTPConsoleLoggerSys) String() string {
+ return logger.ConsoleLoggerTgt
+}
+
+// Stats returns the target statistics.
+func (sys *HTTPConsoleLoggerSys) Stats() types.TargetStats {
+ return types.TargetStats{
+ TotalMessages: atomic.LoadInt64(&sys.totalMessages),
+ FailedMessages: atomic.LoadInt64(&sys.failedMessages),
+ QueueLength: 0,
+ }
+}
+
+// Content returns the console stdout log
+func (sys *HTTPConsoleLoggerSys) Content() (logs []log.Entry) {
+ sys.RLock()
+ sys.logBuf.Do(func(p any) {
+ if p != nil {
+ lg, ok := p.(log.Info)
+ if ok {
+ if (lg.Entry != log.Entry{}) {
+ logs = append(logs, lg.Entry)
+ }
+ }
+ }
+ })
+ sys.RUnlock()
+
+ return logs
+}
+
+// Cancel - cancels the target
+func (sys *HTTPConsoleLoggerSys) Cancel() {
+}
+
+// Type - returns type of the target
+func (sys *HTTPConsoleLoggerSys) Type() types.TargetType {
+ return types.TargetConsole
+}
+
+// Send log message 'e' to console and publish to console
+// log pubsub system
+func (sys *HTTPConsoleLoggerSys) Send(ctx context.Context, entry any) error {
+ var lg log.Info
+ switch e := entry.(type) {
+ case log.Entry:
+ lg = log.Info{Entry: e, NodeName: sys.nodeName}
+ case string:
+ lg = log.Info{ConsoleMsg: e, NodeName: sys.nodeName}
+ }
+ atomic.AddInt64(&sys.totalMessages, 1)
+
+ sys.pubsub.Publish(lg)
+ sys.Lock()
+ // add log to ring buffer
+ sys.logBuf.Value = lg
+ sys.logBuf = sys.logBuf.Next()
+ sys.Unlock()
+ err := sys.console.Send(entry)
+ if err != nil {
+ atomic.AddInt64(&sys.failedMessages, 1)
+ }
+ return err
+}
diff --git a/cmd/copy-part-range.go b/cmd/copy-part-range.go
new file mode 100644
index 00000000..1c2f84b0
--- /dev/null
+++ b/cmd/copy-part-range.go
@@ -0,0 +1,72 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "net/http"
+ "net/url"
+)
+
+// Writes S3 compatible copy part range error.
+func writeCopyPartErr(ctx context.Context, w http.ResponseWriter, err error, url *url.URL) {
+ switch err {
+ case errInvalidRange:
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidCopyPartRange), url)
+ return
+ case errInvalidRangeSource:
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidCopyPartRangeSource), url)
+ return
+ default:
+ apiErr := errorCodes.ToAPIErr(ErrInvalidCopyPartRangeSource)
+ apiErr.Description = err.Error()
+ writeErrorResponse(ctx, w, apiErr, url)
+ return
+ }
+}
+
+// Parses x-amz-copy-source-range for CopyObjectPart API. Its behavior
+// is different from regular HTTP range header. It only supports the
+// form `bytes=first-last` where first and last are zero-based byte
+// offsets. See
+// http://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadUploadPartCopy.html
+// for full details. This function treats an empty rangeString as
+// referring to the whole resource.
+func parseCopyPartRangeSpec(rangeString string) (hrange *HTTPRangeSpec, err error) {
+ hrange, err = parseRequestRangeSpec(rangeString)
+ if err != nil {
+ return nil, err
+ }
+ if hrange.IsSuffixLength || hrange.Start < 0 || hrange.End < 0 {
+ return nil, errInvalidRange
+ }
+ return hrange, nil
+}
+
+// checkCopyPartRangeWithSize adds more check to the range string in case of
+// copy object part. This API requires having specific start and end range values
+// e.g. 'bytes=3-10'. Other use cases will be rejected.
+func checkCopyPartRangeWithSize(rs *HTTPRangeSpec, resourceSize int64) (err error) {
+ if rs == nil {
+ return nil
+ }
+ if rs.IsSuffixLength || rs.Start >= resourceSize || rs.End >= resourceSize {
+ return errInvalidRangeSource
+ }
+ return nil
+}
diff --git a/cmd/copy-part-range_test.go b/cmd/copy-part-range_test.go
new file mode 100644
index 00000000..afe8cd0c
--- /dev/null
+++ b/cmd/copy-part-range_test.go
@@ -0,0 +1,95 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import "testing"
+
+// Test parseCopyPartRange()
+func TestParseCopyPartRangeSpec(t *testing.T) {
+ // Test success cases.
+ successCases := []struct {
+ rangeString string
+ offsetBegin int64
+ offsetEnd int64
+ }{
+ {"bytes=2-5", 2, 5},
+ {"bytes=2-9", 2, 9},
+ {"bytes=2-2", 2, 2},
+ {"bytes=0000-0006", 0, 6},
+ }
+ objectSize := int64(10)
+
+ for _, successCase := range successCases {
+ rs, err := parseCopyPartRangeSpec(successCase.rangeString)
+ if err != nil {
+ t.Fatalf("expected: , got: %s", err)
+ }
+
+ start, length, err1 := rs.GetOffsetLength(objectSize)
+ if err1 != nil {
+ t.Fatalf("expected: , got: %s", err1)
+ }
+
+ if start != successCase.offsetBegin {
+ t.Fatalf("expected: %d, got: %d", successCase.offsetBegin, start)
+ }
+
+ if start+length-1 != successCase.offsetEnd {
+ t.Fatalf("expected: %d, got: %d", successCase.offsetEnd, start+length-1)
+ }
+ }
+
+ // Test invalid range strings.
+ invalidRangeStrings := []string{
+ "bytes=8",
+ "bytes=5-2",
+ "bytes=+2-5",
+ "bytes=2-+5",
+ "bytes=2--5",
+ "bytes=-",
+ "2-5",
+ "bytes = 2-5",
+ "bytes=2 - 5",
+ "bytes=0-0,-1",
+ "bytes=2-5 ",
+ "bytes=-1",
+ "bytes=1-",
+ }
+ for _, rangeString := range invalidRangeStrings {
+ if _, err := parseCopyPartRangeSpec(rangeString); err == nil {
+ t.Fatalf("expected: an error, got: for range %s", rangeString)
+ }
+ }
+
+ // Test error range strings.
+ errorRangeString := []string{
+ "bytes=10-10",
+ "bytes=20-30",
+ }
+ for _, rangeString := range errorRangeString {
+ rs, err := parseCopyPartRangeSpec(rangeString)
+ if err == nil {
+ err1 := checkCopyPartRangeWithSize(rs, objectSize)
+ if err1 != errInvalidRangeSource {
+ t.Fatalf("expected: %s, got: %s", errInvalidRangeSource, err)
+ }
+ } else {
+ t.Fatalf("expected: %s, got: ", errInvalidRangeSource)
+ }
+ }
+}
diff --git a/cmd/crossdomain-xml-handler.go b/cmd/crossdomain-xml-handler.go
new file mode 100644
index 00000000..78cd9652
--- /dev/null
+++ b/cmd/crossdomain-xml-handler.go
@@ -0,0 +1,48 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import "net/http"
+
+// Standard cross domain policy information located at https://s3.amazonaws.com/crossdomain.xml
+const crossDomainXML = ``
+
+// Standard path where an app would find cross domain policy information.
+const crossDomainXMLEntity = "/crossdomain.xml"
+
+// A cross-domain policy file is an XML document that grants a web client, such as Adobe Flash Player
+// or Adobe Acrobat (though not necessarily limited to these), permission to handle data across domains.
+// When clients request content hosted on a particular source domain and that content make requests
+// directed towards a domain other than its own, the remote domain needs to host a cross-domain
+// policy file that grants access to the source domain, allowing the client to continue the transaction.
+func setCrossDomainPolicyMiddleware(h http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ cxml := crossDomainXML
+ if globalServerCtxt.CrossDomainXML != "" {
+ cxml = globalServerCtxt.CrossDomainXML
+ }
+ // Look for 'crossdomain.xml' in the incoming request.
+ if r.URL.Path == crossDomainXMLEntity {
+ // Write the standard cross domain policy xml.
+ w.Write([]byte(cxml))
+ // Request completed, no need to serve to other handlers.
+ return
+ }
+ h.ServeHTTP(w, r)
+ })
+}
diff --git a/cmd/crossdomain-xml-handler_test.go b/cmd/crossdomain-xml-handler_test.go
new file mode 100644
index 00000000..a5f0cb5a
--- /dev/null
+++ b/cmd/crossdomain-xml-handler_test.go
@@ -0,0 +1,42 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/minio/mux"
+)
+
+// Test cross domain xml handler.
+func TestCrossXMLHandler(t *testing.T) {
+ // Server initialization.
+ router := mux.NewRouter().SkipClean(true).UseEncodedPath()
+ handler := setCrossDomainPolicyMiddleware(router)
+ srv := httptest.NewServer(handler)
+
+ resp, err := http.Get(srv.URL + crossDomainXMLEntity)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if resp.StatusCode != http.StatusOK {
+ t.Fatal("Unexpected http status received", resp.Status)
+ }
+}
diff --git a/cmd/data-scanner-metric.go b/cmd/data-scanner-metric.go
new file mode 100644
index 00000000..db0bd21b
--- /dev/null
+++ b/cmd/data-scanner-metric.go
@@ -0,0 +1,344 @@
+// Copyright (c) 2015-2022 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+ "unsafe"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/bucket/lifecycle"
+)
+
+//go:generate stringer -type=scannerMetric -trimprefix=scannerMetric $GOFILE
+
+type scannerMetric uint8
+
+type scannerMetrics struct {
+ // All fields must be accessed atomically and aligned.
+ operations [scannerMetricLast]uint64
+ latency [scannerMetricLastRealtime]lockedLastMinuteLatency
+
+ // actions records actions performed.
+ actions [lifecycle.ActionCount]uint64
+ actionsLatency [lifecycle.ActionCount]lockedLastMinuteLatency
+
+ // currentPaths contains (string,*currentPathTracker) for each disk processing.
+ // Alignment not required.
+ currentPaths sync.Map
+
+ cycleInfoMu sync.Mutex
+ cycleInfo *currentScannerCycle
+}
+
+var globalScannerMetrics scannerMetrics
+
+const (
+ // START Realtime metrics, that only to records
+ // last minute latencies and total operation count.
+ scannerMetricReadMetadata scannerMetric = iota
+ scannerMetricCheckMissing
+ scannerMetricSaveUsage
+ scannerMetricApplyAll
+ scannerMetricApplyVersion
+ scannerMetricTierObjSweep
+ scannerMetricHealCheck
+ scannerMetricILM
+ scannerMetricCheckReplication
+ scannerMetricYield
+ scannerMetricCleanAbandoned
+ scannerMetricApplyNonCurrent
+ scannerMetricHealAbandonedVersion
+
+ // START Trace metrics:
+ scannerMetricStartTrace
+ scannerMetricScanObject // Scan object. All operations included.
+ scannerMetricHealAbandonedObject
+
+ // END realtime metrics:
+ scannerMetricLastRealtime
+
+ // Trace only metrics:
+ scannerMetricScanFolder // Scan a folder on disk, recursively.
+ scannerMetricScanCycle // Full cycle, cluster global
+ scannerMetricScanBucketDrive // Single bucket on one drive
+ scannerMetricCompactFolder // Folder compacted.
+
+ // Must be last:
+ scannerMetricLast
+)
+
+// log scanner action.
+// Use for s > scannerMetricStartTrace
+func (p *scannerMetrics) log(s scannerMetric, paths ...string) func(custom map[string]string) {
+ startTime := time.Now()
+ return func(custom map[string]string) {
+ duration := time.Since(startTime)
+
+ atomic.AddUint64(&p.operations[s], 1)
+ if s < scannerMetricLastRealtime {
+ p.latency[s].add(duration)
+ }
+
+ if s > scannerMetricStartTrace && globalTrace.NumSubscribers(madmin.TraceScanner) > 0 {
+ globalTrace.Publish(scannerTrace(s, startTime, duration, strings.Join(paths, " "), custom))
+ }
+ }
+}
+
+// time n scanner actions.
+// Use for s < scannerMetricLastRealtime
+func (p *scannerMetrics) timeN(s scannerMetric) func(n int) {
+ startTime := time.Now()
+ return func(n int) {
+ duration := time.Since(startTime)
+
+ atomic.AddUint64(&p.operations[s], uint64(n))
+ if s < scannerMetricLastRealtime {
+ p.latency[s].add(duration)
+ }
+ }
+}
+
+// time a scanner action.
+// Use for s < scannerMetricLastRealtime
+func (p *scannerMetrics) time(s scannerMetric) func() {
+ startTime := time.Now()
+ return func() {
+ duration := time.Since(startTime)
+
+ atomic.AddUint64(&p.operations[s], 1)
+ if s < scannerMetricLastRealtime {
+ p.latency[s].add(duration)
+ }
+ }
+}
+
+// timeSize add time and size of a scanner action.
+// Use for s < scannerMetricLastRealtime
+func (p *scannerMetrics) timeSize(s scannerMetric) func(sz int) {
+ startTime := time.Now()
+ return func(sz int) {
+ duration := time.Since(startTime)
+
+ atomic.AddUint64(&p.operations[s], 1)
+ if s < scannerMetricLastRealtime {
+ p.latency[s].addSize(duration, int64(sz))
+ }
+ }
+}
+
+// incTime will increment time on metric s with a specific duration.
+// Use for s < scannerMetricLastRealtime
+func (p *scannerMetrics) incTime(s scannerMetric, d time.Duration) {
+ atomic.AddUint64(&p.operations[s], 1)
+ if s < scannerMetricLastRealtime {
+ p.latency[s].add(d)
+ }
+}
+
+// timeILM times an ILM action.
+// lifecycle.NoneAction is ignored.
+// Use for s < scannerMetricLastRealtime
+func (p *scannerMetrics) timeILM(a lifecycle.Action) func(versions uint64) {
+ if a == lifecycle.NoneAction || a >= lifecycle.ActionCount {
+ return func(_ uint64) {}
+ }
+ startTime := time.Now()
+ return func(versions uint64) {
+ duration := time.Since(startTime)
+ atomic.AddUint64(&p.actions[a], versions)
+ p.actionsLatency[a].add(duration)
+ }
+}
+
+type currentPathTracker struct {
+ name *unsafe.Pointer // contains atomically accessed *string
+}
+
+// currentPathUpdater provides a lightweight update function for keeping track of
+// current objects for each disk.
+// Returns a function that can be used to update the current object
+// and a function to call to when processing finished.
+func (p *scannerMetrics) currentPathUpdater(disk, initial string) (update func(path string), done func()) {
+ initialPtr := unsafe.Pointer(&initial)
+ tracker := ¤tPathTracker{
+ name: &initialPtr,
+ }
+
+ p.currentPaths.Store(disk, tracker)
+ return func(path string) {
+ atomic.StorePointer(tracker.name, unsafe.Pointer(&path))
+ }, func() {
+ p.currentPaths.Delete(disk)
+ }
+}
+
+// getCurrentPaths returns the paths currently being processed.
+func (p *scannerMetrics) getCurrentPaths() []string {
+ var res []string
+ prefix := globalLocalNodeName + "/"
+ p.currentPaths.Range(func(key, value any) bool {
+ // We are a bit paranoid, but better miss an entry than crash.
+ name, ok := key.(string)
+ if !ok {
+ return true
+ }
+ obj, ok := value.(*currentPathTracker)
+ if !ok {
+ return true
+ }
+ strptr := (*string)(atomic.LoadPointer(obj.name))
+ if strptr != nil {
+ res = append(res, pathJoin(prefix, name, *strptr))
+ }
+ return true
+ })
+ return res
+}
+
+// activeDrives returns the number of currently active disks.
+// (since this is concurrent it may not be 100% reliable)
+func (p *scannerMetrics) activeDrives() int {
+ var i int
+ p.currentPaths.Range(func(k, v any) bool {
+ i++
+ return true
+ })
+ return i
+}
+
+// lifetime returns the lifetime count of the specified metric.
+func (p *scannerMetrics) lifetime(m scannerMetric) uint64 {
+ if m >= scannerMetricLast {
+ return 0
+ }
+ val := atomic.LoadUint64(&p.operations[m])
+ return val
+}
+
+// lastMinute returns the last minute statistics of a metric.
+// m should be < scannerMetricLastRealtime
+func (p *scannerMetrics) lastMinute(m scannerMetric) AccElem {
+ if m >= scannerMetricLastRealtime {
+ return AccElem{}
+ }
+ val := p.latency[m].total()
+ return val
+}
+
+// lifetimeActions returns the lifetime count of the specified ilm metric.
+func (p *scannerMetrics) lifetimeActions(a lifecycle.Action) uint64 {
+ if a == lifecycle.NoneAction || a >= lifecycle.ActionCount {
+ return 0
+ }
+ val := atomic.LoadUint64(&p.actions[a])
+ return val
+}
+
+// lastMinuteActions returns the last minute statistics of an ilm metric.
+func (p *scannerMetrics) lastMinuteActions(a lifecycle.Action) AccElem {
+ if a == lifecycle.NoneAction || a >= lifecycle.ActionCount {
+ return AccElem{}
+ }
+ val := p.actionsLatency[a].total()
+ return val
+}
+
+// setCycle updates the current cycle metrics.
+func (p *scannerMetrics) setCycle(c *currentScannerCycle) {
+ if c != nil {
+ c2 := c.clone()
+ c = &c2
+ }
+ p.cycleInfoMu.Lock()
+ p.cycleInfo = c
+ p.cycleInfoMu.Unlock()
+}
+
+// getCycle returns the current cycle metrics.
+// If not nil, the returned value can safely be modified.
+func (p *scannerMetrics) getCycle() *currentScannerCycle {
+ p.cycleInfoMu.Lock()
+ defer p.cycleInfoMu.Unlock()
+ if p.cycleInfo == nil {
+ return nil
+ }
+ c := p.cycleInfo.clone()
+ return &c
+}
+
+func (p *scannerMetrics) report() madmin.ScannerMetrics {
+ var m madmin.ScannerMetrics
+ cycle := p.getCycle()
+ if cycle != nil {
+ m.CurrentCycle = cycle.current
+ m.CyclesCompletedAt = cycle.cycleCompleted
+ m.CurrentStarted = cycle.started
+ }
+ m.CollectedAt = time.Now()
+ m.ActivePaths = p.getCurrentPaths()
+ m.LifeTimeOps = make(map[string]uint64, scannerMetricLast)
+ for i := range scannerMetricLast {
+ if n := atomic.LoadUint64(&p.operations[i]); n > 0 {
+ m.LifeTimeOps[i.String()] = n
+ }
+ }
+ if len(m.LifeTimeOps) == 0 {
+ m.LifeTimeOps = nil
+ }
+
+ m.LastMinute.Actions = make(map[string]madmin.TimedAction, scannerMetricLastRealtime)
+ for i := range scannerMetricLastRealtime {
+ lm := p.lastMinute(i)
+ if lm.N > 0 {
+ m.LastMinute.Actions[i.String()] = lm.asTimedAction()
+ }
+ }
+ if len(m.LastMinute.Actions) == 0 {
+ m.LastMinute.Actions = nil
+ }
+
+ // ILM
+ m.LifeTimeILM = make(map[string]uint64)
+ for i := lifecycle.NoneAction + 1; i < lifecycle.ActionCount; i++ {
+ if n := atomic.LoadUint64(&p.actions[i]); n > 0 {
+ m.LifeTimeILM[i.String()] = n
+ }
+ }
+ if len(m.LifeTimeILM) == 0 {
+ m.LifeTimeILM = nil
+ }
+
+ if len(m.LifeTimeILM) > 0 {
+ m.LastMinute.ILM = make(map[string]madmin.TimedAction, len(m.LifeTimeILM))
+ for i := lifecycle.NoneAction + 1; i < lifecycle.ActionCount; i++ {
+ lm := p.lastMinuteActions(i)
+ if lm.N > 0 {
+ m.LastMinute.ILM[i.String()] = madmin.TimedAction{Count: uint64(lm.N), AccTime: uint64(lm.Total)}
+ }
+ }
+ if len(m.LastMinute.ILM) == 0 {
+ m.LastMinute.ILM = nil
+ }
+ }
+ return m
+}
diff --git a/cmd/data-scanner.go b/cmd/data-scanner.go
new file mode 100644
index 00000000..288f4d71
--- /dev/null
+++ b/cmd/data-scanner.go
@@ -0,0 +1,1498 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/binary"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/fs"
+ "math"
+ "math/rand"
+ "os"
+ "path"
+ "strconv"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/bucket/lifecycle"
+ "github.com/minio/minio/internal/bucket/object/lock"
+ "github.com/minio/minio/internal/bucket/replication"
+ "github.com/minio/minio/internal/color"
+ "github.com/minio/minio/internal/config/heal"
+ "github.com/minio/minio/internal/event"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/pkg/v3/console"
+ uatomic "go.uber.org/atomic"
+)
+
+const (
+ dataScannerSleepPerFolder = time.Millisecond // Time to wait between folders.
+ dataUsageUpdateDirCycles = 16 // Visit all folders every n cycles.
+ dataScannerCompactLeastObject = 500 // Compact when there is less than this many objects in a branch.
+ dataScannerCompactAtChildren = 10000 // Compact when there are this many children in a branch.
+ dataScannerCompactAtFolders = dataScannerCompactAtChildren / 4 // Compact when this many subfolders in a single folder.
+ dataScannerForceCompactAtFolders = 250_000 // Compact when this many subfolders in a single folder (even top level).
+ dataScannerStartDelay = 1 * time.Minute // Time to wait on startup and between cycles.
+
+ healDeleteDangling = true
+ healObjectSelectProb = 1024 // Overall probability of a file being scanned; one in n.
+)
+
+var (
+ globalHealConfig heal.Config
+
+ // Sleeper values are updated when config is loaded.
+ scannerSleeper = newDynamicSleeper(2, time.Second, true) // Keep defaults same as config defaults
+ scannerCycle = uatomic.NewDuration(dataScannerStartDelay)
+ scannerIdleMode = uatomic.NewInt32(0) // default is throttled when idle
+ scannerExcessObjectVersions = uatomic.NewInt64(100)
+ scannerExcessObjectVersionsTotalSize = uatomic.NewInt64(1024 * 1024 * 1024 * 1024) // 1 TB
+ scannerExcessFolders = uatomic.NewInt64(50000)
+)
+
+// initDataScanner will start the scanner in the background.
+func initDataScanner(ctx context.Context, objAPI ObjectLayer) {
+ go func() {
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+ // Run the data scanner in a loop
+ for {
+ runDataScanner(ctx, objAPI)
+ duration := max(time.Duration(r.Float64()*float64(scannerCycle.Load())),
+ // Make sure to sleep at least a second to avoid high CPU ticks.
+ time.Second)
+ time.Sleep(duration)
+ }
+ }()
+}
+
+func getCycleScanMode(currentCycle, bitrotStartCycle uint64, bitrotStartTime time.Time) madmin.HealScanMode {
+ bitrotCycle := globalHealConfig.BitrotScanCycle()
+ switch bitrotCycle {
+ case -1:
+ return madmin.HealNormalScan
+ case 0:
+ return madmin.HealDeepScan
+ }
+
+ if currentCycle-bitrotStartCycle < healObjectSelectProb {
+ return madmin.HealDeepScan
+ }
+
+ if time.Since(bitrotStartTime) > bitrotCycle {
+ return madmin.HealDeepScan
+ }
+
+ return madmin.HealNormalScan
+}
+
+type backgroundHealInfo struct {
+ BitrotStartTime time.Time `json:"bitrotStartTime"`
+ BitrotStartCycle uint64 `json:"bitrotStartCycle"`
+ CurrentScanMode madmin.HealScanMode `json:"currentScanMode"`
+}
+
+func readBackgroundHealInfo(ctx context.Context, objAPI ObjectLayer) backgroundHealInfo {
+ if globalIsErasureSD {
+ return backgroundHealInfo{}
+ }
+
+ // Get last healing information
+ buf, err := readConfig(ctx, objAPI, backgroundHealInfoPath)
+ if err != nil {
+ if !errors.Is(err, errConfigNotFound) {
+ internalLogOnceIf(ctx, err, backgroundHealInfoPath)
+ }
+ return backgroundHealInfo{}
+ }
+ var info backgroundHealInfo
+ if err = json.Unmarshal(buf, &info); err != nil {
+ bugLogIf(ctx, err, backgroundHealInfoPath)
+ }
+ return info
+}
+
+func saveBackgroundHealInfo(ctx context.Context, objAPI ObjectLayer, info backgroundHealInfo) {
+ if globalIsErasureSD {
+ return
+ }
+
+ b, err := json.Marshal(info)
+ if err != nil {
+ bugLogIf(ctx, err)
+ return
+ }
+ // Get last healing information
+ err = saveConfig(ctx, objAPI, backgroundHealInfoPath, b)
+ if err != nil {
+ internalLogIf(ctx, err)
+ }
+}
+
+// runDataScanner will start a data scanner.
+// The function will block until the context is canceled.
+// There should only ever be one scanner running per cluster.
+func runDataScanner(ctx context.Context, objAPI ObjectLayer) {
+ ctx, cancel := globalLeaderLock.GetLock(ctx)
+ defer cancel()
+
+ // Load current bloom cycle
+ var cycleInfo currentScannerCycle
+
+ buf, _ := readConfig(ctx, objAPI, dataUsageBloomNamePath)
+ if len(buf) == 8 {
+ cycleInfo.next = binary.LittleEndian.Uint64(buf)
+ } else if len(buf) > 8 {
+ cycleInfo.next = binary.LittleEndian.Uint64(buf[:8])
+ buf = buf[8:]
+ _, err := cycleInfo.UnmarshalMsg(buf)
+ bugLogIf(ctx, err)
+ }
+
+ scannerTimer := time.NewTimer(scannerCycle.Load())
+ defer scannerTimer.Stop()
+ defer globalScannerMetrics.setCycle(nil)
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-scannerTimer.C:
+ // Reset the timer for next cycle.
+ // If scanner takes longer we start at once.
+ scannerTimer.Reset(scannerCycle.Load())
+
+ stopFn := globalScannerMetrics.log(scannerMetricScanCycle)
+ cycleInfo.current = cycleInfo.next
+ cycleInfo.started = time.Now()
+ globalScannerMetrics.setCycle(&cycleInfo)
+
+ bgHealInfo := readBackgroundHealInfo(ctx, objAPI)
+ scanMode := getCycleScanMode(cycleInfo.current, bgHealInfo.BitrotStartCycle, bgHealInfo.BitrotStartTime)
+ if bgHealInfo.CurrentScanMode != scanMode {
+ newHealInfo := bgHealInfo
+ newHealInfo.CurrentScanMode = scanMode
+ if scanMode == madmin.HealDeepScan {
+ newHealInfo.BitrotStartTime = time.Now().UTC()
+ newHealInfo.BitrotStartCycle = cycleInfo.current
+ }
+ saveBackgroundHealInfo(ctx, objAPI, newHealInfo)
+ }
+
+ // Wait before starting next cycle and wait on startup.
+ results := make(chan DataUsageInfo, 1)
+ go storeDataUsageInBackend(ctx, objAPI, results)
+ err := objAPI.NSScanner(ctx, results, uint32(cycleInfo.current), scanMode)
+ scannerLogIf(ctx, err)
+ res := map[string]string{"cycle": strconv.FormatUint(cycleInfo.current, 10)}
+ if err != nil {
+ res["error"] = err.Error()
+ }
+ stopFn(res)
+ if err == nil {
+ // Store new cycle...
+ cycleInfo.next++
+ cycleInfo.current = 0
+ cycleInfo.cycleCompleted = append(cycleInfo.cycleCompleted, time.Now())
+ if len(cycleInfo.cycleCompleted) > dataUsageUpdateDirCycles {
+ cycleInfo.cycleCompleted = cycleInfo.cycleCompleted[len(cycleInfo.cycleCompleted)-dataUsageUpdateDirCycles:]
+ }
+ globalScannerMetrics.setCycle(&cycleInfo)
+ tmp := make([]byte, 8, 8+cycleInfo.Msgsize())
+ // Cycle for backward compat.
+ binary.LittleEndian.PutUint64(tmp, cycleInfo.next)
+ tmp, _ = cycleInfo.MarshalMsg(tmp)
+ err = saveConfig(ctx, objAPI, dataUsageBloomNamePath, tmp)
+ if err != nil {
+ scannerLogIf(ctx, fmt.Errorf("%w, Object %s", err, dataUsageBloomNamePath))
+ }
+ }
+ }
+ }
+}
+
+type cachedFolder struct {
+ name string
+ parent *dataUsageHash
+ objectHealProbDiv uint32
+}
+
+type folderScanner struct {
+ root string
+ getSize getSizeFn
+ oldCache dataUsageCache
+ newCache dataUsageCache
+ updateCache dataUsageCache
+
+ dataUsageScannerDebug bool
+ healObjectSelect uint32 // Do a heal check on an object once every n cycles. Must divide into healFolderInclude
+ scanMode madmin.HealScanMode
+
+ weSleep func() bool
+ shouldHeal func() bool
+
+ disks []StorageAPI
+ disksQuorum int
+
+ // If set updates will be sent regularly to this channel.
+ // Will not be closed when returned.
+ updates chan<- dataUsageEntry
+ lastUpdate time.Time
+
+ // updateCurrentPath should be called whenever a new path is scanned.
+ updateCurrentPath func(string)
+}
+
+// Cache structure and compaction:
+//
+// A cache structure will be kept with a tree of usages.
+// The cache is a tree structure where each keeps track of its children.
+//
+// An uncompacted branch contains a count of the files only directly at the
+// branch level, and contains link to children branches or leaves.
+//
+// The leaves are "compacted" based on a number of properties.
+// A compacted leaf contains the totals of all files beneath it.
+//
+// A leaf is only scanned once every dataUsageUpdateDirCycles,
+// rarer if the bloom filter for the path is clean and no lifecycles are applied.
+// Skipped leaves have their totals transferred from the previous cycle.
+//
+// When selected there is a one in healObjectSelectProb that any object will be chosen for heal scan.
+//
+// Compaction happens when either:
+//
+// 1) The folder (and subfolders) contains less than dataScannerCompactLeastObject objects.
+// 2) The folder itself contains more than dataScannerCompactAtFolders folders.
+// 3) The folder only contains objects and no subfolders.
+//
+// A bucket root will never be compacted.
+//
+// Furthermore if a has more than dataScannerCompactAtChildren recursive children (uncompacted folders)
+// the tree will be recursively scanned and the branches with the least number of objects will be
+// compacted until the limit is reached.
+//
+// This ensures that any branch will never contain an unreasonable amount of other branches,
+// and also that small branches with few objects don't take up unreasonable amounts of space.
+// This keeps the cache size at a reasonable size for all buckets.
+//
+// Whenever a branch is scanned, it is assumed that it will be un-compacted
+// before it hits any of the above limits.
+// This will make the branch rebalance itself when scanned if the distribution of objects has changed.
+
+// scanDataFolder will scanner the basepath+cache.Info.Name and return an updated cache.
+// The returned cache will always be valid, but may not be updated from the existing.
+// Before each operation sleepDuration is called which can be used to temporarily halt the scanner.
+// If the supplied context is canceled the function will return at the first chance.
+func scanDataFolder(ctx context.Context, disks []StorageAPI, drive *xlStorage, cache dataUsageCache, getSize getSizeFn, scanMode madmin.HealScanMode, weSleep func() bool) (dataUsageCache, error) {
+ switch cache.Info.Name {
+ case "", dataUsageRoot:
+ return cache, errors.New("internal error: root scan attempted")
+ }
+ basePath := drive.drivePath
+ updatePath, closeDisk := globalScannerMetrics.currentPathUpdater(basePath, cache.Info.Name)
+ defer closeDisk()
+
+ s := folderScanner{
+ root: basePath,
+ getSize: getSize,
+ oldCache: cache,
+ newCache: dataUsageCache{Info: cache.Info},
+ updateCache: dataUsageCache{Info: cache.Info},
+ dataUsageScannerDebug: false,
+ healObjectSelect: 0,
+ scanMode: scanMode,
+ weSleep: weSleep,
+ updates: cache.Info.updates,
+ updateCurrentPath: updatePath,
+ disks: disks,
+ disksQuorum: len(disks) / 2,
+ }
+
+ var skipHeal atomic.Bool
+ if !globalIsErasure || cache.Info.SkipHealing {
+ skipHeal.Store(true)
+ }
+
+ // Check if we should do healing at all.
+ s.shouldHeal = func() bool {
+ if skipHeal.Load() {
+ return false
+ }
+ if s.healObjectSelect == 0 {
+ return false
+ }
+ if di, _ := drive.DiskInfo(ctx, DiskInfoOptions{}); di.Healing {
+ skipHeal.Store(true)
+ return false
+ }
+ return true
+ }
+
+ // Enable healing in XL mode.
+ if globalIsErasure && !cache.Info.SkipHealing {
+ // Do a heal check on an object once every n cycles. Must divide into healFolderInclude
+ s.healObjectSelect = healObjectSelectProb
+ }
+
+ done := ctx.Done()
+
+ // Read top level in bucket.
+ select {
+ case <-done:
+ return cache, ctx.Err()
+ default:
+ }
+ root := dataUsageEntry{}
+ folder := cachedFolder{name: cache.Info.Name, objectHealProbDiv: 1}
+ err := s.scanFolder(ctx, folder, &root)
+ if err != nil {
+ // No useful information...
+ return cache, err
+ }
+ s.newCache.forceCompact(dataScannerCompactAtChildren)
+ s.newCache.Info.LastUpdate = UTCNow()
+ s.newCache.Info.NextCycle = cache.Info.NextCycle
+ return s.newCache, nil
+}
+
+// sendUpdate() should be called on a regular basis when the newCache contains more recent total than previously.
+// May or may not send an update upstream.
+func (f *folderScanner) sendUpdate() {
+ // Send at most an update every minute.
+ if f.updates == nil || time.Since(f.lastUpdate) < time.Minute {
+ return
+ }
+ if flat := f.updateCache.sizeRecursive(f.newCache.Info.Name); flat != nil {
+ select {
+ case f.updates <- flat.clone():
+ default:
+ }
+ f.lastUpdate = time.Now()
+ }
+}
+
+// scanFolder will scan the provided folder.
+// Files found in the folders will be added to f.newCache.
+// If final is provided folders will be put into f.newFolders or f.existingFolders.
+// If final is not provided the folders found are returned from the function.
+func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, into *dataUsageEntry) error {
+ done := ctx.Done()
+ scannerLogPrefix := color.Green("folder-scanner:")
+
+ noWait := func() {}
+
+ thisHash := hashPath(folder.name)
+ // Store initial compaction state.
+ wasCompacted := into.Compacted
+
+ for {
+ select {
+ case <-done:
+ return ctx.Err()
+ default:
+ }
+ var abandonedChildren dataUsageHashMap
+ if !into.Compacted {
+ abandonedChildren = f.oldCache.findChildrenCopy(thisHash)
+ }
+
+ // If there are lifecycle rules for the prefix.
+ _, prefix := path2BucketObjectWithBasePath(f.root, folder.name)
+ var activeLifeCycle *lifecycle.Lifecycle
+ if f.oldCache.Info.lifeCycle != nil && f.oldCache.Info.lifeCycle.HasActiveRules(prefix) {
+ if f.dataUsageScannerDebug {
+ console.Debugf(scannerLogPrefix+" Prefix %q has active rules\n", prefix)
+ }
+ activeLifeCycle = f.oldCache.Info.lifeCycle
+ }
+ // If there are replication rules for the prefix.
+ var replicationCfg replicationConfig
+ if !f.oldCache.Info.replication.Empty() && f.oldCache.Info.replication.Config.HasActiveRules(prefix, true) {
+ replicationCfg = f.oldCache.Info.replication
+ }
+
+ if f.weSleep() {
+ scannerSleeper.Sleep(ctx, dataScannerSleepPerFolder)
+ }
+
+ var existingFolders, newFolders []cachedFolder
+ var foundObjects bool
+ err := readDirFn(pathJoin(f.root, folder.name), func(entName string, typ os.FileMode) error {
+ // Parse
+ entName = pathClean(pathJoin(folder.name, entName))
+ if entName == "" || entName == folder.name {
+ if f.dataUsageScannerDebug {
+ console.Debugf(scannerLogPrefix+" no entity (%s,%s)\n", f.root, entName)
+ }
+ return nil
+ }
+ bucket, prefix := path2BucketObjectWithBasePath(f.root, entName)
+ if bucket == "" {
+ if f.dataUsageScannerDebug {
+ console.Debugf(scannerLogPrefix+" no bucket (%s,%s)\n", f.root, entName)
+ }
+ return errDoneForNow
+ }
+
+ if isReservedOrInvalidBucket(bucket, false) {
+ if f.dataUsageScannerDebug {
+ console.Debugf(scannerLogPrefix+" invalid bucket: %v, entry: %v\n", bucket, entName)
+ }
+ return errDoneForNow
+ }
+
+ select {
+ case <-done:
+ return errDoneForNow
+ default:
+ }
+
+ if typ&os.ModeDir != 0 {
+ h := hashPath(entName)
+ _, exists := f.oldCache.Cache[h.Key()]
+ if h == thisHash {
+ return nil
+ }
+ this := cachedFolder{name: entName, parent: &thisHash, objectHealProbDiv: folder.objectHealProbDiv}
+ delete(abandonedChildren, h.Key()) // h.Key() already accounted for.
+ if exists {
+ existingFolders = append(existingFolders, this)
+ f.updateCache.copyWithChildren(&f.oldCache, h, &thisHash)
+ } else {
+ newFolders = append(newFolders, this)
+ }
+ return nil
+ }
+
+ wait := noWait
+ if f.weSleep() {
+ // Dynamic time delay.
+ wait = scannerSleeper.Timer(ctx)
+ }
+
+ // Get file size, ignore errors.
+ item := scannerItem{
+ Path: pathJoin(f.root, entName),
+ Typ: typ,
+ bucket: bucket,
+ prefix: path.Dir(prefix),
+ objectName: path.Base(entName),
+ debug: f.dataUsageScannerDebug,
+ lifeCycle: activeLifeCycle,
+ replication: replicationCfg,
+ }
+
+ item.heal.enabled = thisHash.modAlt(f.oldCache.Info.NextCycle/folder.objectHealProbDiv, f.healObjectSelect/folder.objectHealProbDiv) && f.shouldHeal()
+ item.heal.bitrot = f.scanMode == madmin.HealDeepScan
+
+ sz, err := f.getSize(item)
+ if err != nil && err != errIgnoreFileContrib {
+ wait() // wait to proceed to next entry.
+ if err != errSkipFile && f.dataUsageScannerDebug {
+ console.Debugf(scannerLogPrefix+" getSize \"%v/%v\" returned err: %v\n", bucket, item.objectPath(), err)
+ }
+ return nil
+ }
+
+ // successfully read means we have a valid object.
+ foundObjects = true
+ // Remove filename i.e is the meta file to construct object name
+ item.transformMetaDir()
+
+ // Object already accounted for, remove from heal map,
+ // simply because getSize() function already heals the
+ // object.
+ delete(abandonedChildren, pathJoin(item.bucket, item.objectPath()))
+
+ if err != errIgnoreFileContrib {
+ into.addSizes(sz)
+ into.Objects++
+ }
+
+ wait() // wait to proceed to next entry.
+
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ if foundObjects && globalIsErasure {
+ // If we found an object in erasure mode, we skip subdirs (only datadirs)...
+ break
+ }
+
+ // If we have many subfolders, compact ourself.
+ shouldCompact := f.newCache.Info.Name != folder.name &&
+ len(existingFolders)+len(newFolders) >= dataScannerCompactAtFolders ||
+ len(existingFolders)+len(newFolders) >= dataScannerForceCompactAtFolders
+
+ if totalFolders := len(existingFolders) + len(newFolders); totalFolders > int(scannerExcessFolders.Load()) {
+ prefixName := strings.TrimSuffix(folder.name, "/") + "/"
+ sendEvent(eventArgs{
+ EventName: event.PrefixManyFolders,
+ BucketName: f.root,
+ Object: ObjectInfo{
+ Name: prefixName,
+ Size: int64(totalFolders),
+ },
+ UserAgent: "Scanner",
+ Host: globalMinioHost,
+ })
+ auditLogInternal(context.Background(), AuditLogOptions{
+ Event: "scanner:manyprefixes",
+ APIName: "Scanner",
+ Bucket: f.root,
+ Object: prefixName,
+ Tags: map[string]string{
+ "x-minio-prefixes-total": strconv.Itoa(totalFolders),
+ },
+ })
+ }
+ if !into.Compacted && shouldCompact {
+ into.Compacted = true
+ newFolders = append(newFolders, existingFolders...)
+ existingFolders = nil
+ if f.dataUsageScannerDebug {
+ console.Debugf(scannerLogPrefix+" Preemptively compacting: %v, entries: %v\n", folder.name, len(existingFolders)+len(newFolders))
+ }
+ }
+
+ scanFolder := func(folder cachedFolder) {
+ if contextCanceled(ctx) {
+ return
+ }
+ dst := into
+ if !into.Compacted {
+ dst = &dataUsageEntry{Compacted: false}
+ }
+ if err := f.scanFolder(ctx, folder, dst); err != nil {
+ return
+ }
+ if !into.Compacted {
+ h := dataUsageHash(folder.name)
+ into.addChild(h)
+ // We scanned a folder, optionally send update.
+ f.updateCache.deleteRecursive(h)
+ f.updateCache.copyWithChildren(&f.newCache, h, folder.parent)
+ f.sendUpdate()
+ }
+ }
+
+ // Transfer existing
+ if !into.Compacted {
+ for _, folder := range existingFolders {
+ h := hashPath(folder.name)
+ f.updateCache.copyWithChildren(&f.oldCache, h, folder.parent)
+ }
+ }
+ // Scan new...
+ for _, folder := range newFolders {
+ h := hashPath(folder.name)
+ // Add new folders to the update tree so totals update for these.
+ if !into.Compacted {
+ var foundAny bool
+ parent := thisHash
+ for parent != hashPath(f.updateCache.Info.Name) {
+ e := f.updateCache.find(parent.Key())
+ if e == nil || e.Compacted {
+ foundAny = true
+ break
+ }
+ next := f.updateCache.searchParent(parent)
+ if next == nil {
+ foundAny = true
+ break
+ }
+ parent = *next
+ }
+ if !foundAny {
+ // Add non-compacted empty entry.
+ f.updateCache.replaceHashed(h, &thisHash, dataUsageEntry{})
+ }
+ }
+ f.updateCurrentPath(folder.name)
+ stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, folder.name)
+ scanFolder(folder)
+ stopFn(map[string]string{"type": "new"})
+
+ // Add new folders if this is new and we don't have existing.
+ if !into.Compacted {
+ parent := f.updateCache.find(thisHash.Key())
+ if parent != nil && !parent.Compacted {
+ f.updateCache.deleteRecursive(h)
+ f.updateCache.copyWithChildren(&f.newCache, h, &thisHash)
+ }
+ }
+ }
+
+ // Scan existing...
+ for _, folder := range existingFolders {
+ h := hashPath(folder.name)
+ // Check if we should skip scanning folder...
+ // We can only skip if we are not indexing into a compacted destination
+ // and the entry itself is compacted.
+ if !into.Compacted && f.oldCache.isCompacted(h) {
+ if !h.mod(f.oldCache.Info.NextCycle, dataUsageUpdateDirCycles) {
+ // Transfer and add as child...
+ f.newCache.copyWithChildren(&f.oldCache, h, folder.parent)
+ into.addChild(h)
+ 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)
+ stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, folder.name)
+ scanFolder(folder)
+ stopFn(map[string]string{"type": "existing"})
+ }
+
+ // Scan for healing
+ if len(abandonedChildren) == 0 || !f.shouldHeal() {
+ // If we are not heal scanning, return now.
+ break
+ }
+
+ if len(f.disks) == 0 || f.disksQuorum == 0 {
+ break
+ }
+
+ bgSeq, found := globalBackgroundHealState.getHealSequenceByToken(bgHealingUUID)
+ if !found {
+ break
+ }
+
+ // Whatever remains in 'abandonedChildren' are folders at this level
+ // that existed in the previous run but wasn't found now.
+ //
+ // This may be because of 2 reasons:
+ //
+ // 1) The folder/object was deleted.
+ // 2) We come from another disk and this disk missed the write.
+ //
+ // We therefore perform a heal check.
+ // If that doesn't bring it back we remove the folder and assume it was deleted.
+ // This means that the next run will not look for it.
+ // How to resolve results.
+ resolver := metadataResolutionParams{
+ dirQuorum: f.disksQuorum,
+ objQuorum: f.disksQuorum,
+ bucket: "",
+ strict: false,
+ }
+
+ healObjectsPrefix := color.Green("healObjects:")
+ for k := range abandonedChildren {
+ if !f.shouldHeal() {
+ break
+ }
+ bucket, prefix := path2BucketObject(k)
+ stopFn := globalScannerMetrics.time(scannerMetricCheckMissing)
+ f.updateCurrentPath(k)
+
+ if bucket != resolver.bucket {
+ // Bucket might be missing as well with abandoned children.
+ // make sure it is created first otherwise healing won't proceed
+ // for objects.
+ bgSeq.queueHealTask(healSource{
+ bucket: bucket,
+ }, madmin.HealItemBucket)
+ }
+
+ resolver.bucket = bucket
+
+ foundObjs := false
+ ctx, cancel := context.WithCancel(ctx)
+
+ err := listPathRaw(ctx, listPathRawOptions{
+ disks: f.disks,
+ bucket: bucket,
+ path: prefix,
+ recursive: true,
+ reportNotFound: true,
+ minDisks: f.disksQuorum,
+ agreed: func(entry metaCacheEntry) {
+ f.updateCurrentPath(entry.name)
+ if f.dataUsageScannerDebug {
+ console.Debugf(healObjectsPrefix+" got agreement: %v\n", entry.name)
+ }
+ },
+ // Some disks have data for this.
+ partial: func(entries metaCacheEntries, errs []error) {
+ if !f.shouldHeal() {
+ cancel()
+ return
+ }
+ entry, ok := entries.resolve(&resolver)
+ if !ok {
+ // check if we can get one entry at least
+ // proceed to heal nonetheless, since
+ // this object might be dangling.
+ entry, _ = entries.firstFound()
+ }
+ wait := noWait
+ if f.weSleep() {
+ // wait timer per object.
+ wait = scannerSleeper.Timer(ctx)
+ }
+ defer wait()
+ f.updateCurrentPath(entry.name)
+ stopFn := globalScannerMetrics.log(scannerMetricHealAbandonedObject, f.root, entry.name)
+ custom := make(map[string]string)
+ defer stopFn(custom)
+
+ if f.dataUsageScannerDebug {
+ console.Debugf(healObjectsPrefix+" resolved to: %v, dir: %v\n", entry.name, entry.isDir())
+ }
+
+ if entry.isDir() {
+ return
+ }
+
+ // We got an entry which we should be able to heal.
+ fiv, err := entry.fileInfoVersions(bucket)
+ if err != nil {
+ err := bgSeq.queueHealTask(healSource{
+ bucket: bucket,
+ object: entry.name,
+ versionID: "",
+ }, madmin.HealItemObject)
+ if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
+ scannerLogIf(ctx, err)
+ }
+ foundObjs = foundObjs || err == nil
+ return
+ }
+
+ custom["versions"] = fmt.Sprint(len(fiv.Versions))
+ var successVersions, failVersions int
+ for _, ver := range fiv.Versions {
+ stopFn := globalScannerMetrics.timeSize(scannerMetricHealAbandonedVersion)
+ err := bgSeq.queueHealTask(healSource{
+ bucket: bucket,
+ object: fiv.Name,
+ versionID: ver.VersionID,
+ }, madmin.HealItemObject)
+ stopFn(int(ver.Size))
+ if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
+ if err != nil {
+ scannerLogIf(ctx, fmt.Errorf("%w, Object %s/%s/%s", err, bucket, fiv.Name, ver.VersionID))
+ }
+ }
+ if err == nil {
+ successVersions++
+ } else {
+ failVersions++
+ }
+ foundObjs = foundObjs || err == nil
+ }
+ custom["success_versions"] = fmt.Sprint(successVersions)
+ custom["failed_versions"] = fmt.Sprint(failVersions)
+ },
+ // Too many disks failed.
+ finished: func(errs []error) {
+ if f.dataUsageScannerDebug {
+ console.Debugf(healObjectsPrefix+" too many errors: %v\n", errs)
+ }
+ cancel()
+ },
+ })
+
+ stopFn()
+ if f.dataUsageScannerDebug && err != nil && err != errFileNotFound {
+ console.Debugf(healObjectsPrefix+" checking returned value %v (%T)\n", err, err)
+ }
+
+ // Add unless healing returned an error.
+ if foundObjs {
+ this := cachedFolder{name: k, parent: &thisHash, objectHealProbDiv: 1}
+ stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, this.name, "HEALED")
+ scanFolder(this)
+ stopFn(map[string]string{"type": "healed"})
+ }
+ }
+ break
+ }
+ if !wasCompacted {
+ f.newCache.replaceHashed(thisHash, folder.parent, *into)
+ }
+
+ if !into.Compacted && f.newCache.Info.Name != folder.name {
+ flat := f.newCache.sizeRecursive(thisHash.Key())
+ flat.Compacted = true
+ var compact bool
+ if flat.Objects < dataScannerCompactLeastObject {
+ compact = true
+ } else {
+ // Compact if we only have objects as children...
+ compact = true
+ for k := range into.Children {
+ if v, ok := f.newCache.Cache[k]; ok {
+ if len(v.Children) > 0 || v.Objects > 1 {
+ compact = false
+ break
+ }
+ }
+ }
+ }
+
+ if compact {
+ stop := globalScannerMetrics.log(scannerMetricCompactFolder, folder.name)
+ f.newCache.deleteRecursive(thisHash)
+ f.newCache.replaceHashed(thisHash, folder.parent, *flat)
+ total := map[string]string{
+ "objects": strconv.FormatUint(flat.Objects, 10),
+ "size": strconv.FormatInt(flat.Size, 10),
+ }
+ if flat.Versions > 0 {
+ total["versions"] = strconv.FormatUint(flat.Versions, 10)
+ }
+ stop(total)
+ }
+ }
+ // Compact if too many children...
+ if !into.Compacted {
+ f.newCache.reduceChildrenOf(thisHash, dataScannerCompactAtChildren, f.newCache.Info.Name != folder.name)
+ }
+ if _, ok := f.updateCache.Cache[thisHash.Key()]; !wasCompacted && ok {
+ // Replace if existed before.
+ if flat := f.newCache.sizeRecursive(thisHash.Key()); flat != nil {
+ f.updateCache.deleteRecursive(thisHash)
+ f.updateCache.replaceHashed(thisHash, folder.parent, *flat)
+ }
+ }
+
+ return nil
+}
+
+// scannerItem represents each file while walking.
+type scannerItem struct {
+ Path string
+ bucket string // Bucket.
+ prefix string // Only the prefix if any, does not have final object name.
+ objectName string // Only the object name without prefixes.
+ replication replicationConfig
+ lifeCycle *lifecycle.Lifecycle
+ Typ fs.FileMode
+ heal struct {
+ enabled bool
+ bitrot bool
+ } // Has the object been selected for heal check?
+ debug bool
+}
+
+type sizeSummary struct {
+ totalSize int64
+ versions uint64
+ deleteMarkers uint64
+ replicatedSize int64
+ replicatedCount int64
+ pendingSize int64
+ failedSize int64
+ replicaSize int64
+ replicaCount int64
+ pendingCount uint64
+ failedCount uint64
+ replTargetStats map[string]replTargetSizeSummary
+ tiers map[string]tierStats
+}
+
+// replTargetSizeSummary holds summary of replication stats by target
+type replTargetSizeSummary struct {
+ replicatedSize int64
+ replicatedCount int64
+ pendingSize int64
+ failedSize int64
+ pendingCount uint64
+ failedCount uint64
+}
+
+type getSizeFn func(item scannerItem) (sizeSummary, error)
+
+// transformMetaDir will transform a directory to prefix/file.ext
+func (i *scannerItem) transformMetaDir() {
+ split := strings.Split(i.prefix, SlashSeparator)
+ if len(split) > 1 {
+ i.prefix = pathJoin(split[:len(split)-1]...)
+ } else {
+ i.prefix = ""
+ }
+ // Object name is last element
+ i.objectName = split[len(split)-1]
+}
+
+var applyActionsLogPrefix = color.Green("applyActions:")
+
+func (i *scannerItem) applyHealing(ctx context.Context, o ObjectLayer, oi ObjectInfo) (size int64) {
+ if i.debug {
+ if oi.VersionID != "" {
+ console.Debugf(applyActionsLogPrefix+" heal checking: %v/%v v(%s)\n", i.bucket, i.objectPath(), oi.VersionID)
+ } else {
+ console.Debugf(applyActionsLogPrefix+" heal checking: %v/%v\n", i.bucket, i.objectPath())
+ }
+ }
+ scanMode := madmin.HealNormalScan
+ if i.heal.bitrot {
+ scanMode = madmin.HealDeepScan
+ }
+ healOpts := madmin.HealOpts{
+ Remove: healDeleteDangling,
+ ScanMode: scanMode,
+ }
+ res, _ := o.HealObject(ctx, i.bucket, i.objectPath(), oi.VersionID, healOpts)
+ if res.ObjectSize > 0 {
+ return res.ObjectSize
+ }
+ return 0
+}
+
+func (i *scannerItem) alertExcessiveVersions(remainingVersions int, cumulativeSize int64) {
+ if remainingVersions >= int(scannerExcessObjectVersions.Load()) {
+ // Notify object accessed via a GET request.
+ sendEvent(eventArgs{
+ EventName: event.ObjectManyVersions,
+ BucketName: i.bucket,
+ Object: ObjectInfo{
+ Name: i.objectPath(),
+ },
+ UserAgent: "Scanner",
+ Host: globalLocalNodeName,
+ RespElements: map[string]string{"x-minio-versions": strconv.Itoa(remainingVersions)},
+ })
+
+ auditLogInternal(context.Background(), AuditLogOptions{
+ Event: "scanner:manyversions",
+ APIName: "Scanner",
+ Bucket: i.bucket,
+ Object: i.objectPath(),
+ Tags: map[string]string{
+ "x-minio-versions": strconv.Itoa(remainingVersions),
+ },
+ })
+ }
+
+ // Check if the cumulative size of all versions of this object is high.
+ if cumulativeSize >= scannerExcessObjectVersionsTotalSize.Load() {
+ // Notify object accessed via a GET request.
+ sendEvent(eventArgs{
+ EventName: event.ObjectLargeVersions,
+ BucketName: i.bucket,
+ Object: ObjectInfo{
+ Name: i.objectPath(),
+ },
+ UserAgent: "Scanner",
+ Host: globalLocalNodeName,
+ RespElements: map[string]string{
+ "x-minio-versions-count": strconv.Itoa(remainingVersions),
+ "x-minio-versions-size": strconv.FormatInt(cumulativeSize, 10),
+ },
+ })
+
+ auditLogInternal(context.Background(), AuditLogOptions{
+ Event: "scanner:largeversions",
+ APIName: "Scanner",
+ Bucket: i.bucket,
+ Object: i.objectPath(),
+ Tags: map[string]string{
+ "x-minio-versions-count": strconv.Itoa(remainingVersions),
+ "x-minio-versions-size": strconv.FormatInt(cumulativeSize, 10),
+ },
+ })
+ }
+}
+
+type actionsAccountingFn func(oi ObjectInfo, sz, actualSz int64, sizeS *sizeSummary)
+
+// applyActions will apply lifecycle checks on to a scanned item.
+// 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.
+// If no metadata is supplied, -1 is returned if no action is taken.
+func (i *scannerItem) applyActions(ctx context.Context, objAPI ObjectLayer, objInfos []ObjectInfo, lr lock.Retention, sizeS *sizeSummary, fn actionsAccountingFn) {
+ if len(objInfos) == 0 {
+ return
+ }
+ healActions := func(oi ObjectInfo, actualSz int64) int64 {
+ size := actualSz
+ if i.heal.enabled {
+ done := globalScannerMetrics.time(scannerMetricHealCheck)
+ size = i.applyHealing(ctx, objAPI, oi)
+ done()
+
+ if healDeleteDangling {
+ done := globalScannerMetrics.time(scannerMetricCleanAbandoned)
+ err := objAPI.CheckAbandonedParts(ctx, i.bucket, i.objectPath(), madmin.HealOpts{Remove: healDeleteDangling})
+ done()
+ if err != nil {
+ healingLogIf(ctx, fmt.Errorf("unable to check object %s/%s for abandoned data: %w", i.bucket, i.objectPath(), err), i.objectPath())
+ }
+ }
+ }
+
+ // replicate only if lifecycle rules are not applied.
+ done := globalScannerMetrics.time(scannerMetricCheckReplication)
+ i.healReplication(ctx, oi.Clone(), sizeS)
+ done()
+ return 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 {
+ event := lc.Eval(obj.ToLifecycleOpts())
+ if serverDebugLog {
+ console.Debugf(applyActionsLogPrefix+" lifecycle: Secondary scan: %v\n", event.Action)
+ }
+
+ switch event.Action {
+ case lifecycle.DeleteAllVersionsAction, lifecycle.DelMarkerDeleteAllVersionsAction:
+ // Skip if bucket has object locking enabled; To prevent the
+ // possibility of violating an object retention on one of the
+ // noncurrent versions of this object.
+ if lr.LockEnabled {
+ return lifecycle.Event{Action: lifecycle.NoneAction}
+ }
+
+ case lifecycle.DeleteVersionAction, lifecycle.DeleteRestoredVersionAction:
+ // Defensive code, should never happen
+ if obj.VersionID == "" {
+ return lifecycle.Event{Action: lifecycle.NoneAction}
+ }
+ if lr.LockEnabled && enforceRetentionForDeletion(ctx, obj) {
+ if serverDebugLog {
+ if obj.VersionID != "" {
+ console.Debugf(applyActionsLogPrefix+" lifecycle: %s v(%s) is locked, not deleting\n", obj.Name, obj.VersionID)
+ } else {
+ console.Debugf(applyActionsLogPrefix+" lifecycle: %s is locked, not deleting\n", obj.Name)
+ }
+ }
+ return lifecycle.Event{Action: lifecycle.NoneAction}
+ }
+ if rcfg != nil && !obj.VersionPurgeStatus.Empty() && rcfg.HasActiveRules(obj.Name, true) {
+ return lifecycle.Event{Action: lifecycle.NoneAction}
+ }
+ }
+
+ return event
+}
+
+func applyTransitionRule(event lifecycle.Event, src lcEventSrc, obj ObjectInfo) bool {
+ if obj.DeleteMarker || obj.IsDir {
+ return false
+ }
+ globalTransitionState.queueTransitionTask(obj, event, src)
+ return true
+}
+
+func applyExpiryOnTransitionedObject(ctx context.Context, objLayer ObjectLayer, obj ObjectInfo, lcEvent lifecycle.Event, src lcEventSrc) (ok bool) {
+ timeILM := globalScannerMetrics.timeILM(lcEvent.Action)
+ if err := expireTransitionedObject(ctx, objLayer, &obj, lcEvent, src); err != nil {
+ if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
+ return false
+ }
+ ilmLogIf(ctx, fmt.Errorf("expireTransitionedObject(%s, %s): %w", obj.Bucket, obj.Name, err))
+ return false
+ }
+ timeILM(1)
+
+ // Notification already sent in *expireTransitionedObject*, just return 'true' here.
+ return true
+}
+
+func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLayer, obj ObjectInfo, lcEvent lifecycle.Event, src lcEventSrc) (ok bool) {
+ traceFn := globalLifecycleSys.trace(obj)
+ opts := ObjectOptions{
+ Expiration: ExpirationOptions{Expire: true},
+ }
+
+ if lcEvent.Action.DeleteVersioned() {
+ opts.VersionID = obj.VersionID
+ }
+
+ opts.Versioned = globalBucketVersioningSys.PrefixEnabled(obj.Bucket, obj.Name)
+ opts.VersionSuspended = globalBucketVersioningSys.PrefixSuspended(obj.Bucket, obj.Name)
+
+ if lcEvent.Action.DeleteAll() {
+ opts.DeletePrefix = true
+ // use prefix delete on exact object (this is an optimization to avoid fan-out calls)
+ opts.DeletePrefixObject = true
+ }
+ var (
+ dobj ObjectInfo
+ err error
+ )
+
+ timeILM := globalScannerMetrics.timeILM(lcEvent.Action)
+ defer func() {
+ if !ok {
+ return
+ }
+
+ if lcEvent.Action != lifecycle.NoneAction {
+ numVersions := uint64(1)
+ if lcEvent.Action.DeleteAll() {
+ numVersions = uint64(obj.NumVersions)
+ }
+ timeILM(numVersions)
+ }
+ }()
+
+ dobj, err = objLayer.DeleteObject(ctx, obj.Bucket, encodeDirObject(obj.Name), opts)
+ if err != nil {
+ if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
+ traceFn(ILMExpiry, nil, nil)
+ return false
+ }
+ // Assume it is still there.
+ 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
+ }
+ if dobj.Name == "" {
+ dobj = obj
+ }
+
+ tags := newLifecycleAuditEvent(src, lcEvent).Tags()
+ tags["version-id"] = dobj.VersionID
+
+ // Send audit for the lifecycle delete operation
+ auditLogLifecycle(ctx, dobj, ILMExpiry, tags, traceFn)
+
+ eventName := event.ObjectRemovedDelete
+ if obj.DeleteMarker {
+ eventName = event.ObjectRemovedDeleteMarkerCreated
+ }
+ switch lcEvent.Action {
+ case lifecycle.DeleteAllVersionsAction:
+ eventName = event.ObjectRemovedDeleteAllVersions
+ case lifecycle.DelMarkerDeleteAllVersionsAction:
+ eventName = event.ILMDelMarkerExpirationDelete
+ }
+ // Notify object deleted event.
+ sendEvent(eventArgs{
+ EventName: eventName,
+ BucketName: dobj.Bucket,
+ Object: dobj,
+ UserAgent: "Internal: [ILM-Expiry]",
+ Host: globalLocalNodeName,
+ })
+
+ return true
+}
+
+// Apply object, object version, restored object or restored object version action on the given object
+func applyExpiryRule(event lifecycle.Event, src lcEventSrc, obj ObjectInfo) {
+ globalExpiryState.enqueueByDays(obj, event, src)
+}
+
+// objectPath returns the prefix and object name.
+func (i *scannerItem) objectPath() string {
+ return pathJoin(i.prefix, i.objectName)
+}
+
+// healReplication will heal a scanned item that has failed replication.
+func (i *scannerItem) healReplication(ctx context.Context, oi ObjectInfo, sizeS *sizeSummary) {
+ if oi.VersionID == "" {
+ return
+ }
+ if i.replication.Config == nil {
+ return
+ }
+ roi := queueReplicationHeal(ctx, oi.Bucket, oi, i.replication, 0)
+ if oi.DeleteMarker || !oi.VersionPurgeStatus.Empty() {
+ return
+ }
+
+ if sizeS.replTargetStats == nil && len(roi.TargetStatuses) > 0 {
+ sizeS.replTargetStats = make(map[string]replTargetSizeSummary)
+ }
+
+ for arn, tgtStatus := range roi.TargetStatuses {
+ tgtSizeS, ok := sizeS.replTargetStats[arn]
+ if !ok {
+ tgtSizeS = replTargetSizeSummary{}
+ }
+ switch tgtStatus {
+ case replication.Pending:
+ tgtSizeS.pendingCount++
+ tgtSizeS.pendingSize += oi.Size
+ sizeS.pendingCount++
+ sizeS.pendingSize += oi.Size
+ case replication.Failed:
+ tgtSizeS.failedSize += oi.Size
+ tgtSizeS.failedCount++
+ sizeS.failedSize += oi.Size
+ sizeS.failedCount++
+ case replication.Completed, replication.CompletedLegacy:
+ tgtSizeS.replicatedSize += oi.Size
+ tgtSizeS.replicatedCount++
+ sizeS.replicatedSize += oi.Size
+ sizeS.replicatedCount++
+ }
+ sizeS.replTargetStats[arn] = tgtSizeS
+ }
+
+ if oi.ReplicationStatus == replication.Replica {
+ sizeS.replicaSize += oi.Size
+ sizeS.replicaCount++
+ }
+}
+
+type dynamicSleeper struct {
+ mu sync.RWMutex
+
+ // Sleep factor
+ factor float64
+
+ // maximum sleep cap,
+ // set to <= 0 to disable.
+ maxSleep time.Duration
+
+ // Don't sleep at all, if time taken is below this value.
+ // This is to avoid too small costly sleeps.
+ minSleep time.Duration
+
+ // cycle will be closed
+ cycle chan struct{}
+
+ // isScanner should be set when this is used by the scanner
+ // to record metrics.
+ isScanner bool
+}
+
+// newDynamicSleeper
+func newDynamicSleeper(factor float64, maxWait time.Duration, isScanner bool) *dynamicSleeper {
+ return &dynamicSleeper{
+ factor: factor,
+ cycle: make(chan struct{}),
+ maxSleep: maxWait,
+ minSleep: 100 * time.Microsecond,
+ isScanner: isScanner,
+ }
+}
+
+// Timer returns a timer that has started.
+// When the returned function is called it will wait.
+func (d *dynamicSleeper) Timer(ctx context.Context) func() {
+ t := time.Now()
+ return func() {
+ doneAt := time.Now()
+ d.Sleep(ctx, doneAt.Sub(t))
+ }
+}
+
+// Sleep sleeps the specified time multiplied by the sleep factor.
+// If the factor is updated the sleep will be done again with the new factor.
+func (d *dynamicSleeper) Sleep(ctx context.Context, base time.Duration) {
+ for {
+ // Grab current values
+ d.mu.RLock()
+ minWait, maxWait := d.minSleep, d.maxSleep
+ factor := d.factor
+ cycle := d.cycle
+ d.mu.RUnlock()
+ // Don't sleep for really small amount of time
+ wantSleep := time.Duration(float64(base) * factor)
+ if wantSleep <= minWait {
+ return
+ }
+ if maxWait > 0 && wantSleep > maxWait {
+ wantSleep = maxWait
+ }
+ timer := time.NewTimer(wantSleep)
+ select {
+ case <-ctx.Done():
+ if !timer.Stop() {
+ <-timer.C
+ if d.isScanner {
+ globalScannerMetrics.incTime(scannerMetricYield, wantSleep)
+ }
+ }
+ return
+ case <-timer.C:
+ if d.isScanner {
+ globalScannerMetrics.incTime(scannerMetricYield, wantSleep)
+ }
+ return
+ case <-cycle:
+ if !timer.Stop() {
+ // We expired.
+ <-timer.C
+ if d.isScanner {
+ globalScannerMetrics.incTime(scannerMetricYield, wantSleep)
+ }
+ return
+ }
+ }
+ }
+}
+
+// Update the current settings and cycle all waiting.
+// Parameters are the same as in the constructor.
+func (d *dynamicSleeper) Update(factor float64, maxWait time.Duration) error {
+ d.mu.Lock()
+ defer d.mu.Unlock()
+ if math.Abs(d.factor-factor) < 1e-10 && d.maxSleep == maxWait {
+ return nil
+ }
+ // Update values and cycle waiting.
+ xioutil.SafeClose(d.cycle)
+ d.factor = factor
+ d.maxSleep = maxWait
+ d.cycle = make(chan struct{})
+ return nil
+}
+
+const (
+ // ILMExpiry - audit trail for ILM expiry
+ ILMExpiry = "ilm:expiry"
+ // ILMFreeVersionDelete - audit trail for ILM free-version delete
+ ILMFreeVersionDelete = "ilm:free-version-delete"
+ // ILMTransition - audit trail for ILM transitioning.
+ ILMTransition = " ilm:transition"
+)
+
+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
+ switch event {
+ case ILMExpiry:
+ apiName = "ILMExpiry"
+ case ILMFreeVersionDelete:
+ apiName = "ILMFreeVersionDelete"
+ case ILMTransition:
+ apiName = "ILMTransition"
+ }
+ auditLogInternal(ctx, AuditLogOptions{
+ Event: event,
+ APIName: apiName,
+ Bucket: oi.Bucket,
+ Object: oi.Name,
+ VersionID: oi.VersionID,
+ Tags: tags,
+ })
+ traceFn(event, tags, nil)
+}
diff --git a/cmd/data-scanner_test.go b/cmd/data-scanner_test.go
new file mode 100644
index 00000000..c45ad1e7
--- /dev/null
+++ b/cmd/data-scanner_test.go
@@ -0,0 +1,407 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "encoding/xml"
+ "fmt"
+ "slices"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/minio/minio/internal/amztime"
+ "github.com/minio/minio/internal/bucket/lifecycle"
+ objectlock "github.com/minio/minio/internal/bucket/object/lock"
+ "github.com/minio/minio/internal/bucket/replication"
+ "github.com/minio/minio/internal/bucket/versioning"
+ xhttp "github.com/minio/minio/internal/http"
+)
+
+func TestApplyNewerNoncurrentVersionsLimit(t *testing.T) {
+ // Prepare object layer
+ objAPI, disks, err := prepareErasure(t.Context(), 8)
+ if err != nil {
+ t.Fatalf("Failed to initialize object layer: %v", err)
+ }
+ defer removeRoots(disks)
+ setObjectLayer(objAPI)
+
+ // Prepare bucket metadata
+ globalBucketMetadataSys = NewBucketMetadataSys()
+ globalBucketObjectLockSys = &BucketObjectLockSys{}
+ globalBucketVersioningSys = &BucketVersioningSys{}
+
+ lcXML := `
+
+
+ max-versions
+ Enabled
+
+ 2
+
+
+
+ delete-all-versions
+ Enabled
+
+
+ del-all
+ true
+
+
+
+ 1
+ true
+
+
+
+`
+ lc, err := lifecycle.ParseLifecycleConfig(strings.NewReader(lcXML))
+ if err != nil {
+ t.Fatalf("Failed to unmarshal lifecycle config: %v", err)
+ }
+
+ vcfg := versioning.Versioning{
+ Status: "Enabled",
+ }
+ vcfgXML, err := xml.Marshal(vcfg)
+ if err != nil {
+ t.Fatalf("Failed to marshal versioning config: %v", err)
+ }
+
+ bucket := "bucket"
+ now := time.Now()
+ meta := BucketMetadata{
+ Name: bucket,
+ Created: now,
+ LifecycleConfigXML: []byte(lcXML),
+ VersioningConfigXML: vcfgXML,
+ VersioningConfigUpdatedAt: now,
+ LifecycleConfigUpdatedAt: now,
+ lifecycleConfig: lc,
+ versioningConfig: &vcfg,
+ }
+ globalBucketMetadataSys.Set(bucket, meta)
+ // Prepare lifecycle expiration workers
+ es := newExpiryState(t.Context(), objAPI, 0)
+ globalExpiryState = es
+
+ // Prepare object versions
+ obj := "obj-1"
+ // Simulate objects uploaded 30 hours ago
+ modTime := now.Add(-48 * time.Hour)
+ uuids := make([]uuid.UUID, 5)
+ for i := range uuids {
+ uuids[i] = uuid.UUID([16]byte{15: uint8(i + 1)})
+ }
+ 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 := range 5 {
+ fivs[i] = FileInfo{
+ Volume: bucket,
+ Name: obj,
+ VersionID: uuids[i].String(),
+ IsLatest: i == 0,
+ ModTime: modTime.Add(-1 * time.Duration(i) * time.Minute),
+ Size: 1 << 10,
+ NumVersions: 5,
+ }
+ objInfos[i] = fivs[i].ToObjectInfo(bucket, obj, true)
+ }
+ /*
+ lrObjInfos: objInfos with following modifications
+ version stack for obj-1
+ 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",
+ },
+ },
+ },
+ }
+ lr := objectlock.Retention{
+ Mode: objectlock.RetCompliance,
+ Validity: 12 * time.Hour,
+ LockEnabled: true,
+ }
+
+ expiryWorker := func(wg *sync.WaitGroup, readyCh chan<- struct{}, taskCh <-chan expiryOp, gotExpired *[]ObjectToDelete) {
+ defer wg.Done()
+ // signal the calling goroutine that the worker is ready tor receive tasks
+ close(readyCh)
+ var expired []ObjectToDelete
+ for t := range taskCh {
+ switch v := t.(type) {
+ 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}}},
+ },
+ {
+ // When no versions are present, in practice this could be an object with only free versions
+ objInfos: nil,
+ wants: nil,
+ wantExpired: nil,
+ },
+ }
+ 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(t.Context(), 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) {
+ // Tests cover only ExpiredObjectDeleteAllVersions and DelMarkerExpiration actions
+ numVersions := 4
+ obj := ObjectInfo{
+ Name: "foo",
+ ModTime: time.Now().Add(-31 * 24 * time.Hour),
+ Size: 100 << 20,
+ VersionID: uuid.New().String(),
+ IsLatest: true,
+ NumVersions: numVersions,
+ }
+ delMarker := ObjectInfo{
+ Name: "foo-deleted",
+ ModTime: time.Now().Add(-61 * 24 * time.Hour),
+ Size: 0,
+ VersionID: uuid.New().String(),
+ IsLatest: true,
+ DeleteMarker: true,
+ NumVersions: numVersions,
+ }
+
+ deleteAllILM := `
+
+
+ 30
+ true
+
+
+ Enabled
+ DeleteAllVersions
+
+ `
+ delMarkerILM := `
+
+ DelMarkerExpiration
+
+ Enabled
+
+ 60
+
+
+ `
+ deleteAllLc, err := lifecycle.ParseLifecycleConfig(strings.NewReader(deleteAllILM))
+ if err != nil {
+ t.Fatalf("Failed to parse deleteAllILM test ILM policy %v", err)
+ }
+ delMarkerLc, err := lifecycle.ParseLifecycleConfig(strings.NewReader(delMarkerILM))
+ if err != nil {
+ t.Fatalf("Failed to parse delMarkerILM test ILM policy %v", err)
+ }
+ tests := []struct {
+ ilm lifecycle.Lifecycle
+ retention *objectlock.Retention
+ obj ObjectInfo
+ want lifecycle.Action
+ }{
+ {
+ // with object locking
+ ilm: *deleteAllLc,
+ retention: &objectlock.Retention{LockEnabled: true},
+ obj: obj,
+ want: lifecycle.NoneAction,
+ },
+ {
+ // without object locking
+ ilm: *deleteAllLc,
+ retention: &objectlock.Retention{},
+ obj: obj,
+ want: lifecycle.DeleteAllVersionsAction,
+ },
+ {
+ // with object locking
+ ilm: *delMarkerLc,
+ retention: &objectlock.Retention{LockEnabled: true},
+ obj: delMarker,
+ want: lifecycle.NoneAction,
+ },
+ {
+ // without object locking
+ ilm: *delMarkerLc,
+ retention: &objectlock.Retention{},
+ obj: delMarker,
+ want: lifecycle.DelMarkerDeleteAllVersionsAction,
+ },
+ }
+
+ for i, test := range tests {
+ t.Run(fmt.Sprintf("TestEvalAction-%d", i), func(t *testing.T) {
+ gotEvent := evalActionFromLifecycle(t.Context(), test.ilm, *test.retention, nil, test.obj)
+ if gotEvent.Action != test.want {
+ t.Fatalf("Expected %v but got %v", test.want, gotEvent.Action)
+ }
+ })
+ }
+}
diff --git a/cmd/data-usage-cache.go b/cmd/data-usage-cache.go
new file mode 100644
index 00000000..5752daae
--- /dev/null
+++ b/cmd/data-usage-cache.go
@@ -0,0 +1,1323 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "maps"
+ "math/rand"
+ "net/http"
+ "path"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/cespare/xxhash/v2"
+ "github.com/dustin/go-humanize"
+ "github.com/klauspost/compress/zstd"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/bucket/lifecycle"
+ "github.com/tinylib/msgp/msgp"
+ "github.com/valyala/bytebufferpool"
+)
+
+//msgp:clearomitted
+
+//go:generate msgp -file $GOFILE -unexported
+
+// dataUsageHash is the hash type used.
+type dataUsageHash string
+
+// sizeHistogramV1 is size histogram V1, which has fewer intervals esp. between
+// 1024B and 1MiB.
+type sizeHistogramV1 [dataUsageBucketLenV1]uint64
+
+// sizeHistogram is a size histogram.
+type sizeHistogram [dataUsageBucketLen]uint64
+
+// versionsHistogram is a histogram of number of versions in an object.
+type versionsHistogram [dataUsageVersionLen]uint64
+
+type dataUsageEntry struct {
+ Children dataUsageHashMap `msg:"ch"`
+ // These fields do no include any children.
+ Size int64 `msg:"sz"`
+ Objects uint64 `msg:"os"`
+ Versions uint64 `msg:"vs"` // Versions that are not delete markers.
+ DeleteMarkers uint64 `msg:"dms"`
+ ObjSizes sizeHistogram `msg:"szs"`
+ ObjVersions versionsHistogram `msg:"vh"`
+ AllTierStats *allTierStats `msg:"ats,omitempty"`
+ Compacted bool `msg:"c"`
+}
+
+// allTierStats is a collection of per-tier stats across all configured remote
+// tiers.
+type allTierStats struct {
+ Tiers map[string]tierStats `msg:"ts"`
+}
+
+func newAllTierStats() *allTierStats {
+ return &allTierStats{
+ Tiers: make(map[string]tierStats),
+ }
+}
+
+func (ats *allTierStats) addSizes(tiers map[string]tierStats) {
+ for tier, st := range tiers {
+ ats.Tiers[tier] = ats.Tiers[tier].add(st)
+ }
+}
+
+func (ats *allTierStats) merge(other *allTierStats) {
+ for tier, st := range other.Tiers {
+ ats.Tiers[tier] = ats.Tiers[tier].add(st)
+ }
+}
+
+func (ats *allTierStats) clone() *allTierStats {
+ if ats == nil {
+ return nil
+ }
+ dst := *ats
+ dst.Tiers = make(map[string]tierStats, len(ats.Tiers))
+ maps.Copy(dst.Tiers, ats.Tiers)
+ return &dst
+}
+
+func (ats *allTierStats) populateStats(stats map[string]madmin.TierStats) {
+ if ats == nil {
+ return
+ }
+
+ // Update stats for tiers as they become available.
+ for tier, st := range ats.Tiers {
+ stats[tier] = madmin.TierStats{
+ TotalSize: st.TotalSize,
+ NumVersions: st.NumVersions,
+ NumObjects: st.NumObjects,
+ }
+ }
+}
+
+// tierStats holds per-tier stats of a remote tier.
+type tierStats struct {
+ TotalSize uint64 `msg:"ts"`
+ NumVersions int `msg:"nv"`
+ NumObjects int `msg:"no"`
+}
+
+func (ts tierStats) add(u tierStats) tierStats {
+ return tierStats{
+ TotalSize: ts.TotalSize + u.TotalSize,
+ NumVersions: ts.NumVersions + u.NumVersions,
+ NumObjects: ts.NumObjects + u.NumObjects,
+ }
+}
+
+//msgp:encode ignore dataUsageEntryV2 dataUsageEntryV3 dataUsageEntryV4 dataUsageEntryV5 dataUsageEntryV6 dataUsageEntryV7
+//msgp:marshal ignore dataUsageEntryV2 dataUsageEntryV3 dataUsageEntryV4 dataUsageEntryV5 dataUsageEntryV6 dataUsageEntryV7
+
+//msgp:tuple dataUsageEntryV2
+type dataUsageEntryV2 struct {
+ // These fields do no include any children.
+ Size int64
+ Objects uint64
+ ObjSizes sizeHistogram
+ Children dataUsageHashMap
+}
+
+//msgp:tuple dataUsageEntryV3
+type dataUsageEntryV3 struct {
+ // These fields do no include any children.
+ Size int64
+ Objects uint64
+ ObjSizes sizeHistogram
+ Children dataUsageHashMap
+}
+
+//msgp:tuple dataUsageEntryV4
+type dataUsageEntryV4 struct {
+ Children dataUsageHashMap
+ // These fields do no include any children.
+ Size int64
+ Objects uint64
+ ObjSizes sizeHistogram
+}
+
+//msgp:tuple dataUsageEntryV5
+type dataUsageEntryV5 struct {
+ Children dataUsageHashMap
+ // These fields do no include any children.
+ Size int64
+ Objects uint64
+ Versions uint64 // Versions that are not delete markers.
+ ObjSizes sizeHistogram
+ Compacted bool
+}
+
+//msgp:tuple dataUsageEntryV6
+type dataUsageEntryV6 struct {
+ Children dataUsageHashMap
+ // These fields do no include any children.
+ Size int64
+ Objects uint64
+ Versions uint64 // Versions that are not delete markers.
+ ObjSizes sizeHistogram
+ Compacted bool
+}
+
+type dataUsageEntryV7 struct {
+ Children dataUsageHashMap `msg:"ch"`
+ // These fields do no include any children.
+ Size int64 `msg:"sz"`
+ Objects uint64 `msg:"os"`
+ Versions uint64 `msg:"vs"` // Versions that are not delete markers.
+ DeleteMarkers uint64 `msg:"dms"`
+ ObjSizes sizeHistogramV1 `msg:"szs"`
+ ObjVersions versionsHistogram `msg:"vh"`
+ AllTierStats *allTierStats `msg:"ats,omitempty"`
+ Compacted bool `msg:"c"`
+}
+
+// dataUsageCache contains a cache of data usage entries latest version.
+type dataUsageCache struct {
+ Info dataUsageCacheInfo
+ Cache map[string]dataUsageEntry
+}
+
+//msgp:encode ignore dataUsageCacheV2 dataUsageCacheV3 dataUsageCacheV4 dataUsageCacheV5 dataUsageCacheV6 dataUsageCacheV7
+//msgp:marshal ignore dataUsageCacheV2 dataUsageCacheV3 dataUsageCacheV4 dataUsageCacheV5 dataUsageCacheV6 dataUsageCacheV7
+
+// dataUsageCacheV2 contains a cache of data usage entries version 2.
+type dataUsageCacheV2 struct {
+ Info dataUsageCacheInfo
+ Cache map[string]dataUsageEntryV2
+}
+
+// dataUsageCacheV3 contains a cache of data usage entries version 3.
+type dataUsageCacheV3 struct {
+ Info dataUsageCacheInfo
+ Cache map[string]dataUsageEntryV3
+}
+
+// dataUsageCacheV4 contains a cache of data usage entries version 4.
+type dataUsageCacheV4 struct {
+ Info dataUsageCacheInfo
+ Cache map[string]dataUsageEntryV4
+}
+
+// dataUsageCacheV5 contains a cache of data usage entries version 5.
+type dataUsageCacheV5 struct {
+ Info dataUsageCacheInfo
+ Cache map[string]dataUsageEntryV5
+}
+
+// dataUsageCacheV6 contains a cache of data usage entries version 6.
+type dataUsageCacheV6 struct {
+ Info dataUsageCacheInfo
+ Cache map[string]dataUsageEntryV6
+}
+
+// dataUsageCacheV7 contains a cache of data usage entries version 7.
+type dataUsageCacheV7 struct {
+ Info dataUsageCacheInfo
+ Cache map[string]dataUsageEntryV7
+}
+
+//msgp:ignore dataUsageEntryInfo
+type dataUsageEntryInfo struct {
+ Name string
+ Parent string
+ Entry dataUsageEntry
+}
+
+type dataUsageCacheInfo struct {
+ // Name of the bucket. Also root element.
+ Name string
+ NextCycle uint32
+ LastUpdate time.Time
+ // indicates if the disk is being healed and scanner
+ // should skip healing the disk
+ SkipHealing bool
+
+ // Active lifecycle, if any on the bucket
+ lifeCycle *lifecycle.Lifecycle `msg:"-"`
+
+ // optional updates channel.
+ // If set updates will be sent regularly to this channel.
+ // Will not be closed when returned.
+ updates chan<- dataUsageEntry `msg:"-"`
+ replication replicationConfig `msg:"-"`
+}
+
+func (e *dataUsageEntry) addSizes(summary sizeSummary) {
+ e.Size += summary.totalSize
+ e.Versions += summary.versions
+ e.DeleteMarkers += summary.deleteMarkers
+ e.ObjSizes.add(summary.totalSize)
+ e.ObjVersions.add(summary.versions)
+
+ if len(summary.tiers) != 0 {
+ if e.AllTierStats == nil {
+ e.AllTierStats = newAllTierStats()
+ }
+ e.AllTierStats.addSizes(summary.tiers)
+ }
+}
+
+// merge other data usage entry into this, excluding children.
+func (e *dataUsageEntry) merge(other dataUsageEntry) {
+ e.Objects += other.Objects
+ e.Versions += other.Versions
+ e.DeleteMarkers += other.DeleteMarkers
+ e.Size += other.Size
+
+ for i, v := range other.ObjSizes[:] {
+ e.ObjSizes[i] += v
+ }
+
+ for i, v := range other.ObjVersions[:] {
+ e.ObjVersions[i] += v
+ }
+
+ if other.AllTierStats != nil && len(other.AllTierStats.Tiers) != 0 {
+ if e.AllTierStats == nil {
+ e.AllTierStats = newAllTierStats()
+ }
+ e.AllTierStats.merge(other.AllTierStats)
+ }
+}
+
+// mod returns true if the hash mod cycles == cycle.
+// If cycles is 0 false is always returned.
+// If cycles is 1 true is always returned (as expected).
+func (h dataUsageHash) mod(cycle uint32, cycles uint32) bool {
+ if cycles <= 1 {
+ return cycles == 1
+ }
+ return uint32(xxhash.Sum64String(string(h)))%cycles == cycle%cycles
+}
+
+// modAlt returns true if the hash mod cycles == cycle.
+// This is out of sync with mod.
+// If cycles is 0 false is always returned.
+// If cycles is 1 true is always returned (as expected).
+func (h dataUsageHash) modAlt(cycle uint32, cycles uint32) bool {
+ if cycles <= 1 {
+ return cycles == 1
+ }
+ return uint32(xxhash.Sum64String(string(h))>>32)%(cycles) == cycle%cycles
+}
+
+// addChild will add a child based on its hash.
+// If it already exists it will not be added again.
+func (e *dataUsageEntry) addChild(hash dataUsageHash) {
+ if _, ok := e.Children[hash.Key()]; ok {
+ return
+ }
+ if e.Children == nil {
+ e.Children = make(dataUsageHashMap, 1)
+ }
+ e.Children[hash.Key()] = struct{}{}
+}
+
+// Create a clone of the entry.
+func (e dataUsageEntry) clone() dataUsageEntry {
+ // We operate on a copy from the receiver.
+ if e.Children != nil {
+ ch := make(dataUsageHashMap, len(e.Children))
+ maps.Copy(ch, e.Children)
+ e.Children = ch
+ }
+
+ if e.AllTierStats != nil {
+ e.AllTierStats = e.AllTierStats.clone()
+ }
+ return e
+}
+
+// find a path in the cache.
+// Returns nil if not found.
+func (d *dataUsageCache) find(path string) *dataUsageEntry {
+ due, ok := d.Cache[hashPath(path).Key()]
+ if !ok {
+ return nil
+ }
+ return &due
+}
+
+// isCompacted returns whether an entry is compacted.
+// Returns false if not found.
+func (d *dataUsageCache) isCompacted(h dataUsageHash) bool {
+ due, ok := d.Cache[h.Key()]
+ if !ok {
+ return false
+ }
+ return due.Compacted
+}
+
+// findChildrenCopy returns a copy of the children of the supplied hash.
+func (d *dataUsageCache) findChildrenCopy(h dataUsageHash) dataUsageHashMap {
+ ch := d.Cache[h.String()].Children
+ res := make(dataUsageHashMap, len(ch))
+ for k := range ch {
+ res[k] = struct{}{}
+ }
+ return res
+}
+
+// searchParent will search for the parent of h.
+// This is an O(N*N) operation if there is no parent or it cannot be guessed.
+func (d *dataUsageCache) searchParent(h dataUsageHash) *dataUsageHash {
+ want := h.Key()
+ if idx := strings.LastIndexByte(want, '/'); idx >= 0 {
+ if v := d.find(want[:idx]); v != nil {
+ _, ok := v.Children[want]
+ if ok {
+ found := hashPath(want[:idx])
+ return &found
+ }
+ }
+ }
+ for k, v := range d.Cache {
+ _, ok := v.Children[want]
+ if ok {
+ found := dataUsageHash(k)
+ return &found
+ }
+ }
+ return nil
+}
+
+// deleteRecursive will delete an entry recursively, but not change its parent.
+func (d *dataUsageCache) deleteRecursive(h dataUsageHash) {
+ if existing, ok := d.Cache[h.String()]; ok {
+ // Delete first if there should be a loop.
+ delete(d.Cache, h.Key())
+ for child := range existing.Children {
+ d.deleteRecursive(dataUsageHash(child))
+ }
+ }
+}
+
+// dui converts the flattened version of the path to madmin.DataUsageInfo.
+// As a side effect d will be flattened, use a clone if this is not ok.
+func (d *dataUsageCache) dui(path string, buckets []BucketInfo) DataUsageInfo {
+ e := d.find(path)
+ if e == nil {
+ // No entry found, return empty.
+ return DataUsageInfo{}
+ }
+ flat := d.flatten(*e)
+ dui := DataUsageInfo{
+ LastUpdate: d.Info.LastUpdate,
+ ObjectsTotalCount: flat.Objects,
+ VersionsTotalCount: flat.Versions,
+ DeleteMarkersTotalCount: flat.DeleteMarkers,
+ ObjectsTotalSize: uint64(flat.Size),
+ BucketsCount: uint64(len(e.Children)),
+ BucketsUsage: d.bucketsUsageInfo(buckets),
+ TierStats: d.tiersUsageInfo(buckets),
+ }
+ return dui
+}
+
+// replace will add or replace an entry in the cache.
+// If a parent is specified it will be added to that if not already there.
+// If the parent does not exist, it will be added.
+func (d *dataUsageCache) replace(path, parent string, e dataUsageEntry) {
+ hash := hashPath(path)
+ if d.Cache == nil {
+ d.Cache = make(map[string]dataUsageEntry, 100)
+ }
+ d.Cache[hash.Key()] = e
+ if parent != "" {
+ phash := hashPath(parent)
+ p := d.Cache[phash.Key()]
+ p.addChild(hash)
+ d.Cache[phash.Key()] = p
+ }
+}
+
+// replaceHashed add or replaces an entry to the cache based on its hash.
+// If a parent is specified it will be added to that if not already there.
+// If the parent does not exist, it will be added.
+func (d *dataUsageCache) replaceHashed(hash dataUsageHash, parent *dataUsageHash, e dataUsageEntry) {
+ if d.Cache == nil {
+ d.Cache = make(map[string]dataUsageEntry, 100)
+ }
+ d.Cache[hash.Key()] = e
+ if parent != nil {
+ p := d.Cache[parent.Key()]
+ p.addChild(hash)
+ d.Cache[parent.Key()] = p
+ }
+}
+
+// copyWithChildren will copy entry with hash from src if it exists along with any children.
+// If a parent is specified it will be added to that if not already there.
+// If the parent does not exist, it will be added.
+func (d *dataUsageCache) copyWithChildren(src *dataUsageCache, hash dataUsageHash, parent *dataUsageHash) {
+ if d.Cache == nil {
+ d.Cache = make(map[string]dataUsageEntry, 100)
+ }
+ e, ok := src.Cache[hash.String()]
+ if !ok {
+ return
+ }
+ d.Cache[hash.Key()] = e
+ for ch := range e.Children {
+ if ch == hash.Key() {
+ scannerLogIf(GlobalContext, errors.New("dataUsageCache.copyWithChildren: Circular reference"))
+ return
+ }
+ d.copyWithChildren(src, dataUsageHash(ch), &hash)
+ }
+ if parent != nil {
+ p := d.Cache[parent.Key()]
+ p.addChild(hash)
+ d.Cache[parent.Key()] = p
+ }
+}
+
+// reduceChildrenOf will reduce the recursive number of children to the limit
+// by compacting the children with the least number of objects.
+func (d *dataUsageCache) reduceChildrenOf(path dataUsageHash, limit int, compactSelf bool) {
+ e, ok := d.Cache[path.Key()]
+ if !ok {
+ return
+ }
+ if e.Compacted {
+ return
+ }
+ // If direct children have more, compact all.
+ if len(e.Children) > limit && compactSelf {
+ flat := d.sizeRecursive(path.Key())
+ flat.Compacted = true
+ d.deleteRecursive(path)
+ d.replaceHashed(path, nil, *flat)
+ return
+ }
+ total := d.totalChildrenRec(path.Key())
+ if total < limit {
+ return
+ }
+
+ // Appears to be printed with _MINIO_SERVER_DEBUG=off
+ // console.Debugf(" %d children found, compacting %v\n", total, path)
+
+ leaves := make([]struct {
+ objects uint64
+ path dataUsageHash
+ }, total)
+ // Collect current leaves that have children.
+ leaves = leaves[:0]
+ remove := total - limit
+ var add func(path dataUsageHash)
+ add = func(path dataUsageHash) {
+ e, ok := d.Cache[path.Key()]
+ if !ok {
+ return
+ }
+ if len(e.Children) == 0 {
+ return
+ }
+ sz := d.sizeRecursive(path.Key())
+ leaves = append(leaves, struct {
+ objects uint64
+ path dataUsageHash
+ }{objects: sz.Objects, path: path})
+ for ch := range e.Children {
+ add(dataUsageHash(ch))
+ }
+ }
+
+ // Add path recursively.
+ add(path)
+ sort.Slice(leaves, func(i, j int) bool {
+ return leaves[i].objects < leaves[j].objects
+ })
+ for remove > 0 && len(leaves) > 0 {
+ // Remove top entry.
+ e := leaves[0]
+ candidate := e.path
+ if candidate == path && !compactSelf {
+ // We should be the biggest,
+ // if we cannot compact ourself, we are done.
+ break
+ }
+ removing := d.totalChildrenRec(candidate.Key())
+ flat := d.sizeRecursive(candidate.Key())
+ if flat == nil {
+ leaves = leaves[1:]
+ continue
+ }
+ // Appears to be printed with _MINIO_SERVER_DEBUG=off
+ // console.Debugf("compacting %v, removing %d children\n", candidate, removing)
+
+ flat.Compacted = true
+ d.deleteRecursive(candidate)
+ d.replaceHashed(candidate, nil, *flat)
+
+ // Remove top entry and subtract removed children.
+ remove -= removing
+ leaves = leaves[1:]
+ }
+}
+
+// forceCompact will force compact the cache of the top entry.
+// If the number of children is more than limit*100, it will compact self.
+// When above the limit a cleanup will also be performed to remove any possible abandoned entries.
+func (d *dataUsageCache) forceCompact(limit int) {
+ if d == nil || len(d.Cache) <= limit {
+ return
+ }
+ top := hashPath(d.Info.Name).Key()
+ topE := d.find(top)
+ if topE == nil {
+ scannerLogIf(GlobalContext, errors.New("forceCompact: root not found"))
+ return
+ }
+ // If off by 2 orders of magnitude, compact self and log error.
+ if len(topE.Children) > dataScannerForceCompactAtFolders {
+ // If we still have too many children, compact self.
+ scannerLogOnceIf(GlobalContext, fmt.Errorf("forceCompact: %q has %d children. Force compacting. Expect reduced scanner performance", d.Info.Name, len(topE.Children)), d.Info.Name)
+ d.reduceChildrenOf(hashPath(d.Info.Name), limit, true)
+ }
+ if len(d.Cache) <= limit {
+ return
+ }
+
+ // Check for abandoned entries.
+ found := make(map[string]struct{}, len(d.Cache))
+
+ // Mark all children recursively
+ var mark func(entry dataUsageEntry)
+ mark = func(entry dataUsageEntry) {
+ for k := range entry.Children {
+ found[k] = struct{}{}
+ if ch, ok := d.Cache[k]; ok {
+ mark(ch)
+ }
+ }
+ }
+ found[top] = struct{}{}
+ mark(*topE)
+
+ // Delete all entries not found.
+ for k := range d.Cache {
+ if _, ok := found[k]; !ok {
+ delete(d.Cache, k)
+ }
+ }
+}
+
+// StringAll returns a detailed string representation of all entries in the cache.
+func (d *dataUsageCache) StringAll() string {
+ // Remove bloom filter from print.
+ s := fmt.Sprintf("info:%+v\n", d.Info)
+ for k, v := range d.Cache {
+ s += fmt.Sprintf("\t%v: %+v\n", k, v)
+ }
+ return strings.TrimSpace(s)
+}
+
+// String returns a human readable representation of the string.
+func (h dataUsageHash) String() string {
+ return string(h)
+}
+
+// Key returns the key.
+func (h dataUsageHash) Key() string {
+ return string(h)
+}
+
+func (d *dataUsageCache) flattenChildrens(root dataUsageEntry) (m map[string]dataUsageEntry) {
+ m = make(map[string]dataUsageEntry)
+ for id := range root.Children {
+ e := d.Cache[id]
+ if len(e.Children) > 0 {
+ e = d.flatten(e)
+ }
+ m[id] = e
+ }
+ return m
+}
+
+// flatten all children of the root into the root element and return it.
+func (d *dataUsageCache) flatten(root dataUsageEntry) dataUsageEntry {
+ for id := range root.Children {
+ e := d.Cache[id]
+ if len(e.Children) > 0 {
+ e = d.flatten(e)
+ }
+ root.merge(e)
+ }
+ root.Children = nil
+ return root
+}
+
+// add a size to the histogram.
+func (h *sizeHistogram) add(size int64) {
+ // Fetch the histogram interval corresponding
+ // to the passed object size.
+ for i, interval := range ObjectsHistogramIntervals[:] {
+ if size >= interval.start && size <= interval.end {
+ h[i]++
+ break
+ }
+ }
+}
+
+// mergeV1 is used to migrate data usage cache from sizeHistogramV1 to
+// sizeHistogram
+func (h *sizeHistogram) mergeV1(v sizeHistogramV1) {
+ var oidx, nidx int
+ for oidx < len(v) {
+ intOld, intNew := ObjectsHistogramIntervalsV1[oidx], ObjectsHistogramIntervals[nidx]
+ // skip intervals that aren't common to both histograms
+ if intOld.start != intNew.start || intOld.end != intNew.end {
+ nidx++
+ continue
+ }
+ h[nidx] += v[oidx]
+ oidx++
+ nidx++
+ }
+}
+
+// toMap returns the map to a map[string]uint64.
+func (h *sizeHistogram) toMap() map[string]uint64 {
+ res := make(map[string]uint64, dataUsageBucketLen)
+ var splCount uint64
+ for i, count := range h {
+ szInt := ObjectsHistogramIntervals[i]
+ switch {
+ case humanize.KiByte == szInt.start && szInt.end == humanize.MiByte-1:
+ // spl interval: [1024B, 1MiB)
+ res[szInt.name] = splCount
+ case humanize.KiByte <= szInt.start && szInt.end <= humanize.MiByte-1:
+ // intervals that fall within the spl interval above; they
+ // appear earlier in this array of intervals, see
+ // ObjectsHistogramIntervals
+ splCount += count
+ fallthrough
+ default:
+ res[szInt.name] = count
+ }
+ }
+ return res
+}
+
+// add a version count to the histogram.
+func (h *versionsHistogram) add(versions uint64) {
+ // Fetch the histogram interval corresponding
+ // to the passed object size.
+ for i, interval := range ObjectsVersionCountIntervals[:] {
+ if versions >= uint64(interval.start) && versions <= uint64(interval.end) {
+ h[i]++
+ break
+ }
+ }
+}
+
+// toMap returns the map to a map[string]uint64.
+func (h *versionsHistogram) toMap() map[string]uint64 {
+ res := make(map[string]uint64, dataUsageVersionLen)
+ for i, count := range h {
+ res[ObjectsVersionCountIntervals[i].name] = count
+ }
+ return res
+}
+
+func (d *dataUsageCache) tiersUsageInfo(buckets []BucketInfo) *allTierStats {
+ dst := newAllTierStats()
+ for _, bucket := range buckets {
+ e := d.find(bucket.Name)
+ if e == nil {
+ continue
+ }
+ flat := d.flatten(*e)
+ if flat.AllTierStats == nil {
+ continue
+ }
+ dst.merge(flat.AllTierStats)
+ }
+ if len(dst.Tiers) == 0 {
+ return nil
+ }
+ return dst
+}
+
+// bucketsUsageInfo returns the buckets usage info as a map, with
+// key as bucket name
+func (d *dataUsageCache) bucketsUsageInfo(buckets []BucketInfo) map[string]BucketUsageInfo {
+ dst := make(map[string]BucketUsageInfo, len(buckets))
+ for _, bucket := range buckets {
+ e := d.find(bucket.Name)
+ if e == nil {
+ continue
+ }
+ flat := d.flatten(*e)
+ bui := BucketUsageInfo{
+ Size: uint64(flat.Size),
+ VersionsCount: flat.Versions,
+ ObjectsCount: flat.Objects,
+ DeleteMarkersCount: flat.DeleteMarkers,
+ ObjectSizesHistogram: flat.ObjSizes.toMap(),
+ ObjectVersionsHistogram: flat.ObjVersions.toMap(),
+ }
+ dst[bucket.Name] = bui
+ }
+ return dst
+}
+
+// sizeRecursive returns the path as a flattened entry.
+func (d *dataUsageCache) sizeRecursive(path string) *dataUsageEntry {
+ root := d.find(path)
+ if root == nil || len(root.Children) == 0 {
+ return root
+ }
+ flat := d.flatten(*root)
+ return &flat
+}
+
+// totalChildrenRec returns the total number of children recorded.
+func (d *dataUsageCache) totalChildrenRec(path string) int {
+ root := d.find(path)
+ if root == nil || len(root.Children) == 0 {
+ return 0
+ }
+ n := len(root.Children)
+ for ch := range root.Children {
+ n += d.totalChildrenRec(ch)
+ }
+ return n
+}
+
+// root returns the root of the cache.
+func (d *dataUsageCache) root() *dataUsageEntry {
+ return d.find(d.Info.Name)
+}
+
+// rootHash returns the root of the cache.
+func (d *dataUsageCache) rootHash() dataUsageHash {
+ return hashPath(d.Info.Name)
+}
+
+// clone returns a copy of the cache with no references to the existing.
+func (d *dataUsageCache) clone() dataUsageCache {
+ clone := dataUsageCache{
+ Info: d.Info,
+ Cache: make(map[string]dataUsageEntry, len(d.Cache)),
+ }
+ for k, v := range d.Cache {
+ clone.Cache[k] = v.clone()
+ }
+ return clone
+}
+
+// merge root of other into d.
+// children of root will be flattened before being merged.
+// Last update time will be set to the last updated.
+func (d *dataUsageCache) merge(other dataUsageCache) {
+ existingRoot := d.root()
+ otherRoot := other.root()
+ if existingRoot == nil && otherRoot == nil {
+ return
+ }
+ if otherRoot == nil {
+ return
+ }
+ if existingRoot == nil {
+ *d = other.clone()
+ return
+ }
+ if other.Info.LastUpdate.After(d.Info.LastUpdate) {
+ d.Info.LastUpdate = other.Info.LastUpdate
+ }
+ existingRoot.merge(*otherRoot)
+ eHash := d.rootHash()
+ for key := range otherRoot.Children {
+ entry := other.Cache[key]
+ flat := other.flatten(entry)
+ existing := d.Cache[key]
+ // If not found, merging simply adds.
+ existing.merge(flat)
+ d.replaceHashed(dataUsageHash(key), &eHash, existing)
+ }
+}
+
+type objectIO interface {
+ GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions) (reader *GetObjectReader, err error)
+ PutObject(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error)
+}
+
+// load the cache content with name from minioMetaBackgroundOpsBucket.
+// Only backend errors are returned as errors.
+// The loader is optimistic and has no locking, but tries 5 times before giving up.
+// If the object is not found, a nil error with empty data usage cache is returned.
+func (d *dataUsageCache) load(ctx context.Context, store objectIO, name string) error {
+ // By default, empty data usage cache
+ *d = dataUsageCache{}
+
+ load := func(name string, timeout time.Duration) (bool, error) {
+ // Abandon if more than time.Minute, so we don't hold up scanner.
+ // drive timeout by default is 2 minutes, we do not need to wait longer.
+ ctx, cancel := context.WithTimeout(ctx, timeout)
+ defer cancel()
+
+ r, err := store.GetObjectNInfo(ctx, minioMetaBucket, pathJoin(bucketMetaPrefix, name), nil, http.Header{}, ObjectOptions{NoLock: true})
+ if err != nil {
+ switch err.(type) {
+ case ObjectNotFound, BucketNotFound:
+ r, err = store.GetObjectNInfo(ctx, dataUsageBucket, name, nil, http.Header{}, ObjectOptions{NoLock: true})
+ if err != nil {
+ switch err.(type) {
+ case ObjectNotFound, BucketNotFound:
+ return false, nil
+ case InsufficientReadQuorum, StorageErr:
+ return true, nil
+ }
+ return false, err
+ }
+ err = d.deserialize(r)
+ r.Close()
+ return err != nil, nil
+ case InsufficientReadQuorum, StorageErr:
+ return true, nil
+ }
+ return false, err
+ }
+ err = d.deserialize(r)
+ r.Close()
+ return err != nil, nil
+ }
+
+ // Caches are read+written without locks,
+ retries := 0
+ for retries < 5 {
+ retry, err := load(name, time.Minute)
+ if err != nil {
+ return toObjectErr(err, dataUsageBucket, name)
+ }
+ if !retry {
+ break
+ }
+ retry, err = load(name+".bkp", 30*time.Second)
+ if err == nil && !retry {
+ // Only return when we have valid data from the backup
+ break
+ }
+ retries++
+ time.Sleep(time.Duration(rand.Int63n(int64(time.Second))))
+ }
+
+ if retries == 5 {
+ scannerLogOnceIf(ctx, fmt.Errorf("maximum retry reached to load the data usage cache `%s`", name), "retry-loading-data-usage-cache")
+ }
+
+ return nil
+}
+
+// Maximum running concurrent saves on server.
+var maxConcurrentScannerSaves = make(chan struct{}, 4)
+
+// save the content of the cache to minioMetaBackgroundOpsBucket with the provided name.
+// Note that no locking is done when saving.
+func (d *dataUsageCache) save(ctx context.Context, store objectIO, name string) error {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case maxConcurrentScannerSaves <- struct{}{}:
+ }
+
+ buf := bytebufferpool.Get()
+ defer func() {
+ <-maxConcurrentScannerSaves
+ buf.Reset()
+ bytebufferpool.Put(buf)
+ }()
+
+ if err := d.serializeTo(buf); err != nil {
+ return err
+ }
+
+ save := func(name string, timeout time.Duration) error {
+ // Abandon if more than a minute, so we don't hold up scanner.
+ ctx, cancel := context.WithTimeout(ctx, timeout)
+ defer cancel()
+
+ return saveConfig(ctx, store, pathJoin(bucketMetaPrefix, name), buf.Bytes())
+ }
+ defer save(name+".bkp", 5*time.Second) // Keep a backup as well
+
+ // drive timeout by default is 2 minutes, we do not need to wait longer.
+ return save(name, time.Minute)
+}
+
+// dataUsageCacheVer indicates the cache version.
+// Bumping the cache version will drop data from previous versions
+// and write new data with the new version.
+const (
+ dataUsageCacheVerCurrent = 8
+ dataUsageCacheVerV7 = 7
+ dataUsageCacheVerV6 = 6
+ dataUsageCacheVerV5 = 5
+ dataUsageCacheVerV4 = 4
+ dataUsageCacheVerV3 = 3
+ dataUsageCacheVerV2 = 2
+ dataUsageCacheVerV1 = 1
+)
+
+// serialize the contents of the cache.
+func (d *dataUsageCache) serializeTo(dst io.Writer) error {
+ // Add version and compress.
+ _, err := dst.Write([]byte{dataUsageCacheVerCurrent})
+ if err != nil {
+ return err
+ }
+ enc, err := zstd.NewWriter(dst,
+ zstd.WithEncoderLevel(zstd.SpeedFastest),
+ zstd.WithWindowSize(1<<20),
+ zstd.WithEncoderConcurrency(2))
+ if err != nil {
+ return err
+ }
+ mEnc := msgp.NewWriter(enc)
+ err = d.EncodeMsg(mEnc)
+ if err != nil {
+ return err
+ }
+ err = mEnc.Flush()
+ if err != nil {
+ return err
+ }
+ err = enc.Close()
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// deserialize the supplied byte slice into the cache.
+func (d *dataUsageCache) deserialize(r io.Reader) error {
+ var b [1]byte
+ n, _ := r.Read(b[:])
+ if n != 1 {
+ return io.ErrUnexpectedEOF
+ }
+ ver := int(b[0])
+ switch ver {
+ case dataUsageCacheVerV1:
+ return errors.New("cache version deprecated (will autoupdate)")
+ case dataUsageCacheVerV2:
+ // Zstd compressed.
+ dec, err := zstd.NewReader(r, zstd.WithDecoderConcurrency(2))
+ if err != nil {
+ return err
+ }
+ defer dec.Close()
+
+ dold := &dataUsageCacheV2{}
+ if err = dold.DecodeMsg(msgp.NewReader(dec)); err != nil {
+ return err
+ }
+ d.Info = dold.Info
+ d.Cache = make(map[string]dataUsageEntry, len(dold.Cache))
+ for k, v := range dold.Cache {
+ d.Cache[k] = dataUsageEntry{
+ Size: v.Size,
+ Objects: v.Objects,
+ ObjSizes: v.ObjSizes,
+ Children: v.Children,
+ Compacted: len(v.Children) == 0 && k != d.Info.Name,
+ }
+ }
+ return nil
+ case dataUsageCacheVerV3:
+ // Zstd compressed.
+ dec, err := zstd.NewReader(r, zstd.WithDecoderConcurrency(2))
+ if err != nil {
+ return err
+ }
+ defer dec.Close()
+ dold := &dataUsageCacheV3{}
+ if err = dold.DecodeMsg(msgp.NewReader(dec)); err != nil {
+ return err
+ }
+ d.Info = dold.Info
+ d.Cache = make(map[string]dataUsageEntry, len(dold.Cache))
+ for k, v := range dold.Cache {
+ due := dataUsageEntry{
+ Size: v.Size,
+ Objects: v.Objects,
+ ObjSizes: v.ObjSizes,
+ Children: v.Children,
+ }
+ due.Compacted = len(due.Children) == 0 && k != d.Info.Name
+
+ d.Cache[k] = due
+ }
+ return nil
+ case dataUsageCacheVerV4:
+ // Zstd compressed.
+ dec, err := zstd.NewReader(r, zstd.WithDecoderConcurrency(2))
+ if err != nil {
+ return err
+ }
+ defer dec.Close()
+ dold := &dataUsageCacheV4{}
+ if err = dold.DecodeMsg(msgp.NewReader(dec)); err != nil {
+ return err
+ }
+ d.Info = dold.Info
+ d.Cache = make(map[string]dataUsageEntry, len(dold.Cache))
+ for k, v := range dold.Cache {
+ due := dataUsageEntry{
+ Size: v.Size,
+ Objects: v.Objects,
+ ObjSizes: v.ObjSizes,
+ Children: v.Children,
+ }
+ due.Compacted = len(due.Children) == 0 && k != d.Info.Name
+
+ d.Cache[k] = due
+ }
+ return nil
+ case dataUsageCacheVerV5:
+ // Zstd compressed.
+ dec, err := zstd.NewReader(r, zstd.WithDecoderConcurrency(2))
+ if err != nil {
+ return err
+ }
+ defer dec.Close()
+ dold := &dataUsageCacheV5{}
+ if err = dold.DecodeMsg(msgp.NewReader(dec)); err != nil {
+ return err
+ }
+ d.Info = dold.Info
+ d.Cache = make(map[string]dataUsageEntry, len(dold.Cache))
+ for k, v := range dold.Cache {
+ due := dataUsageEntry{
+ Size: v.Size,
+ Objects: v.Objects,
+ ObjSizes: v.ObjSizes,
+ Children: v.Children,
+ }
+ due.Compacted = len(due.Children) == 0 && k != d.Info.Name
+
+ d.Cache[k] = due
+ }
+ return nil
+ case dataUsageCacheVerV6:
+ // Zstd compressed.
+ dec, err := zstd.NewReader(r, zstd.WithDecoderConcurrency(2))
+ if err != nil {
+ return err
+ }
+ defer dec.Close()
+ dold := &dataUsageCacheV6{}
+ if err = dold.DecodeMsg(msgp.NewReader(dec)); err != nil {
+ return err
+ }
+ d.Info = dold.Info
+ d.Cache = make(map[string]dataUsageEntry, len(dold.Cache))
+ for k, v := range dold.Cache {
+ due := dataUsageEntry{
+ Children: v.Children,
+ Size: v.Size,
+ Objects: v.Objects,
+ Versions: v.Versions,
+ ObjSizes: v.ObjSizes,
+ Compacted: v.Compacted,
+ }
+ d.Cache[k] = due
+ }
+ return nil
+ case dataUsageCacheVerV7:
+ // Zstd compressed.
+ dec, err := zstd.NewReader(r, zstd.WithDecoderConcurrency(2))
+ if err != nil {
+ return err
+ }
+ defer dec.Close()
+ dold := &dataUsageCacheV7{}
+ if err = dold.DecodeMsg(msgp.NewReader(dec)); err != nil {
+ return err
+ }
+ d.Info = dold.Info
+ d.Cache = make(map[string]dataUsageEntry, len(dold.Cache))
+ for k, v := range dold.Cache {
+ var szHist sizeHistogram
+ szHist.mergeV1(v.ObjSizes)
+ d.Cache[k] = dataUsageEntry{
+ Children: v.Children,
+ Size: v.Size,
+ Objects: v.Objects,
+ Versions: v.Versions,
+ ObjSizes: szHist,
+ Compacted: v.Compacted,
+ }
+ }
+
+ return nil
+ case dataUsageCacheVerCurrent:
+ // Zstd compressed.
+ dec, err := zstd.NewReader(r, zstd.WithDecoderConcurrency(2))
+ if err != nil {
+ return err
+ }
+ defer dec.Close()
+ return d.DecodeMsg(msgp.NewReader(dec))
+ default:
+ return fmt.Errorf("dataUsageCache: unknown version: %d", ver)
+ }
+}
+
+// Trim this from start+end of hashes.
+var hashPathCutSet = dataUsageRoot
+
+func init() {
+ if dataUsageRoot != string(filepath.Separator) {
+ hashPathCutSet = dataUsageRoot + string(filepath.Separator)
+ }
+}
+
+// hashPath calculates a hash of the provided string.
+func hashPath(data string) dataUsageHash {
+ if data != dataUsageRoot {
+ data = strings.Trim(data, hashPathCutSet)
+ }
+ return dataUsageHash(path.Clean(data))
+}
+
+//msgp:ignore dataUsageHashMap
+type dataUsageHashMap map[string]struct{}
+
+// DecodeMsg implements msgp.Decodable
+func (z *dataUsageHashMap) DecodeMsg(dc *msgp.Reader) (err error) {
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return err
+ }
+ if zb0002 == 0 {
+ *z = nil
+ return err
+ }
+ *z = make(dataUsageHashMap, zb0002)
+ for i := uint32(0); i < zb0002; i++ {
+ {
+ var zb0003 string
+ zb0003, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return err
+ }
+ (*z)[zb0003] = struct{}{}
+ }
+ }
+ return err
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z dataUsageHashMap) EncodeMsg(en *msgp.Writer) (err error) {
+ err = en.WriteArrayHeader(uint32(len(z)))
+ if err != nil {
+ err = msgp.WrapError(err)
+ return err
+ }
+ for zb0004 := range z {
+ err = en.WriteString(zb0004)
+ if err != nil {
+ err = msgp.WrapError(err, zb0004)
+ return err
+ }
+ }
+ return err
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z dataUsageHashMap) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ o = msgp.AppendArrayHeader(o, uint32(len(z)))
+ for zb0004 := range z {
+ o = msgp.AppendString(o, zb0004)
+ }
+ return o, err
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *dataUsageHashMap) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return o, err
+ }
+ if zb0002 == 0 {
+ *z = nil
+ return bts, nil
+ }
+ *z = make(dataUsageHashMap, zb0002)
+ for i := uint32(0); i < zb0002; i++ {
+ {
+ var zb0003 string
+ zb0003, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return o, err
+ }
+ (*z)[zb0003] = struct{}{}
+ }
+ }
+ o = bts
+ return o, err
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z dataUsageHashMap) Msgsize() (s int) {
+ s = msgp.ArrayHeaderSize
+ for zb0004 := range z {
+ s += msgp.StringPrefixSize + len(zb0004)
+ }
+ return s
+}
+
+//msgp:encode ignore currentScannerCycle
+//msgp:decode ignore currentScannerCycle
+
+type currentScannerCycle struct {
+ current uint64
+ next uint64
+ started time.Time
+ cycleCompleted []time.Time
+}
+
+// clone returns a clone.
+func (z currentScannerCycle) clone() currentScannerCycle {
+ z.cycleCompleted = append(make([]time.Time, 0, len(z.cycleCompleted)), z.cycleCompleted...)
+ return z
+}
diff --git a/cmd/data-usage-cache_gen.go b/cmd/data-usage-cache_gen.go
new file mode 100644
index 00000000..df9d34a1
--- /dev/null
+++ b/cmd/data-usage-cache_gen.go
@@ -0,0 +1,3698 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "time"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+// DecodeMsg implements msgp.Decodable
+func (z *allTierStats) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ts":
+ var zb0002 uint32
+ zb0002, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Tiers")
+ return
+ }
+ if z.Tiers == nil {
+ z.Tiers = make(map[string]tierStats, zb0002)
+ } else if len(z.Tiers) > 0 {
+ clear(z.Tiers)
+ }
+ for zb0002 > 0 {
+ zb0002--
+ var za0001 string
+ za0001, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Tiers")
+ return
+ }
+ var za0002 tierStats
+ var zb0003 uint32
+ zb0003, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Tiers", za0001)
+ return
+ }
+ for zb0003 > 0 {
+ zb0003--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err, "Tiers", za0001)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ts":
+ za0002.TotalSize, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "Tiers", za0001, "TotalSize")
+ return
+ }
+ case "nv":
+ za0002.NumVersions, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "Tiers", za0001, "NumVersions")
+ return
+ }
+ case "no":
+ za0002.NumObjects, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "Tiers", za0001, "NumObjects")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err, "Tiers", za0001)
+ return
+ }
+ }
+ }
+ z.Tiers[za0001] = za0002
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *allTierStats) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 1
+ // write "ts"
+ err = en.Append(0x81, 0xa2, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteMapHeader(uint32(len(z.Tiers)))
+ if err != nil {
+ err = msgp.WrapError(err, "Tiers")
+ return
+ }
+ for za0001, za0002 := range z.Tiers {
+ err = en.WriteString(za0001)
+ if err != nil {
+ err = msgp.WrapError(err, "Tiers")
+ return
+ }
+ // map header, size 3
+ // write "ts"
+ err = en.Append(0x83, 0xa2, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(za0002.TotalSize)
+ if err != nil {
+ err = msgp.WrapError(err, "Tiers", za0001, "TotalSize")
+ return
+ }
+ // write "nv"
+ err = en.Append(0xa2, 0x6e, 0x76)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(za0002.NumVersions)
+ if err != nil {
+ err = msgp.WrapError(err, "Tiers", za0001, "NumVersions")
+ return
+ }
+ // write "no"
+ err = en.Append(0xa2, 0x6e, 0x6f)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(za0002.NumObjects)
+ if err != nil {
+ err = msgp.WrapError(err, "Tiers", za0001, "NumObjects")
+ return
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *allTierStats) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 1
+ // string "ts"
+ o = append(o, 0x81, 0xa2, 0x74, 0x73)
+ o = msgp.AppendMapHeader(o, uint32(len(z.Tiers)))
+ for za0001, za0002 := range z.Tiers {
+ o = msgp.AppendString(o, za0001)
+ // map header, size 3
+ // string "ts"
+ o = append(o, 0x83, 0xa2, 0x74, 0x73)
+ o = msgp.AppendUint64(o, za0002.TotalSize)
+ // string "nv"
+ o = append(o, 0xa2, 0x6e, 0x76)
+ o = msgp.AppendInt(o, za0002.NumVersions)
+ // string "no"
+ o = append(o, 0xa2, 0x6e, 0x6f)
+ o = msgp.AppendInt(o, za0002.NumObjects)
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *allTierStats) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ts":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Tiers")
+ return
+ }
+ if z.Tiers == nil {
+ z.Tiers = make(map[string]tierStats, zb0002)
+ } else if len(z.Tiers) > 0 {
+ clear(z.Tiers)
+ }
+ for zb0002 > 0 {
+ var za0002 tierStats
+ zb0002--
+ var za0001 string
+ za0001, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Tiers")
+ return
+ }
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Tiers", za0001)
+ return
+ }
+ for zb0003 > 0 {
+ zb0003--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Tiers", za0001)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ts":
+ za0002.TotalSize, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Tiers", za0001, "TotalSize")
+ return
+ }
+ case "nv":
+ za0002.NumVersions, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Tiers", za0001, "NumVersions")
+ return
+ }
+ case "no":
+ za0002.NumObjects, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Tiers", za0001, "NumObjects")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Tiers", za0001)
+ return
+ }
+ }
+ }
+ z.Tiers[za0001] = za0002
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *allTierStats) Msgsize() (s int) {
+ s = 1 + 3 + msgp.MapHeaderSize
+ if z.Tiers != nil {
+ for za0001, za0002 := range z.Tiers {
+ _ = za0002
+ s += msgp.StringPrefixSize + len(za0001) + 1 + 3 + msgp.Uint64Size + 3 + msgp.IntSize + 3 + msgp.IntSize
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *currentScannerCycle) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 4
+ // string "current"
+ o = append(o, 0x84, 0xa7, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74)
+ o = msgp.AppendUint64(o, z.current)
+ // string "next"
+ o = append(o, 0xa4, 0x6e, 0x65, 0x78, 0x74)
+ o = msgp.AppendUint64(o, z.next)
+ // string "started"
+ o = append(o, 0xa7, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64)
+ o = msgp.AppendTime(o, z.started)
+ // string "cycleCompleted"
+ o = append(o, 0xae, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.cycleCompleted)))
+ for za0001 := range z.cycleCompleted {
+ o = msgp.AppendTime(o, z.cycleCompleted[za0001])
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *currentScannerCycle) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "current":
+ z.current, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "current")
+ return
+ }
+ case "next":
+ z.next, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "next")
+ return
+ }
+ case "started":
+ z.started, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "started")
+ return
+ }
+ case "cycleCompleted":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "cycleCompleted")
+ return
+ }
+ if cap(z.cycleCompleted) >= int(zb0002) {
+ z.cycleCompleted = (z.cycleCompleted)[:zb0002]
+ } else {
+ z.cycleCompleted = make([]time.Time, zb0002)
+ }
+ for za0001 := range z.cycleCompleted {
+ z.cycleCompleted[za0001], bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "cycleCompleted", za0001)
+ return
+ }
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *currentScannerCycle) Msgsize() (s int) {
+ s = 1 + 8 + msgp.Uint64Size + 5 + msgp.Uint64Size + 8 + msgp.TimeSize + 15 + msgp.ArrayHeaderSize + (len(z.cycleCompleted) * (msgp.TimeSize))
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *dataUsageCache) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Info":
+ err = z.Info.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Info")
+ return
+ }
+ case "Cache":
+ var zb0002 uint32
+ zb0002, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ if z.Cache == nil {
+ z.Cache = make(map[string]dataUsageEntry, zb0002)
+ } else if len(z.Cache) > 0 {
+ clear(z.Cache)
+ }
+ for zb0002 > 0 {
+ zb0002--
+ var za0001 string
+ za0001, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ var za0002 dataUsageEntry
+ err = za0002.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache", za0001)
+ return
+ }
+ z.Cache[za0001] = za0002
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *dataUsageCache) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 2
+ // write "Info"
+ err = en.Append(0x82, 0xa4, 0x49, 0x6e, 0x66, 0x6f)
+ if err != nil {
+ return
+ }
+ err = z.Info.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Info")
+ return
+ }
+ // write "Cache"
+ err = en.Append(0xa5, 0x43, 0x61, 0x63, 0x68, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteMapHeader(uint32(len(z.Cache)))
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ for za0001, za0002 := range z.Cache {
+ err = en.WriteString(za0001)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ err = za0002.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache", za0001)
+ return
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *dataUsageCache) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 2
+ // string "Info"
+ o = append(o, 0x82, 0xa4, 0x49, 0x6e, 0x66, 0x6f)
+ o, err = z.Info.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Info")
+ return
+ }
+ // string "Cache"
+ o = append(o, 0xa5, 0x43, 0x61, 0x63, 0x68, 0x65)
+ o = msgp.AppendMapHeader(o, uint32(len(z.Cache)))
+ for za0001, za0002 := range z.Cache {
+ o = msgp.AppendString(o, za0001)
+ o, err = za0002.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache", za0001)
+ return
+ }
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *dataUsageCache) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Info":
+ bts, err = z.Info.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Info")
+ return
+ }
+ case "Cache":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ if z.Cache == nil {
+ z.Cache = make(map[string]dataUsageEntry, zb0002)
+ } else if len(z.Cache) > 0 {
+ clear(z.Cache)
+ }
+ for zb0002 > 0 {
+ var za0002 dataUsageEntry
+ zb0002--
+ var za0001 string
+ za0001, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ bts, err = za0002.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache", za0001)
+ return
+ }
+ z.Cache[za0001] = za0002
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *dataUsageCache) Msgsize() (s int) {
+ s = 1 + 5 + z.Info.Msgsize() + 6 + msgp.MapHeaderSize
+ if z.Cache != nil {
+ for za0001, za0002 := range z.Cache {
+ _ = za0002
+ s += msgp.StringPrefixSize + len(za0001) + za0002.Msgsize()
+ }
+ }
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *dataUsageCacheInfo) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Name":
+ z.Name, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Name")
+ return
+ }
+ case "NextCycle":
+ z.NextCycle, err = dc.ReadUint32()
+ if err != nil {
+ err = msgp.WrapError(err, "NextCycle")
+ return
+ }
+ case "LastUpdate":
+ z.LastUpdate, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "LastUpdate")
+ return
+ }
+ case "SkipHealing":
+ z.SkipHealing, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "SkipHealing")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *dataUsageCacheInfo) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 4
+ // write "Name"
+ err = en.Append(0x84, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Name)
+ if err != nil {
+ err = msgp.WrapError(err, "Name")
+ return
+ }
+ // write "NextCycle"
+ err = en.Append(0xa9, 0x4e, 0x65, 0x78, 0x74, 0x43, 0x79, 0x63, 0x6c, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint32(z.NextCycle)
+ if err != nil {
+ err = msgp.WrapError(err, "NextCycle")
+ return
+ }
+ // write "LastUpdate"
+ err = en.Append(0xaa, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.LastUpdate)
+ if err != nil {
+ err = msgp.WrapError(err, "LastUpdate")
+ return
+ }
+ // write "SkipHealing"
+ err = en.Append(0xab, 0x53, 0x6b, 0x69, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x69, 0x6e, 0x67)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.SkipHealing)
+ if err != nil {
+ err = msgp.WrapError(err, "SkipHealing")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *dataUsageCacheInfo) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 4
+ // string "Name"
+ o = append(o, 0x84, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
+ o = msgp.AppendString(o, z.Name)
+ // string "NextCycle"
+ o = append(o, 0xa9, 0x4e, 0x65, 0x78, 0x74, 0x43, 0x79, 0x63, 0x6c, 0x65)
+ o = msgp.AppendUint32(o, z.NextCycle)
+ // string "LastUpdate"
+ o = append(o, 0xaa, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65)
+ o = msgp.AppendTime(o, z.LastUpdate)
+ // string "SkipHealing"
+ o = append(o, 0xab, 0x53, 0x6b, 0x69, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x69, 0x6e, 0x67)
+ o = msgp.AppendBool(o, z.SkipHealing)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *dataUsageCacheInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Name":
+ z.Name, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Name")
+ return
+ }
+ case "NextCycle":
+ z.NextCycle, bts, err = msgp.ReadUint32Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "NextCycle")
+ return
+ }
+ case "LastUpdate":
+ z.LastUpdate, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "LastUpdate")
+ return
+ }
+ case "SkipHealing":
+ z.SkipHealing, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "SkipHealing")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *dataUsageCacheInfo) Msgsize() (s int) {
+ s = 1 + 5 + msgp.StringPrefixSize + len(z.Name) + 10 + msgp.Uint32Size + 11 + msgp.TimeSize + 12 + msgp.BoolSize
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *dataUsageCacheV2) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Info":
+ err = z.Info.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Info")
+ return
+ }
+ case "Cache":
+ var zb0002 uint32
+ zb0002, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ if z.Cache == nil {
+ z.Cache = make(map[string]dataUsageEntryV2, zb0002)
+ } else if len(z.Cache) > 0 {
+ clear(z.Cache)
+ }
+ for zb0002 > 0 {
+ zb0002--
+ var za0001 string
+ za0001, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ var za0002 dataUsageEntryV2
+ err = za0002.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache", za0001)
+ return
+ }
+ z.Cache[za0001] = za0002
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *dataUsageCacheV2) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Info":
+ bts, err = z.Info.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Info")
+ return
+ }
+ case "Cache":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ if z.Cache == nil {
+ z.Cache = make(map[string]dataUsageEntryV2, zb0002)
+ } else if len(z.Cache) > 0 {
+ clear(z.Cache)
+ }
+ for zb0002 > 0 {
+ var za0002 dataUsageEntryV2
+ zb0002--
+ var za0001 string
+ za0001, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ bts, err = za0002.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache", za0001)
+ return
+ }
+ z.Cache[za0001] = za0002
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *dataUsageCacheV2) Msgsize() (s int) {
+ s = 1 + 5 + z.Info.Msgsize() + 6 + msgp.MapHeaderSize
+ if z.Cache != nil {
+ for za0001, za0002 := range z.Cache {
+ _ = za0002
+ s += msgp.StringPrefixSize + len(za0001) + za0002.Msgsize()
+ }
+ }
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *dataUsageCacheV3) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Info":
+ err = z.Info.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Info")
+ return
+ }
+ case "Cache":
+ var zb0002 uint32
+ zb0002, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ if z.Cache == nil {
+ z.Cache = make(map[string]dataUsageEntryV3, zb0002)
+ } else if len(z.Cache) > 0 {
+ clear(z.Cache)
+ }
+ for zb0002 > 0 {
+ zb0002--
+ var za0001 string
+ za0001, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ var za0002 dataUsageEntryV3
+ err = za0002.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache", za0001)
+ return
+ }
+ z.Cache[za0001] = za0002
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *dataUsageCacheV3) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Info":
+ bts, err = z.Info.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Info")
+ return
+ }
+ case "Cache":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ if z.Cache == nil {
+ z.Cache = make(map[string]dataUsageEntryV3, zb0002)
+ } else if len(z.Cache) > 0 {
+ clear(z.Cache)
+ }
+ for zb0002 > 0 {
+ var za0002 dataUsageEntryV3
+ zb0002--
+ var za0001 string
+ za0001, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ bts, err = za0002.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache", za0001)
+ return
+ }
+ z.Cache[za0001] = za0002
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *dataUsageCacheV3) Msgsize() (s int) {
+ s = 1 + 5 + z.Info.Msgsize() + 6 + msgp.MapHeaderSize
+ if z.Cache != nil {
+ for za0001, za0002 := range z.Cache {
+ _ = za0002
+ s += msgp.StringPrefixSize + len(za0001) + za0002.Msgsize()
+ }
+ }
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *dataUsageCacheV4) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Info":
+ err = z.Info.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Info")
+ return
+ }
+ case "Cache":
+ var zb0002 uint32
+ zb0002, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ if z.Cache == nil {
+ z.Cache = make(map[string]dataUsageEntryV4, zb0002)
+ } else if len(z.Cache) > 0 {
+ clear(z.Cache)
+ }
+ for zb0002 > 0 {
+ zb0002--
+ var za0001 string
+ za0001, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ var za0002 dataUsageEntryV4
+ err = za0002.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache", za0001)
+ return
+ }
+ z.Cache[za0001] = za0002
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *dataUsageCacheV4) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Info":
+ bts, err = z.Info.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Info")
+ return
+ }
+ case "Cache":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ if z.Cache == nil {
+ z.Cache = make(map[string]dataUsageEntryV4, zb0002)
+ } else if len(z.Cache) > 0 {
+ clear(z.Cache)
+ }
+ for zb0002 > 0 {
+ var za0002 dataUsageEntryV4
+ zb0002--
+ var za0001 string
+ za0001, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ bts, err = za0002.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache", za0001)
+ return
+ }
+ z.Cache[za0001] = za0002
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *dataUsageCacheV4) Msgsize() (s int) {
+ s = 1 + 5 + z.Info.Msgsize() + 6 + msgp.MapHeaderSize
+ if z.Cache != nil {
+ for za0001, za0002 := range z.Cache {
+ _ = za0002
+ s += msgp.StringPrefixSize + len(za0001) + za0002.Msgsize()
+ }
+ }
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *dataUsageCacheV5) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Info":
+ err = z.Info.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Info")
+ return
+ }
+ case "Cache":
+ var zb0002 uint32
+ zb0002, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ if z.Cache == nil {
+ z.Cache = make(map[string]dataUsageEntryV5, zb0002)
+ } else if len(z.Cache) > 0 {
+ clear(z.Cache)
+ }
+ for zb0002 > 0 {
+ zb0002--
+ var za0001 string
+ za0001, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ var za0002 dataUsageEntryV5
+ err = za0002.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache", za0001)
+ return
+ }
+ z.Cache[za0001] = za0002
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *dataUsageCacheV5) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Info":
+ bts, err = z.Info.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Info")
+ return
+ }
+ case "Cache":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ if z.Cache == nil {
+ z.Cache = make(map[string]dataUsageEntryV5, zb0002)
+ } else if len(z.Cache) > 0 {
+ clear(z.Cache)
+ }
+ for zb0002 > 0 {
+ var za0002 dataUsageEntryV5
+ zb0002--
+ var za0001 string
+ za0001, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ bts, err = za0002.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache", za0001)
+ return
+ }
+ z.Cache[za0001] = za0002
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *dataUsageCacheV5) Msgsize() (s int) {
+ s = 1 + 5 + z.Info.Msgsize() + 6 + msgp.MapHeaderSize
+ if z.Cache != nil {
+ for za0001, za0002 := range z.Cache {
+ _ = za0002
+ s += msgp.StringPrefixSize + len(za0001) + za0002.Msgsize()
+ }
+ }
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *dataUsageCacheV6) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Info":
+ err = z.Info.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Info")
+ return
+ }
+ case "Cache":
+ var zb0002 uint32
+ zb0002, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ if z.Cache == nil {
+ z.Cache = make(map[string]dataUsageEntryV6, zb0002)
+ } else if len(z.Cache) > 0 {
+ clear(z.Cache)
+ }
+ for zb0002 > 0 {
+ zb0002--
+ var za0001 string
+ za0001, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ var za0002 dataUsageEntryV6
+ err = za0002.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache", za0001)
+ return
+ }
+ z.Cache[za0001] = za0002
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *dataUsageCacheV6) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Info":
+ bts, err = z.Info.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Info")
+ return
+ }
+ case "Cache":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ if z.Cache == nil {
+ z.Cache = make(map[string]dataUsageEntryV6, zb0002)
+ } else if len(z.Cache) > 0 {
+ clear(z.Cache)
+ }
+ for zb0002 > 0 {
+ var za0002 dataUsageEntryV6
+ zb0002--
+ var za0001 string
+ za0001, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ bts, err = za0002.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache", za0001)
+ return
+ }
+ z.Cache[za0001] = za0002
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *dataUsageCacheV6) Msgsize() (s int) {
+ s = 1 + 5 + z.Info.Msgsize() + 6 + msgp.MapHeaderSize
+ if z.Cache != nil {
+ for za0001, za0002 := range z.Cache {
+ _ = za0002
+ s += msgp.StringPrefixSize + len(za0001) + za0002.Msgsize()
+ }
+ }
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *dataUsageCacheV7) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Info":
+ err = z.Info.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Info")
+ return
+ }
+ case "Cache":
+ var zb0002 uint32
+ zb0002, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ if z.Cache == nil {
+ z.Cache = make(map[string]dataUsageEntryV7, zb0002)
+ } else if len(z.Cache) > 0 {
+ clear(z.Cache)
+ }
+ for zb0002 > 0 {
+ zb0002--
+ var za0001 string
+ za0001, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ var za0002 dataUsageEntryV7
+ err = za0002.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache", za0001)
+ return
+ }
+ z.Cache[za0001] = za0002
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *dataUsageCacheV7) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Info":
+ bts, err = z.Info.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Info")
+ return
+ }
+ case "Cache":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ if z.Cache == nil {
+ z.Cache = make(map[string]dataUsageEntryV7, zb0002)
+ } else if len(z.Cache) > 0 {
+ clear(z.Cache)
+ }
+ for zb0002 > 0 {
+ var za0002 dataUsageEntryV7
+ zb0002--
+ var za0001 string
+ za0001, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache")
+ return
+ }
+ bts, err = za0002.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Cache", za0001)
+ return
+ }
+ z.Cache[za0001] = za0002
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *dataUsageCacheV7) Msgsize() (s int) {
+ s = 1 + 5 + z.Info.Msgsize() + 6 + msgp.MapHeaderSize
+ if z.Cache != nil {
+ for za0001, za0002 := range z.Cache {
+ _ = za0002
+ s += msgp.StringPrefixSize + len(za0001) + za0002.Msgsize()
+ }
+ }
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *dataUsageEntry) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ var zb0001Mask uint8 /* 1 bits */
+ _ = zb0001Mask
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ch":
+ err = z.Children.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Children")
+ return
+ }
+ case "sz":
+ z.Size, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ case "os":
+ z.Objects, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "Objects")
+ return
+ }
+ case "vs":
+ z.Versions, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "Versions")
+ return
+ }
+ case "dms":
+ z.DeleteMarkers, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "DeleteMarkers")
+ return
+ }
+ case "szs":
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes")
+ return
+ }
+ if zb0002 != uint32(dataUsageBucketLen) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLen), Got: zb0002}
+ return
+ }
+ for za0001 := range z.ObjSizes {
+ z.ObjSizes[za0001], err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes", za0001)
+ return
+ }
+ }
+ case "vh":
+ var zb0003 uint32
+ zb0003, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "ObjVersions")
+ return
+ }
+ if zb0003 != uint32(dataUsageVersionLen) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageVersionLen), Got: zb0003}
+ return
+ }
+ for za0002 := range z.ObjVersions {
+ z.ObjVersions[za0002], err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "ObjVersions", za0002)
+ return
+ }
+ }
+ case "ats":
+ if dc.IsNil() {
+ err = dc.ReadNil()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats")
+ return
+ }
+ z.AllTierStats = nil
+ } else {
+ if z.AllTierStats == nil {
+ z.AllTierStats = new(allTierStats)
+ }
+ var zb0004 uint32
+ zb0004, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats")
+ return
+ }
+ for zb0004 > 0 {
+ zb0004--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ts":
+ var zb0005 uint32
+ zb0005, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers")
+ return
+ }
+ if z.AllTierStats.Tiers == nil {
+ z.AllTierStats.Tiers = make(map[string]tierStats, zb0005)
+ } else if len(z.AllTierStats.Tiers) > 0 {
+ clear(z.AllTierStats.Tiers)
+ }
+ for zb0005 > 0 {
+ zb0005--
+ var za0003 string
+ za0003, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers")
+ return
+ }
+ var za0004 tierStats
+ var zb0006 uint32
+ zb0006, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003)
+ return
+ }
+ for zb0006 > 0 {
+ zb0006--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ts":
+ za0004.TotalSize, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003, "TotalSize")
+ return
+ }
+ case "nv":
+ za0004.NumVersions, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003, "NumVersions")
+ return
+ }
+ case "no":
+ za0004.NumObjects, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003, "NumObjects")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003)
+ return
+ }
+ }
+ }
+ z.AllTierStats.Tiers[za0003] = za0004
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats")
+ return
+ }
+ }
+ }
+ }
+ zb0001Mask |= 0x1
+ case "c":
+ z.Compacted, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "Compacted")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ // Clear omitted fields.
+ if (zb0001Mask & 0x1) == 0 {
+ z.AllTierStats = nil
+ }
+
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *dataUsageEntry) EncodeMsg(en *msgp.Writer) (err error) {
+ // check for omitted fields
+ zb0001Len := uint32(9)
+ var zb0001Mask uint16 /* 9 bits */
+ _ = zb0001Mask
+ if z.AllTierStats == nil {
+ zb0001Len--
+ zb0001Mask |= 0x80
+ }
+ // variable map header, size zb0001Len
+ err = en.Append(0x80 | uint8(zb0001Len))
+ if err != nil {
+ return
+ }
+
+ // skip if no fields are to be emitted
+ if zb0001Len != 0 {
+ // write "ch"
+ err = en.Append(0xa2, 0x63, 0x68)
+ if err != nil {
+ return
+ }
+ err = z.Children.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Children")
+ return
+ }
+ // write "sz"
+ err = en.Append(0xa2, 0x73, 0x7a)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.Size)
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ // write "os"
+ err = en.Append(0xa2, 0x6f, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.Objects)
+ if err != nil {
+ err = msgp.WrapError(err, "Objects")
+ return
+ }
+ // write "vs"
+ err = en.Append(0xa2, 0x76, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.Versions)
+ if err != nil {
+ err = msgp.WrapError(err, "Versions")
+ return
+ }
+ // write "dms"
+ err = en.Append(0xa3, 0x64, 0x6d, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.DeleteMarkers)
+ if err != nil {
+ err = msgp.WrapError(err, "DeleteMarkers")
+ return
+ }
+ // write "szs"
+ err = en.Append(0xa3, 0x73, 0x7a, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(dataUsageBucketLen))
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes")
+ return
+ }
+ for za0001 := range z.ObjSizes {
+ err = en.WriteUint64(z.ObjSizes[za0001])
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes", za0001)
+ return
+ }
+ }
+ // write "vh"
+ err = en.Append(0xa2, 0x76, 0x68)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(dataUsageVersionLen))
+ if err != nil {
+ err = msgp.WrapError(err, "ObjVersions")
+ return
+ }
+ for za0002 := range z.ObjVersions {
+ err = en.WriteUint64(z.ObjVersions[za0002])
+ if err != nil {
+ err = msgp.WrapError(err, "ObjVersions", za0002)
+ return
+ }
+ }
+ if (zb0001Mask & 0x80) == 0 { // if not omitted
+ // write "ats"
+ err = en.Append(0xa3, 0x61, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ if z.AllTierStats == nil {
+ err = en.WriteNil()
+ if err != nil {
+ return
+ }
+ } else {
+ // map header, size 1
+ // write "ts"
+ err = en.Append(0x81, 0xa2, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteMapHeader(uint32(len(z.AllTierStats.Tiers)))
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers")
+ return
+ }
+ for za0003, za0004 := range z.AllTierStats.Tiers {
+ err = en.WriteString(za0003)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers")
+ return
+ }
+ // map header, size 3
+ // write "ts"
+ err = en.Append(0x83, 0xa2, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(za0004.TotalSize)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003, "TotalSize")
+ return
+ }
+ // write "nv"
+ err = en.Append(0xa2, 0x6e, 0x76)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(za0004.NumVersions)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003, "NumVersions")
+ return
+ }
+ // write "no"
+ err = en.Append(0xa2, 0x6e, 0x6f)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(za0004.NumObjects)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003, "NumObjects")
+ return
+ }
+ }
+ }
+ }
+ // write "c"
+ err = en.Append(0xa1, 0x63)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.Compacted)
+ if err != nil {
+ err = msgp.WrapError(err, "Compacted")
+ return
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *dataUsageEntry) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // check for omitted fields
+ zb0001Len := uint32(9)
+ var zb0001Mask uint16 /* 9 bits */
+ _ = zb0001Mask
+ if z.AllTierStats == nil {
+ zb0001Len--
+ zb0001Mask |= 0x80
+ }
+ // variable map header, size zb0001Len
+ o = append(o, 0x80|uint8(zb0001Len))
+
+ // skip if no fields are to be emitted
+ if zb0001Len != 0 {
+ // string "ch"
+ o = append(o, 0xa2, 0x63, 0x68)
+ o, err = z.Children.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Children")
+ return
+ }
+ // string "sz"
+ o = append(o, 0xa2, 0x73, 0x7a)
+ o = msgp.AppendInt64(o, z.Size)
+ // string "os"
+ o = append(o, 0xa2, 0x6f, 0x73)
+ o = msgp.AppendUint64(o, z.Objects)
+ // string "vs"
+ o = append(o, 0xa2, 0x76, 0x73)
+ o = msgp.AppendUint64(o, z.Versions)
+ // string "dms"
+ o = append(o, 0xa3, 0x64, 0x6d, 0x73)
+ o = msgp.AppendUint64(o, z.DeleteMarkers)
+ // string "szs"
+ o = append(o, 0xa3, 0x73, 0x7a, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(dataUsageBucketLen))
+ for za0001 := range z.ObjSizes {
+ o = msgp.AppendUint64(o, z.ObjSizes[za0001])
+ }
+ // string "vh"
+ o = append(o, 0xa2, 0x76, 0x68)
+ o = msgp.AppendArrayHeader(o, uint32(dataUsageVersionLen))
+ for za0002 := range z.ObjVersions {
+ o = msgp.AppendUint64(o, z.ObjVersions[za0002])
+ }
+ if (zb0001Mask & 0x80) == 0 { // if not omitted
+ // string "ats"
+ o = append(o, 0xa3, 0x61, 0x74, 0x73)
+ if z.AllTierStats == nil {
+ o = msgp.AppendNil(o)
+ } else {
+ // map header, size 1
+ // string "ts"
+ o = append(o, 0x81, 0xa2, 0x74, 0x73)
+ o = msgp.AppendMapHeader(o, uint32(len(z.AllTierStats.Tiers)))
+ for za0003, za0004 := range z.AllTierStats.Tiers {
+ o = msgp.AppendString(o, za0003)
+ // map header, size 3
+ // string "ts"
+ o = append(o, 0x83, 0xa2, 0x74, 0x73)
+ o = msgp.AppendUint64(o, za0004.TotalSize)
+ // string "nv"
+ o = append(o, 0xa2, 0x6e, 0x76)
+ o = msgp.AppendInt(o, za0004.NumVersions)
+ // string "no"
+ o = append(o, 0xa2, 0x6e, 0x6f)
+ o = msgp.AppendInt(o, za0004.NumObjects)
+ }
+ }
+ }
+ // string "c"
+ o = append(o, 0xa1, 0x63)
+ o = msgp.AppendBool(o, z.Compacted)
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *dataUsageEntry) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ var zb0001Mask uint8 /* 1 bits */
+ _ = zb0001Mask
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ch":
+ bts, err = z.Children.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Children")
+ return
+ }
+ case "sz":
+ z.Size, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ case "os":
+ z.Objects, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Objects")
+ return
+ }
+ case "vs":
+ z.Versions, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Versions")
+ return
+ }
+ case "dms":
+ z.DeleteMarkers, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "DeleteMarkers")
+ return
+ }
+ case "szs":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes")
+ return
+ }
+ if zb0002 != uint32(dataUsageBucketLen) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLen), Got: zb0002}
+ return
+ }
+ for za0001 := range z.ObjSizes {
+ z.ObjSizes[za0001], bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes", za0001)
+ return
+ }
+ }
+ case "vh":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjVersions")
+ return
+ }
+ if zb0003 != uint32(dataUsageVersionLen) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageVersionLen), Got: zb0003}
+ return
+ }
+ for za0002 := range z.ObjVersions {
+ z.ObjVersions[za0002], bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjVersions", za0002)
+ return
+ }
+ }
+ case "ats":
+ if msgp.IsNil(bts) {
+ bts, err = msgp.ReadNilBytes(bts)
+ if err != nil {
+ return
+ }
+ z.AllTierStats = nil
+ } else {
+ if z.AllTierStats == nil {
+ z.AllTierStats = new(allTierStats)
+ }
+ var zb0004 uint32
+ zb0004, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats")
+ return
+ }
+ for zb0004 > 0 {
+ zb0004--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ts":
+ var zb0005 uint32
+ zb0005, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers")
+ return
+ }
+ if z.AllTierStats.Tiers == nil {
+ z.AllTierStats.Tiers = make(map[string]tierStats, zb0005)
+ } else if len(z.AllTierStats.Tiers) > 0 {
+ clear(z.AllTierStats.Tiers)
+ }
+ for zb0005 > 0 {
+ var za0004 tierStats
+ zb0005--
+ var za0003 string
+ za0003, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers")
+ return
+ }
+ var zb0006 uint32
+ zb0006, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003)
+ return
+ }
+ for zb0006 > 0 {
+ zb0006--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ts":
+ za0004.TotalSize, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003, "TotalSize")
+ return
+ }
+ case "nv":
+ za0004.NumVersions, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003, "NumVersions")
+ return
+ }
+ case "no":
+ za0004.NumObjects, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003, "NumObjects")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003)
+ return
+ }
+ }
+ }
+ z.AllTierStats.Tiers[za0003] = za0004
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats")
+ return
+ }
+ }
+ }
+ }
+ zb0001Mask |= 0x1
+ case "c":
+ z.Compacted, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Compacted")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ // Clear omitted fields.
+ if (zb0001Mask & 0x1) == 0 {
+ z.AllTierStats = nil
+ }
+
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *dataUsageEntry) Msgsize() (s int) {
+ s = 1 + 3 + z.Children.Msgsize() + 3 + msgp.Int64Size + 3 + msgp.Uint64Size + 3 + msgp.Uint64Size + 4 + msgp.Uint64Size + 4 + msgp.ArrayHeaderSize + (dataUsageBucketLen * (msgp.Uint64Size)) + 3 + msgp.ArrayHeaderSize + (dataUsageVersionLen * (msgp.Uint64Size)) + 4
+ if z.AllTierStats == nil {
+ s += msgp.NilSize
+ } else {
+ s += 1 + 3 + msgp.MapHeaderSize
+ if z.AllTierStats.Tiers != nil {
+ for za0003, za0004 := range z.AllTierStats.Tiers {
+ _ = za0004
+ s += msgp.StringPrefixSize + len(za0003) + 1 + 3 + msgp.Uint64Size + 3 + msgp.IntSize + 3 + msgp.IntSize
+ }
+ }
+ }
+ s += 2 + msgp.BoolSize
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *dataUsageEntryV2) DecodeMsg(dc *msgp.Reader) (err error) {
+ var zb0001 uint32
+ zb0001, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0001 != 4 {
+ err = msgp.ArrayError{Wanted: 4, Got: zb0001}
+ return
+ }
+ z.Size, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ z.Objects, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "Objects")
+ return
+ }
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes")
+ return
+ }
+ if zb0002 != uint32(dataUsageBucketLen) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLen), Got: zb0002}
+ return
+ }
+ for za0001 := range z.ObjSizes {
+ z.ObjSizes[za0001], err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes", za0001)
+ return
+ }
+ }
+ err = z.Children.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Children")
+ return
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *dataUsageEntryV2) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0001 != 4 {
+ err = msgp.ArrayError{Wanted: 4, Got: zb0001}
+ return
+ }
+ z.Size, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ z.Objects, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Objects")
+ return
+ }
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes")
+ return
+ }
+ if zb0002 != uint32(dataUsageBucketLen) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLen), Got: zb0002}
+ return
+ }
+ for za0001 := range z.ObjSizes {
+ z.ObjSizes[za0001], bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes", za0001)
+ return
+ }
+ }
+ bts, err = z.Children.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Children")
+ return
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *dataUsageEntryV2) Msgsize() (s int) {
+ s = 1 + msgp.Int64Size + msgp.Uint64Size + msgp.ArrayHeaderSize + (dataUsageBucketLen * (msgp.Uint64Size)) + z.Children.Msgsize()
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *dataUsageEntryV3) DecodeMsg(dc *msgp.Reader) (err error) {
+ var zb0001 uint32
+ zb0001, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0001 != 4 {
+ err = msgp.ArrayError{Wanted: 4, Got: zb0001}
+ return
+ }
+ z.Size, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ z.Objects, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "Objects")
+ return
+ }
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes")
+ return
+ }
+ if zb0002 != uint32(dataUsageBucketLen) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLen), Got: zb0002}
+ return
+ }
+ for za0001 := range z.ObjSizes {
+ z.ObjSizes[za0001], err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes", za0001)
+ return
+ }
+ }
+ err = z.Children.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Children")
+ return
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *dataUsageEntryV3) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0001 != 4 {
+ err = msgp.ArrayError{Wanted: 4, Got: zb0001}
+ return
+ }
+ z.Size, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ z.Objects, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Objects")
+ return
+ }
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes")
+ return
+ }
+ if zb0002 != uint32(dataUsageBucketLen) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLen), Got: zb0002}
+ return
+ }
+ for za0001 := range z.ObjSizes {
+ z.ObjSizes[za0001], bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes", za0001)
+ return
+ }
+ }
+ bts, err = z.Children.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Children")
+ return
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *dataUsageEntryV3) Msgsize() (s int) {
+ s = 1 + msgp.Int64Size + msgp.Uint64Size + msgp.ArrayHeaderSize + (dataUsageBucketLen * (msgp.Uint64Size)) + z.Children.Msgsize()
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *dataUsageEntryV4) DecodeMsg(dc *msgp.Reader) (err error) {
+ var zb0001 uint32
+ zb0001, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0001 != 4 {
+ err = msgp.ArrayError{Wanted: 4, Got: zb0001}
+ return
+ }
+ err = z.Children.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Children")
+ return
+ }
+ z.Size, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ z.Objects, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "Objects")
+ return
+ }
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes")
+ return
+ }
+ if zb0002 != uint32(dataUsageBucketLen) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLen), Got: zb0002}
+ return
+ }
+ for za0001 := range z.ObjSizes {
+ z.ObjSizes[za0001], err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes", za0001)
+ return
+ }
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *dataUsageEntryV4) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0001 != 4 {
+ err = msgp.ArrayError{Wanted: 4, Got: zb0001}
+ return
+ }
+ bts, err = z.Children.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Children")
+ return
+ }
+ z.Size, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ z.Objects, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Objects")
+ return
+ }
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes")
+ return
+ }
+ if zb0002 != uint32(dataUsageBucketLen) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLen), Got: zb0002}
+ return
+ }
+ for za0001 := range z.ObjSizes {
+ z.ObjSizes[za0001], bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes", za0001)
+ return
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *dataUsageEntryV4) Msgsize() (s int) {
+ s = 1 + z.Children.Msgsize() + msgp.Int64Size + msgp.Uint64Size + msgp.ArrayHeaderSize + (dataUsageBucketLen * (msgp.Uint64Size))
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *dataUsageEntryV5) DecodeMsg(dc *msgp.Reader) (err error) {
+ var zb0001 uint32
+ zb0001, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0001 != 6 {
+ err = msgp.ArrayError{Wanted: 6, Got: zb0001}
+ return
+ }
+ err = z.Children.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Children")
+ return
+ }
+ z.Size, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ z.Objects, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "Objects")
+ return
+ }
+ z.Versions, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "Versions")
+ return
+ }
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes")
+ return
+ }
+ if zb0002 != uint32(dataUsageBucketLen) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLen), Got: zb0002}
+ return
+ }
+ for za0001 := range z.ObjSizes {
+ z.ObjSizes[za0001], err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes", za0001)
+ return
+ }
+ }
+ z.Compacted, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "Compacted")
+ return
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *dataUsageEntryV5) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0001 != 6 {
+ err = msgp.ArrayError{Wanted: 6, Got: zb0001}
+ return
+ }
+ bts, err = z.Children.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Children")
+ return
+ }
+ z.Size, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ z.Objects, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Objects")
+ return
+ }
+ z.Versions, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Versions")
+ return
+ }
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes")
+ return
+ }
+ if zb0002 != uint32(dataUsageBucketLen) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLen), Got: zb0002}
+ return
+ }
+ for za0001 := range z.ObjSizes {
+ z.ObjSizes[za0001], bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes", za0001)
+ return
+ }
+ }
+ z.Compacted, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Compacted")
+ return
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *dataUsageEntryV5) Msgsize() (s int) {
+ s = 1 + z.Children.Msgsize() + msgp.Int64Size + msgp.Uint64Size + msgp.Uint64Size + msgp.ArrayHeaderSize + (dataUsageBucketLen * (msgp.Uint64Size)) + msgp.BoolSize
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *dataUsageEntryV6) DecodeMsg(dc *msgp.Reader) (err error) {
+ var zb0001 uint32
+ zb0001, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0001 != 6 {
+ err = msgp.ArrayError{Wanted: 6, Got: zb0001}
+ return
+ }
+ err = z.Children.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Children")
+ return
+ }
+ z.Size, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ z.Objects, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "Objects")
+ return
+ }
+ z.Versions, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "Versions")
+ return
+ }
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes")
+ return
+ }
+ if zb0002 != uint32(dataUsageBucketLen) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLen), Got: zb0002}
+ return
+ }
+ for za0001 := range z.ObjSizes {
+ z.ObjSizes[za0001], err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes", za0001)
+ return
+ }
+ }
+ z.Compacted, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "Compacted")
+ return
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *dataUsageEntryV6) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0001 != 6 {
+ err = msgp.ArrayError{Wanted: 6, Got: zb0001}
+ return
+ }
+ bts, err = z.Children.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Children")
+ return
+ }
+ z.Size, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ z.Objects, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Objects")
+ return
+ }
+ z.Versions, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Versions")
+ return
+ }
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes")
+ return
+ }
+ if zb0002 != uint32(dataUsageBucketLen) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLen), Got: zb0002}
+ return
+ }
+ for za0001 := range z.ObjSizes {
+ z.ObjSizes[za0001], bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes", za0001)
+ return
+ }
+ }
+ z.Compacted, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Compacted")
+ return
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *dataUsageEntryV6) Msgsize() (s int) {
+ s = 1 + z.Children.Msgsize() + msgp.Int64Size + msgp.Uint64Size + msgp.Uint64Size + msgp.ArrayHeaderSize + (dataUsageBucketLen * (msgp.Uint64Size)) + msgp.BoolSize
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *dataUsageEntryV7) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ var zb0001Mask uint8 /* 1 bits */
+ _ = zb0001Mask
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ch":
+ err = z.Children.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Children")
+ return
+ }
+ case "sz":
+ z.Size, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ case "os":
+ z.Objects, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "Objects")
+ return
+ }
+ case "vs":
+ z.Versions, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "Versions")
+ return
+ }
+ case "dms":
+ z.DeleteMarkers, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "DeleteMarkers")
+ return
+ }
+ case "szs":
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes")
+ return
+ }
+ if zb0002 != uint32(dataUsageBucketLenV1) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLenV1), Got: zb0002}
+ return
+ }
+ for za0001 := range z.ObjSizes {
+ z.ObjSizes[za0001], err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes", za0001)
+ return
+ }
+ }
+ case "vh":
+ var zb0003 uint32
+ zb0003, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "ObjVersions")
+ return
+ }
+ if zb0003 != uint32(dataUsageVersionLen) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageVersionLen), Got: zb0003}
+ return
+ }
+ for za0002 := range z.ObjVersions {
+ z.ObjVersions[za0002], err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "ObjVersions", za0002)
+ return
+ }
+ }
+ case "ats":
+ if dc.IsNil() {
+ err = dc.ReadNil()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats")
+ return
+ }
+ z.AllTierStats = nil
+ } else {
+ if z.AllTierStats == nil {
+ z.AllTierStats = new(allTierStats)
+ }
+ var zb0004 uint32
+ zb0004, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats")
+ return
+ }
+ for zb0004 > 0 {
+ zb0004--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ts":
+ var zb0005 uint32
+ zb0005, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers")
+ return
+ }
+ if z.AllTierStats.Tiers == nil {
+ z.AllTierStats.Tiers = make(map[string]tierStats, zb0005)
+ } else if len(z.AllTierStats.Tiers) > 0 {
+ clear(z.AllTierStats.Tiers)
+ }
+ for zb0005 > 0 {
+ zb0005--
+ var za0003 string
+ za0003, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers")
+ return
+ }
+ var za0004 tierStats
+ var zb0006 uint32
+ zb0006, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003)
+ return
+ }
+ for zb0006 > 0 {
+ zb0006--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ts":
+ za0004.TotalSize, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003, "TotalSize")
+ return
+ }
+ case "nv":
+ za0004.NumVersions, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003, "NumVersions")
+ return
+ }
+ case "no":
+ za0004.NumObjects, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003, "NumObjects")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003)
+ return
+ }
+ }
+ }
+ z.AllTierStats.Tiers[za0003] = za0004
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats")
+ return
+ }
+ }
+ }
+ }
+ zb0001Mask |= 0x1
+ case "c":
+ z.Compacted, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "Compacted")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ // Clear omitted fields.
+ if (zb0001Mask & 0x1) == 0 {
+ z.AllTierStats = nil
+ }
+
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *dataUsageEntryV7) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ var zb0001Mask uint8 /* 1 bits */
+ _ = zb0001Mask
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ch":
+ bts, err = z.Children.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Children")
+ return
+ }
+ case "sz":
+ z.Size, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ case "os":
+ z.Objects, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Objects")
+ return
+ }
+ case "vs":
+ z.Versions, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Versions")
+ return
+ }
+ case "dms":
+ z.DeleteMarkers, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "DeleteMarkers")
+ return
+ }
+ case "szs":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes")
+ return
+ }
+ if zb0002 != uint32(dataUsageBucketLenV1) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLenV1), Got: zb0002}
+ return
+ }
+ for za0001 := range z.ObjSizes {
+ z.ObjSizes[za0001], bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjSizes", za0001)
+ return
+ }
+ }
+ case "vh":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjVersions")
+ return
+ }
+ if zb0003 != uint32(dataUsageVersionLen) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageVersionLen), Got: zb0003}
+ return
+ }
+ for za0002 := range z.ObjVersions {
+ z.ObjVersions[za0002], bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjVersions", za0002)
+ return
+ }
+ }
+ case "ats":
+ if msgp.IsNil(bts) {
+ bts, err = msgp.ReadNilBytes(bts)
+ if err != nil {
+ return
+ }
+ z.AllTierStats = nil
+ } else {
+ if z.AllTierStats == nil {
+ z.AllTierStats = new(allTierStats)
+ }
+ var zb0004 uint32
+ zb0004, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats")
+ return
+ }
+ for zb0004 > 0 {
+ zb0004--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats")
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ts":
+ var zb0005 uint32
+ zb0005, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers")
+ return
+ }
+ if z.AllTierStats.Tiers == nil {
+ z.AllTierStats.Tiers = make(map[string]tierStats, zb0005)
+ } else if len(z.AllTierStats.Tiers) > 0 {
+ clear(z.AllTierStats.Tiers)
+ }
+ for zb0005 > 0 {
+ var za0004 tierStats
+ zb0005--
+ var za0003 string
+ za0003, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers")
+ return
+ }
+ var zb0006 uint32
+ zb0006, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003)
+ return
+ }
+ for zb0006 > 0 {
+ zb0006--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ts":
+ za0004.TotalSize, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003, "TotalSize")
+ return
+ }
+ case "nv":
+ za0004.NumVersions, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003, "NumVersions")
+ return
+ }
+ case "no":
+ za0004.NumObjects, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003, "NumObjects")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats", "Tiers", za0003)
+ return
+ }
+ }
+ }
+ z.AllTierStats.Tiers[za0003] = za0004
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AllTierStats")
+ return
+ }
+ }
+ }
+ }
+ zb0001Mask |= 0x1
+ case "c":
+ z.Compacted, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Compacted")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ // Clear omitted fields.
+ if (zb0001Mask & 0x1) == 0 {
+ z.AllTierStats = nil
+ }
+
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *dataUsageEntryV7) Msgsize() (s int) {
+ s = 1 + 3 + z.Children.Msgsize() + 3 + msgp.Int64Size + 3 + msgp.Uint64Size + 3 + msgp.Uint64Size + 4 + msgp.Uint64Size + 4 + msgp.ArrayHeaderSize + (dataUsageBucketLenV1 * (msgp.Uint64Size)) + 3 + msgp.ArrayHeaderSize + (dataUsageVersionLen * (msgp.Uint64Size)) + 4
+ if z.AllTierStats == nil {
+ s += msgp.NilSize
+ } else {
+ s += 1 + 3 + msgp.MapHeaderSize
+ if z.AllTierStats.Tiers != nil {
+ for za0003, za0004 := range z.AllTierStats.Tiers {
+ _ = za0004
+ s += msgp.StringPrefixSize + len(za0003) + 1 + 3 + msgp.Uint64Size + 3 + msgp.IntSize + 3 + msgp.IntSize
+ }
+ }
+ }
+ s += 2 + msgp.BoolSize
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *dataUsageHash) DecodeMsg(dc *msgp.Reader) (err error) {
+ {
+ var zb0001 string
+ zb0001, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ (*z) = dataUsageHash(zb0001)
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z dataUsageHash) EncodeMsg(en *msgp.Writer) (err error) {
+ err = en.WriteString(string(z))
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z dataUsageHash) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ o = msgp.AppendString(o, string(z))
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *dataUsageHash) 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) = dataUsageHash(zb0001)
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z dataUsageHash) Msgsize() (s int) {
+ s = msgp.StringPrefixSize + len(string(z))
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *sizeHistogram) DecodeMsg(dc *msgp.Reader) (err error) {
+ var zb0001 uint32
+ zb0001, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0001 != uint32(dataUsageBucketLen) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLen), Got: zb0001}
+ return
+ }
+ for za0001 := range z {
+ z[za0001], err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, za0001)
+ return
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *sizeHistogram) EncodeMsg(en *msgp.Writer) (err error) {
+ err = en.WriteArrayHeader(uint32(dataUsageBucketLen))
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for za0001 := range z {
+ err = en.WriteUint64(z[za0001])
+ if err != nil {
+ err = msgp.WrapError(err, za0001)
+ return
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *sizeHistogram) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ o = msgp.AppendArrayHeader(o, uint32(dataUsageBucketLen))
+ for za0001 := range z {
+ o = msgp.AppendUint64(o, z[za0001])
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *sizeHistogram) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0001 != uint32(dataUsageBucketLen) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLen), Got: zb0001}
+ return
+ }
+ for za0001 := range z {
+ z[za0001], bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, za0001)
+ return
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *sizeHistogram) Msgsize() (s int) {
+ s = msgp.ArrayHeaderSize + (dataUsageBucketLen * (msgp.Uint64Size))
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *sizeHistogramV1) DecodeMsg(dc *msgp.Reader) (err error) {
+ var zb0001 uint32
+ zb0001, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0001 != uint32(dataUsageBucketLenV1) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLenV1), Got: zb0001}
+ return
+ }
+ for za0001 := range z {
+ z[za0001], err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, za0001)
+ return
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *sizeHistogramV1) EncodeMsg(en *msgp.Writer) (err error) {
+ err = en.WriteArrayHeader(uint32(dataUsageBucketLenV1))
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for za0001 := range z {
+ err = en.WriteUint64(z[za0001])
+ if err != nil {
+ err = msgp.WrapError(err, za0001)
+ return
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *sizeHistogramV1) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ o = msgp.AppendArrayHeader(o, uint32(dataUsageBucketLenV1))
+ for za0001 := range z {
+ o = msgp.AppendUint64(o, z[za0001])
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *sizeHistogramV1) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0001 != uint32(dataUsageBucketLenV1) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLenV1), Got: zb0001}
+ return
+ }
+ for za0001 := range z {
+ z[za0001], bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, za0001)
+ return
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *sizeHistogramV1) Msgsize() (s int) {
+ s = msgp.ArrayHeaderSize + (dataUsageBucketLenV1 * (msgp.Uint64Size))
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *tierStats) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ts":
+ z.TotalSize, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "TotalSize")
+ return
+ }
+ case "nv":
+ z.NumVersions, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "NumVersions")
+ return
+ }
+ case "no":
+ z.NumObjects, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "NumObjects")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z tierStats) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 3
+ // write "ts"
+ err = en.Append(0x83, 0xa2, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.TotalSize)
+ if err != nil {
+ err = msgp.WrapError(err, "TotalSize")
+ return
+ }
+ // write "nv"
+ err = en.Append(0xa2, 0x6e, 0x76)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.NumVersions)
+ if err != nil {
+ err = msgp.WrapError(err, "NumVersions")
+ return
+ }
+ // write "no"
+ err = en.Append(0xa2, 0x6e, 0x6f)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.NumObjects)
+ if err != nil {
+ err = msgp.WrapError(err, "NumObjects")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z tierStats) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 3
+ // string "ts"
+ o = append(o, 0x83, 0xa2, 0x74, 0x73)
+ o = msgp.AppendUint64(o, z.TotalSize)
+ // string "nv"
+ o = append(o, 0xa2, 0x6e, 0x76)
+ o = msgp.AppendInt(o, z.NumVersions)
+ // string "no"
+ o = append(o, 0xa2, 0x6e, 0x6f)
+ o = msgp.AppendInt(o, z.NumObjects)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *tierStats) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ts":
+ z.TotalSize, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "TotalSize")
+ return
+ }
+ case "nv":
+ z.NumVersions, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "NumVersions")
+ return
+ }
+ case "no":
+ z.NumObjects, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "NumObjects")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z tierStats) Msgsize() (s int) {
+ s = 1 + 3 + msgp.Uint64Size + 3 + msgp.IntSize + 3 + msgp.IntSize
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *versionsHistogram) DecodeMsg(dc *msgp.Reader) (err error) {
+ var zb0001 uint32
+ zb0001, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0001 != uint32(dataUsageVersionLen) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageVersionLen), Got: zb0001}
+ return
+ }
+ for za0001 := range z {
+ z[za0001], err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, za0001)
+ return
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *versionsHistogram) EncodeMsg(en *msgp.Writer) (err error) {
+ err = en.WriteArrayHeader(uint32(dataUsageVersionLen))
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for za0001 := range z {
+ err = en.WriteUint64(z[za0001])
+ if err != nil {
+ err = msgp.WrapError(err, za0001)
+ return
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *versionsHistogram) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ o = msgp.AppendArrayHeader(o, uint32(dataUsageVersionLen))
+ for za0001 := range z {
+ o = msgp.AppendUint64(o, z[za0001])
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *versionsHistogram) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0001 != uint32(dataUsageVersionLen) {
+ err = msgp.ArrayError{Wanted: uint32(dataUsageVersionLen), Got: zb0001}
+ return
+ }
+ for za0001 := range z {
+ z[za0001], bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, za0001)
+ return
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *versionsHistogram) Msgsize() (s int) {
+ s = msgp.ArrayHeaderSize + (dataUsageVersionLen * (msgp.Uint64Size))
+ return
+}
diff --git a/cmd/data-usage-cache_gen_test.go b/cmd/data-usage-cache_gen_test.go
new file mode 100644
index 00000000..b19ca6bb
--- /dev/null
+++ b/cmd/data-usage-cache_gen_test.go
@@ -0,0 +1,972 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+func TestMarshalUnmarshalallTierStats(t *testing.T) {
+ v := allTierStats{}
+ 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 BenchmarkMarshalMsgallTierStats(b *testing.B) {
+ v := allTierStats{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgallTierStats(b *testing.B) {
+ v := allTierStats{}
+ 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 BenchmarkUnmarshalallTierStats(b *testing.B) {
+ v := allTierStats{}
+ 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 TestEncodeDecodeallTierStats(t *testing.T) {
+ v := allTierStats{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeallTierStats Msgsize() is inaccurate")
+ }
+
+ vn := allTierStats{}
+ 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 BenchmarkEncodeallTierStats(b *testing.B) {
+ v := allTierStats{}
+ 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 BenchmarkDecodeallTierStats(b *testing.B) {
+ v := allTierStats{}
+ 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 TestMarshalUnmarshalcurrentScannerCycle(t *testing.T) {
+ v := currentScannerCycle{}
+ 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 BenchmarkMarshalMsgcurrentScannerCycle(b *testing.B) {
+ v := currentScannerCycle{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgcurrentScannerCycle(b *testing.B) {
+ v := currentScannerCycle{}
+ 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 BenchmarkUnmarshalcurrentScannerCycle(b *testing.B) {
+ v := currentScannerCycle{}
+ 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 TestMarshalUnmarshaldataUsageCache(t *testing.T) {
+ v := dataUsageCache{}
+ 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 BenchmarkMarshalMsgdataUsageCache(b *testing.B) {
+ v := dataUsageCache{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgdataUsageCache(b *testing.B) {
+ v := dataUsageCache{}
+ 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 BenchmarkUnmarshaldataUsageCache(b *testing.B) {
+ v := dataUsageCache{}
+ 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 TestEncodeDecodedataUsageCache(t *testing.T) {
+ v := dataUsageCache{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodedataUsageCache Msgsize() is inaccurate")
+ }
+
+ vn := dataUsageCache{}
+ 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 BenchmarkEncodedataUsageCache(b *testing.B) {
+ v := dataUsageCache{}
+ 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 BenchmarkDecodedataUsageCache(b *testing.B) {
+ v := dataUsageCache{}
+ 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 TestMarshalUnmarshaldataUsageCacheInfo(t *testing.T) {
+ v := dataUsageCacheInfo{}
+ bts, err := v.MarshalMsg(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ left, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
+ }
+
+ left, err = msgp.Skip(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
+ }
+}
+
+func BenchmarkMarshalMsgdataUsageCacheInfo(b *testing.B) {
+ v := dataUsageCacheInfo{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgdataUsageCacheInfo(b *testing.B) {
+ v := dataUsageCacheInfo{}
+ bts := make([]byte, 0, v.Msgsize())
+ bts, _ = v.MarshalMsg(bts[0:0])
+ b.SetBytes(int64(len(bts)))
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ bts, _ = v.MarshalMsg(bts[0:0])
+ }
+}
+
+func BenchmarkUnmarshaldataUsageCacheInfo(b *testing.B) {
+ v := dataUsageCacheInfo{}
+ bts, _ := v.MarshalMsg(nil)
+ b.ReportAllocs()
+ b.SetBytes(int64(len(bts)))
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func TestEncodeDecodedataUsageCacheInfo(t *testing.T) {
+ v := dataUsageCacheInfo{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodedataUsageCacheInfo Msgsize() is inaccurate")
+ }
+
+ vn := dataUsageCacheInfo{}
+ err := msgp.Decode(&buf, &vn)
+ if err != nil {
+ t.Error(err)
+ }
+
+ buf.Reset()
+ msgp.Encode(&buf, &v)
+ err = msgp.NewReader(&buf).Skip()
+ if err != nil {
+ t.Error(err)
+ }
+}
+
+func BenchmarkEncodedataUsageCacheInfo(b *testing.B) {
+ v := dataUsageCacheInfo{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+ b.SetBytes(int64(buf.Len()))
+ en := msgp.NewWriter(msgp.Nowhere)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.EncodeMsg(en)
+ }
+ en.Flush()
+}
+
+func BenchmarkDecodedataUsageCacheInfo(b *testing.B) {
+ v := dataUsageCacheInfo{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+ b.SetBytes(int64(buf.Len()))
+ rd := msgp.NewEndlessReader(buf.Bytes(), b)
+ dc := msgp.NewReader(rd)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ err := v.DecodeMsg(dc)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func TestMarshalUnmarshaldataUsageEntry(t *testing.T) {
+ v := dataUsageEntry{}
+ bts, err := v.MarshalMsg(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ left, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
+ }
+
+ left, err = msgp.Skip(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
+ }
+}
+
+func BenchmarkMarshalMsgdataUsageEntry(b *testing.B) {
+ v := dataUsageEntry{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgdataUsageEntry(b *testing.B) {
+ v := dataUsageEntry{}
+ bts := make([]byte, 0, v.Msgsize())
+ bts, _ = v.MarshalMsg(bts[0:0])
+ b.SetBytes(int64(len(bts)))
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ bts, _ = v.MarshalMsg(bts[0:0])
+ }
+}
+
+func BenchmarkUnmarshaldataUsageEntry(b *testing.B) {
+ v := dataUsageEntry{}
+ bts, _ := v.MarshalMsg(nil)
+ b.ReportAllocs()
+ b.SetBytes(int64(len(bts)))
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func TestEncodeDecodedataUsageEntry(t *testing.T) {
+ v := dataUsageEntry{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodedataUsageEntry Msgsize() is inaccurate")
+ }
+
+ vn := dataUsageEntry{}
+ err := msgp.Decode(&buf, &vn)
+ if err != nil {
+ t.Error(err)
+ }
+
+ buf.Reset()
+ msgp.Encode(&buf, &v)
+ err = msgp.NewReader(&buf).Skip()
+ if err != nil {
+ t.Error(err)
+ }
+}
+
+func BenchmarkEncodedataUsageEntry(b *testing.B) {
+ v := dataUsageEntry{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+ b.SetBytes(int64(buf.Len()))
+ en := msgp.NewWriter(msgp.Nowhere)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.EncodeMsg(en)
+ }
+ en.Flush()
+}
+
+func BenchmarkDecodedataUsageEntry(b *testing.B) {
+ v := dataUsageEntry{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+ b.SetBytes(int64(buf.Len()))
+ rd := msgp.NewEndlessReader(buf.Bytes(), b)
+ dc := msgp.NewReader(rd)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ err := v.DecodeMsg(dc)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func TestMarshalUnmarshalsizeHistogram(t *testing.T) {
+ v := sizeHistogram{}
+ bts, err := v.MarshalMsg(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ left, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
+ }
+
+ left, err = msgp.Skip(bts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(left) > 0 {
+ t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
+ }
+}
+
+func BenchmarkMarshalMsgsizeHistogram(b *testing.B) {
+ v := sizeHistogram{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgsizeHistogram(b *testing.B) {
+ v := sizeHistogram{}
+ bts := make([]byte, 0, v.Msgsize())
+ bts, _ = v.MarshalMsg(bts[0:0])
+ b.SetBytes(int64(len(bts)))
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ bts, _ = v.MarshalMsg(bts[0:0])
+ }
+}
+
+func BenchmarkUnmarshalsizeHistogram(b *testing.B) {
+ v := sizeHistogram{}
+ bts, _ := v.MarshalMsg(nil)
+ b.ReportAllocs()
+ b.SetBytes(int64(len(bts)))
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, err := v.UnmarshalMsg(bts)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func TestEncodeDecodesizeHistogram(t *testing.T) {
+ v := sizeHistogram{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodesizeHistogram Msgsize() is inaccurate")
+ }
+
+ vn := sizeHistogram{}
+ err := msgp.Decode(&buf, &vn)
+ if err != nil {
+ t.Error(err)
+ }
+
+ buf.Reset()
+ msgp.Encode(&buf, &v)
+ err = msgp.NewReader(&buf).Skip()
+ if err != nil {
+ t.Error(err)
+ }
+}
+
+func BenchmarkEncodesizeHistogram(b *testing.B) {
+ v := sizeHistogram{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+ b.SetBytes(int64(buf.Len()))
+ en := msgp.NewWriter(msgp.Nowhere)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.EncodeMsg(en)
+ }
+ en.Flush()
+}
+
+func BenchmarkDecodesizeHistogram(b *testing.B) {
+ v := sizeHistogram{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+ b.SetBytes(int64(buf.Len()))
+ rd := msgp.NewEndlessReader(buf.Bytes(), b)
+ dc := msgp.NewReader(rd)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ err := v.DecodeMsg(dc)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func TestMarshalUnmarshalsizeHistogramV1(t *testing.T) {
+ v := sizeHistogramV1{}
+ 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 BenchmarkMarshalMsgsizeHistogramV1(b *testing.B) {
+ v := sizeHistogramV1{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgsizeHistogramV1(b *testing.B) {
+ v := sizeHistogramV1{}
+ 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 BenchmarkUnmarshalsizeHistogramV1(b *testing.B) {
+ v := sizeHistogramV1{}
+ 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 TestEncodeDecodesizeHistogramV1(t *testing.T) {
+ v := sizeHistogramV1{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodesizeHistogramV1 Msgsize() is inaccurate")
+ }
+
+ vn := sizeHistogramV1{}
+ 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 BenchmarkEncodesizeHistogramV1(b *testing.B) {
+ v := sizeHistogramV1{}
+ 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 BenchmarkDecodesizeHistogramV1(b *testing.B) {
+ v := sizeHistogramV1{}
+ 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 TestMarshalUnmarshaltierStats(t *testing.T) {
+ v := tierStats{}
+ 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 BenchmarkMarshalMsgtierStats(b *testing.B) {
+ v := tierStats{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgtierStats(b *testing.B) {
+ v := tierStats{}
+ 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 BenchmarkUnmarshaltierStats(b *testing.B) {
+ v := tierStats{}
+ 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 TestEncodeDecodetierStats(t *testing.T) {
+ v := tierStats{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodetierStats Msgsize() is inaccurate")
+ }
+
+ vn := tierStats{}
+ 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 BenchmarkEncodetierStats(b *testing.B) {
+ v := tierStats{}
+ 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 BenchmarkDecodetierStats(b *testing.B) {
+ v := tierStats{}
+ 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 TestMarshalUnmarshalversionsHistogram(t *testing.T) {
+ v := versionsHistogram{}
+ 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 BenchmarkMarshalMsgversionsHistogram(b *testing.B) {
+ v := versionsHistogram{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgversionsHistogram(b *testing.B) {
+ v := versionsHistogram{}
+ 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 BenchmarkUnmarshalversionsHistogram(b *testing.B) {
+ v := versionsHistogram{}
+ 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 TestEncodeDecodeversionsHistogram(t *testing.T) {
+ v := versionsHistogram{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeversionsHistogram Msgsize() is inaccurate")
+ }
+
+ vn := versionsHistogram{}
+ 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 BenchmarkEncodeversionsHistogram(b *testing.B) {
+ v := versionsHistogram{}
+ 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 BenchmarkDecodeversionsHistogram(b *testing.B) {
+ v := versionsHistogram{}
+ 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)
+ }
+ }
+}
diff --git a/cmd/data-usage-cache_test.go b/cmd/data-usage-cache_test.go
new file mode 100644
index 00000000..69fbd9fe
--- /dev/null
+++ b/cmd/data-usage-cache_test.go
@@ -0,0 +1,98 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/dustin/go-humanize"
+)
+
+func TestSizeHistogramToMap(t *testing.T) {
+ tests := []struct {
+ sizes []int64
+ want map[string]uint64
+ }{
+ {
+ sizes: []int64{100, 1000, 72_000, 100_000},
+ want: map[string]uint64{
+ "LESS_THAN_1024_B": 2,
+ "BETWEEN_64_KB_AND_256_KB": 2,
+ "BETWEEN_1024B_AND_1_MB": 2,
+ },
+ },
+ {
+ sizes: []int64{100, 1000, 2000, 100_000, 13 * humanize.MiByte},
+ want: map[string]uint64{
+ "LESS_THAN_1024_B": 2,
+ "BETWEEN_1024_B_AND_64_KB": 1,
+ "BETWEEN_64_KB_AND_256_KB": 1,
+ "BETWEEN_1024B_AND_1_MB": 2,
+ "BETWEEN_10_MB_AND_64_MB": 1,
+ },
+ },
+ }
+ for i, test := range tests {
+ t.Run(fmt.Sprintf("Test-%d", i), func(t *testing.T) {
+ var h sizeHistogram
+ for _, sz := range test.sizes {
+ h.add(sz)
+ }
+ got := h.toMap()
+ exp := test.want
+ // what is in exp is in got
+ for k := range exp {
+ if exp[k] != got[k] {
+ t.Fatalf("interval %s: Expected %d values but got %d values\n", k, exp[k], got[k])
+ }
+ }
+ // what is absent in exp is absent in got too
+ for k := range got {
+ if _, ok := exp[k]; !ok && got[k] > 0 {
+ t.Fatalf("Unexpected interval: %s has value %d\n", k, got[k])
+ }
+ }
+ })
+ }
+}
+
+func TestMigrateSizeHistogramFromV1(t *testing.T) {
+ tests := []struct {
+ v sizeHistogramV1
+ want sizeHistogram
+ }{
+ {
+ v: sizeHistogramV1{0: 10, 1: 20, 2: 3},
+ want: sizeHistogram{0: 10, 5: 20, 6: 3},
+ },
+ {
+ v: sizeHistogramV1{0: 10, 1: 20, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7},
+ want: sizeHistogram{0: 10, 5: 20, 6: 3, 7: 4, 8: 5, 9: 6, 10: 7},
+ },
+ }
+ for i, test := range tests {
+ t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) {
+ var got sizeHistogram
+ got.mergeV1(test.v)
+ if got != test.want {
+ t.Fatalf("Expected %v but got %v", test.want, got)
+ }
+ })
+ }
+}
diff --git a/cmd/data-usage-utils.go b/cmd/data-usage-utils.go
new file mode 100644
index 00000000..1f6b3f11
--- /dev/null
+++ b/cmd/data-usage-utils.go
@@ -0,0 +1,169 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "sort"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+)
+
+// BucketTargetUsageInfo - bucket target usage info provides
+// - replicated size for all objects sent to this target
+// - replica size for all objects received from this target
+// - replication pending size for all objects pending replication to this target
+// - replication failed size for all objects failed replication to this target
+// - replica pending count
+// - replica failed count
+type BucketTargetUsageInfo struct {
+ ReplicationPendingSize uint64 `json:"objectsPendingReplicationTotalSize"`
+ ReplicationFailedSize uint64 `json:"objectsFailedReplicationTotalSize"`
+ ReplicatedSize uint64 `json:"objectsReplicatedTotalSize"`
+ ReplicaSize uint64 `json:"objectReplicaTotalSize"`
+ ReplicationPendingCount uint64 `json:"objectsPendingReplicationCount"`
+ ReplicationFailedCount uint64 `json:"objectsFailedReplicationCount"`
+ ReplicatedCount uint64 `json:"objectsReplicatedCount"`
+}
+
+// BucketUsageInfo - bucket usage info provides
+// - total size of the bucket
+// - total objects in a bucket
+// - object size histogram per bucket
+type BucketUsageInfo struct {
+ Size uint64 `json:"size"`
+ // Following five fields suffixed with V1 are here for backward compatibility
+ // Total Size for objects that have not yet been replicated
+ ReplicationPendingSizeV1 uint64 `json:"objectsPendingReplicationTotalSize"`
+ // Total size for objects that have witness one or more failures and will be retried
+ ReplicationFailedSizeV1 uint64 `json:"objectsFailedReplicationTotalSize"`
+ // Total size for objects that have been replicated to destination
+ ReplicatedSizeV1 uint64 `json:"objectsReplicatedTotalSize"`
+ // Total number of objects pending replication
+ ReplicationPendingCountV1 uint64 `json:"objectsPendingReplicationCount"`
+ // Total number of objects that failed replication
+ ReplicationFailedCountV1 uint64 `json:"objectsFailedReplicationCount"`
+
+ ObjectsCount uint64 `json:"objectsCount"`
+ ObjectSizesHistogram map[string]uint64 `json:"objectsSizesHistogram"`
+ ObjectVersionsHistogram map[string]uint64 `json:"objectsVersionsHistogram"`
+ VersionsCount uint64 `json:"versionsCount"`
+ DeleteMarkersCount uint64 `json:"deleteMarkersCount"`
+ ReplicaSize uint64 `json:"objectReplicaTotalSize"`
+ ReplicaCount uint64 `json:"objectReplicaCount"`
+ ReplicationInfo map[string]BucketTargetUsageInfo `json:"objectsReplicationInfo"`
+}
+
+// DataUsageInfo represents data usage stats of the underlying Object API
+type DataUsageInfo struct {
+ TotalCapacity uint64 `json:"capacity,omitempty"`
+ TotalUsedCapacity uint64 `json:"usedCapacity,omitempty"`
+ TotalFreeCapacity uint64 `json:"freeCapacity,omitempty"`
+
+ // LastUpdate is the timestamp of when the data usage info was last updated.
+ // This does not indicate a full scan.
+ LastUpdate time.Time `json:"lastUpdate"`
+
+ // Objects total count across all buckets
+ ObjectsTotalCount uint64 `json:"objectsCount"`
+
+ // Versions total count across all buckets
+ VersionsTotalCount uint64 `json:"versionsCount"`
+
+ // Delete markers total count across all buckets
+ DeleteMarkersTotalCount uint64 `json:"deleteMarkersCount"`
+
+ // Objects total size across all buckets
+ ObjectsTotalSize uint64 `json:"objectsTotalSize"`
+ ReplicationInfo map[string]BucketTargetUsageInfo `json:"objectsReplicationInfo"`
+
+ // Total number of buckets in this cluster
+ BucketsCount uint64 `json:"bucketsCount"`
+
+ // Buckets usage info provides following information across all buckets
+ // - total size of the bucket
+ // - total objects in a bucket
+ // - object size histogram per bucket
+ BucketsUsage map[string]BucketUsageInfo `json:"bucketsUsageInfo"`
+ // Deprecated kept here for backward compatibility reasons.
+ BucketSizes map[string]uint64 `json:"bucketsSizes"`
+
+ // TierStats contains per-tier stats of all configured remote tiers
+ TierStats *allTierStats `json:"tierStats,omitempty"`
+}
+
+func (dui DataUsageInfo) tierStats() []madmin.TierInfo {
+ if dui.TierStats == nil {
+ return nil
+ }
+
+ if globalTierConfigMgr.Empty() {
+ return nil
+ }
+
+ ts := make(map[string]madmin.TierStats)
+ dui.TierStats.populateStats(ts)
+
+ infos := make([]madmin.TierInfo, 0, len(ts))
+ for tier, stats := range ts {
+ infos = append(infos, madmin.TierInfo{
+ Name: tier,
+ Type: globalTierConfigMgr.TierType(tier),
+ Stats: stats,
+ })
+ }
+
+ sort.Slice(infos, func(i, j int) bool {
+ if infos[i].Type == "internal" {
+ return true
+ }
+ if infos[j].Type == "internal" {
+ return false
+ }
+ return infos[i].Name < infos[j].Name
+ })
+ return infos
+}
+
+func (dui DataUsageInfo) tierMetrics() (metrics []MetricV2) {
+ if dui.TierStats == nil {
+ return nil
+ }
+ // e.g minio_cluster_ilm_transitioned_bytes{tier="S3TIER-1"}=136314880
+ // minio_cluster_ilm_transitioned_objects{tier="S3TIER-1"}=1
+ // minio_cluster_ilm_transitioned_versions{tier="S3TIER-1"}=3
+ for tier, st := range dui.TierStats.Tiers {
+ metrics = append(metrics, MetricV2{
+ Description: getClusterTransitionedBytesMD(),
+ Value: float64(st.TotalSize),
+ VariableLabels: map[string]string{"tier": tier},
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getClusterTransitionedObjectsMD(),
+ Value: float64(st.NumObjects),
+ VariableLabels: map[string]string{"tier": tier},
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getClusterTransitionedVersionsMD(),
+ Value: float64(st.NumVersions),
+ VariableLabels: map[string]string{"tier": tier},
+ })
+ }
+
+ return metrics
+}
diff --git a/cmd/data-usage.go b/cmd/data-usage.go
new file mode 100644
index 00000000..51227e10
--- /dev/null
+++ b/cmd/data-usage.go
@@ -0,0 +1,165 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "errors"
+ "strings"
+ "time"
+
+ jsoniter "github.com/json-iterator/go"
+ "github.com/minio/minio/internal/cachevalue"
+)
+
+const (
+ dataUsageRoot = SlashSeparator
+ dataUsageBucket = minioMetaBucket + SlashSeparator + bucketMetaPrefix
+
+ dataUsageObjName = ".usage.json"
+ dataUsageObjNamePath = bucketMetaPrefix + SlashSeparator + dataUsageObjName
+ dataUsageBloomName = ".bloomcycle.bin"
+ dataUsageBloomNamePath = bucketMetaPrefix + SlashSeparator + dataUsageBloomName
+
+ backgroundHealInfoPath = bucketMetaPrefix + SlashSeparator + ".background-heal.json"
+
+ dataUsageCacheName = ".usage-cache.bin"
+)
+
+// storeDataUsageInBackend will store all objects sent on the dui channel until closed.
+func storeDataUsageInBackend(ctx context.Context, objAPI ObjectLayer, dui <-chan DataUsageInfo) {
+ attempts := 1
+ for dataUsageInfo := range dui {
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ dataUsageJSON, err := json.Marshal(dataUsageInfo)
+ if err != nil {
+ scannerLogIf(ctx, err)
+ continue
+ }
+ if attempts > 10 {
+ saveConfig(ctx, objAPI, dataUsageObjNamePath+".bkp", dataUsageJSON) // Save a backup every 10th update.
+ attempts = 1
+ }
+ if err = saveConfig(ctx, objAPI, dataUsageObjNamePath, dataUsageJSON); err != nil {
+ scannerLogOnceIf(ctx, err, dataUsageObjNamePath)
+ }
+ attempts++
+ }
+}
+
+var prefixUsageCache = cachevalue.New[map[string]uint64]()
+
+// loadPrefixUsageFromBackend returns prefix usages found in passed buckets
+//
+// e.g.: /testbucket/prefix => 355601334
+func loadPrefixUsageFromBackend(ctx context.Context, objAPI ObjectLayer, bucket string) (map[string]uint64, error) {
+ z, ok := objAPI.(*erasureServerPools)
+ if !ok {
+ // Prefix usage is empty
+ return map[string]uint64{}, nil
+ }
+
+ cache := dataUsageCache{}
+
+ prefixUsageCache.InitOnce(30*time.Second,
+ // No need to fail upon Update() error, fallback to old value.
+ cachevalue.Opts{ReturnLastGood: true, NoWait: true},
+ func(ctx context.Context) (map[string]uint64, error) {
+ m := make(map[string]uint64)
+ for _, pool := range z.serverPools {
+ for _, er := range pool.sets {
+ // Load bucket usage prefixes
+ ctx, done := context.WithTimeout(ctx, 2*time.Second)
+ ok := cache.load(ctx, er, bucket+slashSeparator+dataUsageCacheName) == nil
+ done()
+ if ok {
+ root := cache.find(bucket)
+ if root == nil {
+ // We dont have usage information for this bucket in this
+ // set, go to the next set
+ continue
+ }
+
+ for id, usageInfo := range cache.flattenChildrens(*root) {
+ prefix := decodeDirObject(strings.TrimPrefix(id, bucket+slashSeparator))
+ // decodeDirObject to avoid any __XLDIR__ objects
+ m[prefix] += uint64(usageInfo.Size)
+ }
+ }
+ }
+ }
+ return m, nil
+ },
+ )
+
+ return prefixUsageCache.GetWithCtx(ctx)
+}
+
+func loadDataUsageFromBackend(ctx context.Context, objAPI ObjectLayer) (DataUsageInfo, error) {
+ buf, err := readConfig(ctx, objAPI, dataUsageObjNamePath)
+ if err != nil {
+ buf, err = readConfig(ctx, objAPI, dataUsageObjNamePath+".bkp")
+ if err != nil {
+ if errors.Is(err, errConfigNotFound) {
+ return DataUsageInfo{}, nil
+ }
+ return DataUsageInfo{}, toObjectErr(err, minioMetaBucket, dataUsageObjNamePath)
+ }
+ }
+
+ var dataUsageInfo DataUsageInfo
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ if err = json.Unmarshal(buf, &dataUsageInfo); err != nil {
+ return DataUsageInfo{}, err
+ }
+ // For forward compatibility reasons, we need to add this code.
+ if len(dataUsageInfo.BucketsUsage) == 0 {
+ dataUsageInfo.BucketsUsage = make(map[string]BucketUsageInfo, len(dataUsageInfo.BucketSizes))
+ for bucket, size := range dataUsageInfo.BucketSizes {
+ dataUsageInfo.BucketsUsage[bucket] = BucketUsageInfo{Size: size}
+ }
+ }
+
+ // For backward compatibility reasons, we need to add this code.
+ if len(dataUsageInfo.BucketSizes) == 0 {
+ dataUsageInfo.BucketSizes = make(map[string]uint64, len(dataUsageInfo.BucketsUsage))
+ for bucket, bui := range dataUsageInfo.BucketsUsage {
+ dataUsageInfo.BucketSizes[bucket] = bui.Size
+ }
+ }
+ // For forward compatibility reasons, we need to add this code.
+ for bucket, bui := range dataUsageInfo.BucketsUsage {
+ if bui.ReplicatedSizeV1 > 0 || bui.ReplicationFailedCountV1 > 0 ||
+ bui.ReplicationFailedSizeV1 > 0 || bui.ReplicationPendingCountV1 > 0 {
+ cfg, _ := getReplicationConfig(GlobalContext, bucket)
+ if cfg != nil && cfg.RoleArn != "" {
+ if dataUsageInfo.ReplicationInfo == nil {
+ dataUsageInfo.ReplicationInfo = make(map[string]BucketTargetUsageInfo)
+ }
+ dataUsageInfo.ReplicationInfo[cfg.RoleArn] = BucketTargetUsageInfo{
+ ReplicationFailedSize: bui.ReplicationFailedSizeV1,
+ ReplicationFailedCount: bui.ReplicationFailedCountV1,
+ ReplicatedSize: bui.ReplicatedSizeV1,
+ ReplicationPendingCount: bui.ReplicationPendingCountV1,
+ ReplicationPendingSize: bui.ReplicationPendingSizeV1,
+ }
+ }
+ }
+ }
+ return dataUsageInfo, nil
+}
diff --git a/cmd/data-usage_test.go b/cmd/data-usage_test.go
new file mode 100644
index 00000000..51a46e65
--- /dev/null
+++ b/cmd/data-usage_test.go
@@ -0,0 +1,631 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "os"
+ "path"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/minio/minio/internal/cachevalue"
+)
+
+type usageTestFile struct {
+ name string
+ size int
+}
+
+func TestDataUsageUpdate(t *testing.T) {
+ base := t.TempDir()
+ const bucket = "bucket"
+ files := []usageTestFile{
+ {name: "rootfile", size: 10000},
+ {name: "rootfile2", size: 10000},
+ {name: "dir1/d1file", size: 2000},
+ {name: "dir2/d2file", size: 300},
+ {name: "dir1/dira/dafile", size: 100000},
+ {name: "dir1/dira/dbfile", size: 200000},
+ {name: "dir1/dira/dirasub/dcfile", size: 1000000},
+ {name: "dir1/dira/dirasub/sublevel3/dccccfile", size: 10},
+ }
+ createUsageTestFiles(t, base, bucket, files)
+
+ getSize := func(item scannerItem) (sizeS sizeSummary, err error) {
+ if item.Typ&os.ModeDir == 0 {
+ var s os.FileInfo
+ s, err = os.Stat(item.Path)
+ if err != nil {
+ return sizeS, err
+ }
+ sizeS.totalSize = s.Size()
+ sizeS.versions++
+ return sizeS, nil
+ }
+ return sizeS, err
+ }
+ xls := xlStorage{drivePath: base, diskInfoCache: cachevalue.New[DiskInfo]()}
+ xls.diskInfoCache.InitOnce(time.Second, cachevalue.Opts{}, func(ctx context.Context) (DiskInfo, error) {
+ return DiskInfo{Total: 1 << 40, Free: 1 << 40}, nil
+ })
+ weSleep := func() bool { return false }
+
+ got, err := scanDataFolder(t.Context(), nil, &xls, dataUsageCache{Info: dataUsageCacheInfo{Name: bucket}}, getSize, 0, weSleep)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Test dirs
+ want := []struct {
+ path string
+ isNil bool
+ size, objs int
+ flatten bool
+ oSizes sizeHistogram
+ }{
+ {
+ path: "/",
+ size: 1322310,
+ flatten: true,
+ objs: 8,
+ oSizes: sizeHistogram{0: 2, 1: 3, 2: 2, 4: 1},
+ },
+ {
+ path: "/",
+ size: 20000,
+ objs: 2,
+ oSizes: sizeHistogram{1: 2},
+ },
+ {
+ path: "/dir1",
+ size: 1302010,
+ objs: 5,
+ oSizes: sizeHistogram{0: 1, 1: 1, 2: 2, 4: 1},
+ },
+ {
+ path: "/dir1/dira",
+ isNil: true,
+ },
+ {
+ path: "/nonexistying",
+ isNil: true,
+ },
+ }
+
+ for _, w := range want {
+ p := path.Join(bucket, w.path)
+ t.Run(p, func(t *testing.T) {
+ e := got.find(p)
+ if w.isNil {
+ if e != nil {
+ t.Error("want nil, got", e)
+ }
+ return
+ }
+ if e == nil {
+ t.Fatal("got nil result")
+ }
+ if w.flatten {
+ *e = got.flatten(*e)
+ }
+ if e.Size != int64(w.size) {
+ t.Error("got size", e.Size, "want", w.size)
+ }
+ if e.Objects != uint64(w.objs) {
+ t.Error("got objects", e.Objects, "want", w.objs)
+ }
+ if e.Versions != uint64(w.objs) {
+ t.Error("got versions", e.Versions, "want", w.objs)
+ }
+ if e.ObjSizes != w.oSizes {
+ t.Error("got histogram", e.ObjSizes, "want", w.oSizes)
+ }
+ })
+ }
+
+ files = []usageTestFile{
+ {
+ name: "newfolder/afile",
+ size: 4,
+ },
+ {
+ name: "newfolder/anotherone",
+ size: 1,
+ },
+ {
+ name: "newfolder/anemptyone",
+ size: 0,
+ },
+ {
+ name: "dir1/fileindir1",
+ size: 20000,
+ },
+ {
+ name: "dir1/dirc/fileindirc",
+ size: 20000,
+ },
+ {
+ name: "rootfile3",
+ size: 1000,
+ },
+ {
+ name: "dir1/dira/dirasub/fileindira2",
+ size: 200,
+ },
+ }
+ createUsageTestFiles(t, base, bucket, files)
+ err = os.RemoveAll(filepath.Join(base, bucket, "dir1/dira/dirasub/dcfile"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Changed dir must be picked up in this many cycles.
+ for range dataUsageUpdateDirCycles {
+ got, err = scanDataFolder(t.Context(), nil, &xls, got, getSize, 0, weSleep)
+ got.Info.NextCycle++
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ want = []struct {
+ path string
+ isNil bool
+ size, objs int
+ flatten bool
+ oSizes sizeHistogram
+ }{
+ {
+ path: "/",
+ size: 363515,
+ flatten: true,
+ objs: 14,
+ oSizes: sizeHistogram{0: 7, 1: 5, 2: 2},
+ },
+ {
+ path: "/dir1",
+ size: 342210,
+ objs: 7,
+ flatten: false,
+ oSizes: sizeHistogram{0: 2, 1: 3, 2: 2},
+ },
+ {
+ path: "/newfolder",
+ size: 5,
+ objs: 3,
+ oSizes: sizeHistogram{0: 3},
+ },
+ {
+ path: "/nonexistying",
+ isNil: true,
+ },
+ }
+
+ for _, w := range want {
+ p := path.Join(bucket, w.path)
+ t.Run(p, func(t *testing.T) {
+ e := got.find(p)
+ if w.isNil {
+ if e != nil {
+ t.Error("want nil, got", e)
+ }
+ return
+ }
+ if e == nil {
+ t.Fatal("got nil result")
+ }
+ if w.flatten {
+ *e = got.flatten(*e)
+ }
+ if e.Size != int64(w.size) {
+ t.Error("got size", e.Size, "want", w.size)
+ }
+ if e.Objects != uint64(w.objs) {
+ t.Error("got objects", e.Objects, "want", w.objs)
+ }
+ if e.Versions != uint64(w.objs) {
+ t.Error("got versions", e.Versions, "want", w.objs)
+ }
+ if e.ObjSizes != w.oSizes {
+ t.Error("got histogram", e.ObjSizes, "want", w.oSizes)
+ }
+ })
+ }
+}
+
+func TestDataUsageUpdatePrefix(t *testing.T) {
+ base := t.TempDir()
+ scannerSleeper.Update(0, 0)
+ files := []usageTestFile{
+ {name: "bucket/rootfile", size: 10000},
+ {name: "bucket/rootfile2", size: 10000},
+ {name: "bucket/dir1/d1file", size: 2000},
+ {name: "bucket/dir2/d2file", size: 300},
+ {name: "bucket/dir1/dira/dafile", size: 100000},
+ {name: "bucket/dir1/dira/dbfile", size: 200000},
+ {name: "bucket/dir1/dira/dirasub/dcfile", size: 1000000},
+ {name: "bucket/dir1/dira/dirasub/sublevel3/dccccfile", size: 10},
+ }
+ createUsageTestFiles(t, base, "", files)
+ const foldersBelow = 3
+ const filesBelowT = dataScannerCompactLeastObject / 2
+ const filesAboveT = dataScannerCompactAtFolders + 1
+ const expectSize = foldersBelow*filesBelowT + filesAboveT
+
+ generateUsageTestFiles(t, base, "bucket/dirwithalot", foldersBelow, filesBelowT, 1)
+ generateUsageTestFiles(t, base, "bucket/dirwithevenmore", filesAboveT, 1, 1)
+
+ getSize := func(item scannerItem) (sizeS sizeSummary, err error) {
+ if item.Typ&os.ModeDir == 0 {
+ var s os.FileInfo
+ s, err = os.Stat(item.Path)
+ if err != nil {
+ return sizeS, err
+ }
+ sizeS.totalSize = s.Size()
+ sizeS.versions++
+ return sizeS, err
+ }
+ return sizeS, err
+ }
+
+ weSleep := func() bool { return false }
+ xls := xlStorage{drivePath: base, diskInfoCache: cachevalue.New[DiskInfo]()}
+ xls.diskInfoCache.InitOnce(time.Second, cachevalue.Opts{}, func(ctx context.Context) (DiskInfo, error) {
+ return DiskInfo{Total: 1 << 40, Free: 1 << 40}, nil
+ })
+
+ got, err := scanDataFolder(t.Context(), nil, &xls, dataUsageCache{Info: dataUsageCacheInfo{Name: "bucket"}}, getSize, 0, weSleep)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got.root() == nil {
+ t.Log("cached folders:")
+ for folder := range got.Cache {
+ t.Log("folder:", folder)
+ }
+ t.Fatal("got nil root.")
+ }
+
+ // Test dirs
+ want := []struct {
+ path string
+ isNil bool
+ size, objs int
+ oSizes sizeHistogram
+ }{
+ {
+ path: "flat",
+ size: 1322310 + expectSize,
+ objs: 8 + expectSize,
+ oSizes: sizeHistogram{0: 2 + expectSize, 1: 3, 2: 2, 4: 1},
+ },
+ {
+ path: "bucket/",
+ size: 20000,
+ objs: 2,
+ oSizes: sizeHistogram{1: 2},
+ },
+ {
+ // Gets compacted...
+ path: "bucket/dir1",
+ size: 1302010,
+ objs: 5,
+ oSizes: sizeHistogram{0: 1, 1: 1, 2: 2, 4: 1},
+ },
+ {
+ // Gets compacted at this level...
+ path: "bucket/dirwithalot/0",
+ size: filesBelowT,
+ objs: filesBelowT,
+ oSizes: sizeHistogram{0: filesBelowT},
+ },
+ {
+ // Gets compacted at this level (below obj threshold)...
+ path: "bucket/dirwithalot/0",
+ size: filesBelowT,
+ objs: filesBelowT,
+ oSizes: sizeHistogram{0: filesBelowT},
+ },
+ {
+ // Gets compacted at this level...
+ path: "bucket/dirwithevenmore",
+ size: filesAboveT,
+ objs: filesAboveT,
+ oSizes: sizeHistogram{0: filesAboveT},
+ },
+ {
+ path: "bucket/nonexistying",
+ isNil: true,
+ },
+ }
+
+ for _, w := range want {
+ t.Run(w.path, func(t *testing.T) {
+ e := got.find(w.path)
+ if w.path == "flat" {
+ f := got.flatten(*got.root())
+ e = &f
+ }
+ if w.isNil {
+ if e != nil {
+ t.Error("want nil, got", e)
+ }
+ return
+ }
+ if e == nil {
+ t.Fatal("got nil result")
+ return
+ }
+ if e.Size != int64(w.size) {
+ t.Error("got size", e.Size, "want", w.size)
+ }
+ if e.Objects != uint64(w.objs) {
+ t.Error("got objects", e.Objects, "want", w.objs)
+ }
+ if e.Versions != uint64(w.objs) {
+ t.Error("got versions", e.Versions, "want", w.objs)
+ }
+ if e.ObjSizes != w.oSizes {
+ t.Error("got histogram", e.ObjSizes, "want", w.oSizes)
+ }
+ })
+ }
+
+ files = []usageTestFile{
+ {
+ name: "bucket/newfolder/afile",
+ size: 4,
+ },
+ {
+ name: "bucket/newfolder/anotherone",
+ size: 1,
+ },
+ {
+ name: "bucket/newfolder/anemptyone",
+ size: 0,
+ },
+ {
+ name: "bucket/dir1/fileindir1",
+ size: 20000,
+ },
+ {
+ name: "bucket/dir1/dirc/fileindirc",
+ size: 20000,
+ },
+ {
+ name: "bucket/rootfile3",
+ size: 1000,
+ },
+ {
+ name: "bucket/dir1/dira/dirasub/fileindira2",
+ size: 200,
+ },
+ }
+
+ createUsageTestFiles(t, base, "", files)
+ err = os.RemoveAll(filepath.Join(base, "bucket/dir1/dira/dirasub/dcfile"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Changed dir must be picked up in this many cycles.
+ for range dataUsageUpdateDirCycles {
+ got, err = scanDataFolder(t.Context(), nil, &xls, got, getSize, 0, weSleep)
+ got.Info.NextCycle++
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ want = []struct {
+ path string
+ isNil bool
+ size, objs int
+ oSizes sizeHistogram
+ }{
+ {
+ path: "flat",
+ size: 363515 + expectSize,
+ objs: 14 + expectSize,
+ oSizes: sizeHistogram{0: 7 + expectSize, 1: 5, 2: 2},
+ },
+ {
+ path: "bucket/dir1",
+ size: 342210,
+ objs: 7,
+ oSizes: sizeHistogram{0: 2, 1: 3, 2: 2},
+ },
+ {
+ path: "bucket/",
+ size: 21000,
+ objs: 3,
+ oSizes: sizeHistogram{0: 1, 1: 2},
+ },
+ {
+ path: "bucket/newfolder",
+ size: 5,
+ objs: 3,
+ oSizes: sizeHistogram{0: 3},
+ },
+ {
+ // Compacted into bucket/dir1
+ path: "bucket/dir1/dira",
+ isNil: true,
+ },
+ {
+ path: "bucket/nonexistying",
+ isNil: true,
+ },
+ }
+
+ for _, w := range want {
+ t.Run(w.path, func(t *testing.T) {
+ e := got.find(w.path)
+ if w.path == "flat" {
+ f := got.flatten(*got.root())
+ e = &f
+ }
+ if w.isNil {
+ if e != nil {
+ t.Error("want nil, got", e)
+ }
+ return
+ }
+ if e == nil {
+ t.Error("got nil result")
+ return
+ }
+ if e.Size != int64(w.size) {
+ t.Error("got size", e.Size, "want", w.size)
+ }
+ if e.Objects != uint64(w.objs) {
+ t.Error("got objects", e.Objects, "want", w.objs)
+ }
+ if e.Versions != uint64(w.objs) {
+ t.Error("got versions", e.Versions, "want", w.objs)
+ }
+ if e.ObjSizes != w.oSizes {
+ t.Error("got histogram", e.ObjSizes, "want", w.oSizes)
+ }
+ })
+ }
+}
+
+func createUsageTestFiles(t *testing.T, base, bucket string, files []usageTestFile) {
+ for _, f := range files {
+ err := os.MkdirAll(filepath.Dir(filepath.Join(base, bucket, f.name)), os.ModePerm)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = os.WriteFile(filepath.Join(base, bucket, f.name), make([]byte, f.size), os.ModePerm)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+}
+
+// generateUsageTestFiles create nFolders * nFiles files of size bytes each.
+func generateUsageTestFiles(t *testing.T, base, bucket string, nFolders, nFiles, size int) {
+ pl := make([]byte, size)
+ for i := range nFolders {
+ name := filepath.Join(base, bucket, fmt.Sprint(i), "0.txt")
+ err := os.MkdirAll(filepath.Dir(name), os.ModePerm)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for j := range nFiles {
+ name := filepath.Join(base, bucket, fmt.Sprint(i), fmt.Sprint(j)+".txt")
+ err = os.WriteFile(name, pl, os.ModePerm)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ }
+}
+
+func TestDataUsageCacheSerialize(t *testing.T) {
+ base := t.TempDir()
+ const bucket = "abucket"
+ files := []usageTestFile{
+ {name: "rootfile", size: 10000},
+ {name: "rootfile2", size: 10000},
+ {name: "dir1/d1file", size: 2000},
+ {name: "dir2/d2file", size: 300},
+ {name: "dir2/d2file2", size: 300},
+ {name: "dir2/d2file3/", size: 300},
+ {name: "dir2/d2file4/", size: 300},
+ {name: "dir2/d2file5", size: 300},
+ {name: "dir1/dira/dafile", size: 100000},
+ {name: "dir1/dira/dbfile", size: 200000},
+ {name: "dir1/dira/dirasub/dcfile", size: 1000000},
+ {name: "dir1/dira/dirasub/sublevel3/dccccfile", size: 10},
+ {name: "dir1/dira/dirasub/sublevel3/dccccfile20", size: 20},
+ {name: "dir1/dira/dirasub/sublevel3/dccccfile30", size: 30},
+ {name: "dir1/dira/dirasub/sublevel3/dccccfile40", size: 40},
+ }
+ createUsageTestFiles(t, base, bucket, files)
+
+ getSize := func(item scannerItem) (sizeS sizeSummary, err error) {
+ if item.Typ&os.ModeDir == 0 {
+ var s os.FileInfo
+ s, err = os.Stat(item.Path)
+ if err != nil {
+ return sizeS, err
+ }
+ sizeS.versions++
+ sizeS.totalSize = s.Size()
+ return sizeS, err
+ }
+ return sizeS, err
+ }
+ xls := xlStorage{drivePath: base, diskInfoCache: cachevalue.New[DiskInfo]()}
+ xls.diskInfoCache.InitOnce(time.Second, cachevalue.Opts{}, func(ctx context.Context) (DiskInfo, error) {
+ return DiskInfo{Total: 1 << 40, Free: 1 << 40}, nil
+ })
+ weSleep := func() bool { return false }
+ want, err := scanDataFolder(t.Context(), nil, &xls, dataUsageCache{Info: dataUsageCacheInfo{Name: bucket}}, getSize, 0, weSleep)
+ if err != nil {
+ t.Fatal(err)
+ }
+ e := want.find("abucket/dir2")
+ want.replace("abucket/dir2", "", *e)
+ var buf bytes.Buffer
+ err = want.serializeTo(&buf)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Log("serialized size:", buf.Len(), "bytes")
+ var got dataUsageCache
+ err = got.deserialize(&buf)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got.Info.LastUpdate.IsZero() {
+ t.Error("lastupdate not set")
+ }
+
+ if !want.Info.LastUpdate.Equal(got.Info.LastUpdate) {
+ t.Fatalf("deserialize LastUpdate mismatch\nwant: %+v\ngot: %+v", want, got)
+ }
+ if len(want.Cache) != len(got.Cache) {
+ t.Errorf("deserialize mismatch length\nwant: %+v\ngot: %+v", len(want.Cache), len(got.Cache))
+ }
+ for wkey, wval := range want.Cache {
+ gotv := got.Cache[wkey]
+ if !equalAsJSON(gotv, wval) {
+ t.Errorf("deserialize mismatch, key %v\nwant: %#v\ngot: %#v", wkey, wval, gotv)
+ }
+ }
+}
+
+// equalAsJSON returns whether the values are equal when encoded as JSON.
+func equalAsJSON(a, b any) bool {
+ aj, err := json.Marshal(a)
+ if err != nil {
+ panic(err)
+ }
+ bj, err := json.Marshal(b)
+ if err != nil {
+ panic(err)
+ }
+ return bytes.Equal(aj, bj)
+}
diff --git a/cmd/decommetric_string.go b/cmd/decommetric_string.go
new file mode 100644
index 00000000..fb485ab0
--- /dev/null
+++ b/cmd/decommetric_string.go
@@ -0,0 +1,25 @@
+// Code generated by "stringer -type=decomMetric -trimprefix=decomMetric erasure-server-pool-decom.go"; DO NOT EDIT.
+
+package cmd
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[decomMetricDecommissionBucket-0]
+ _ = x[decomMetricDecommissionObject-1]
+ _ = x[decomMetricDecommissionRemoveObject-2]
+}
+
+const _decomMetric_name = "DecommissionBucketDecommissionObjectDecommissionRemoveObject"
+
+var _decomMetric_index = [...]uint8{0, 18, 36, 60}
+
+func (i decomMetric) String() string {
+ if i >= decomMetric(len(_decomMetric_index)-1) {
+ return "decomMetric(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _decomMetric_name[_decomMetric_index[i]:_decomMetric_index[i+1]]
+}
diff --git a/cmd/dummy-data-generator_test.go b/cmd/dummy-data-generator_test.go
new file mode 100644
index 00000000..07400b30
--- /dev/null
+++ b/cmd/dummy-data-generator_test.go
@@ -0,0 +1,181 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "testing"
+)
+
+var alphabets = []byte("abcdefghijklmnopqrstuvwxyz0123456789")
+
+// DummyDataGen returns a reader that repeats the bytes in `alphabets`
+// upto the desired length.
+type DummyDataGen struct {
+ b []byte
+ idx, length int64
+}
+
+// NewDummyDataGen returns a ReadSeeker over the first `totalLength`
+// bytes from the infinite stream consisting of repeated
+// concatenations of `alphabets`.
+//
+// The skipOffset (generally = 0) can be used to skip a given number
+// of bytes from the beginning of the infinite stream. This is useful
+// to compare such streams of bytes that may be split up, because:
+//
+// Given the function:
+//
+// f := func(r io.Reader) string {
+// b, _ := io.ReadAll(r)
+// return string(b)
+// }
+//
+// for example, the following is true:
+//
+// f(NewDummyDataGen(100, 0)) == f(NewDummyDataGen(50, 0)) + f(NewDummyDataGen(50, 50))
+func NewDummyDataGen(totalLength, skipOffset int64) io.ReadSeeker {
+ if totalLength < 0 {
+ panic("Negative length passed to DummyDataGen!")
+ }
+ if skipOffset < 0 {
+ panic("Negative rotations are not allowed")
+ }
+
+ skipOffset %= int64(len(alphabets))
+ const multiply = 100
+ as := bytes.Repeat(alphabets, multiply)
+ b := as[skipOffset : skipOffset+int64(len(alphabets)*(multiply-1))]
+ return &DummyDataGen{
+ length: totalLength,
+ b: b,
+ }
+}
+
+func (d *DummyDataGen) Read(b []byte) (n int, err error) {
+ k := len(b)
+ numLetters := int64(len(d.b))
+ for k > 0 && d.idx < d.length {
+ w := copy(b[len(b)-k:], d.b[d.idx%numLetters:])
+ k -= w
+ d.idx += int64(w)
+ n += w
+ }
+ if d.idx >= d.length {
+ extraBytes := d.idx - d.length
+ n -= int(extraBytes)
+ if n < 0 {
+ n = 0
+ }
+ err = io.EOF
+ }
+ return n, err
+}
+
+func (d *DummyDataGen) Seek(offset int64, whence int) (int64, error) {
+ switch whence {
+ case io.SeekStart:
+ if offset < 0 {
+ return 0, errors.New("Invalid offset")
+ }
+ d.idx = offset
+ case io.SeekCurrent:
+ if d.idx+offset < 0 {
+ return 0, errors.New("Invalid offset")
+ }
+ d.idx += offset
+ case io.SeekEnd:
+ if d.length+offset < 0 {
+ return 0, errors.New("Invalid offset")
+ }
+ d.idx = d.length + offset
+ }
+ return d.idx, nil
+}
+
+func TestDummyDataGenerator(t *testing.T) {
+ readAll := func(r io.Reader) string {
+ b, _ := io.ReadAll(r)
+ return string(b)
+ }
+ checkEq := func(a, b string) {
+ if a != b {
+ t.Fatalf("Unexpected equality failure")
+ }
+ }
+
+ checkEq(readAll(NewDummyDataGen(0, 0)), "")
+
+ checkEq(readAll(NewDummyDataGen(10, 0)), readAll(NewDummyDataGen(10, int64(len(alphabets)))))
+
+ checkEq(readAll(NewDummyDataGen(100, 0)), readAll(NewDummyDataGen(50, 0))+readAll(NewDummyDataGen(50, 50)))
+
+ r := NewDummyDataGen(100, 0)
+ r.Seek(int64(len(alphabets)), 0)
+ checkEq(readAll(r), readAll(NewDummyDataGen(100-int64(len(alphabets)), 0)))
+}
+
+// Compares all the bytes returned by the given readers. Any Read
+// errors cause a `false` result. A string describing the error is
+// also returned.
+func cmpReaders(r1, r2 io.Reader) (bool, string) {
+ bufLen := 32 * 1024
+ b1, b2 := make([]byte, bufLen), make([]byte, bufLen)
+ for i := 0; true; i++ {
+ n1, e1 := io.ReadFull(r1, b1)
+ n2, e2 := io.ReadFull(r2, b2)
+ if n1 != n2 {
+ return false, fmt.Sprintf("Read %d != %d bytes from the readers", n1, n2)
+ }
+ if !bytes.Equal(b1[:n1], b2[:n2]) {
+ return false, fmt.Sprintf("After reading %d equal buffers (32Kib each), we got the following two strings:\n%v\n%v\n",
+ i, b1, b2)
+ }
+ // Check if stream has ended
+ if (e1 == io.ErrUnexpectedEOF && e2 == io.ErrUnexpectedEOF) || (e1 == io.EOF && e2 == io.EOF) {
+ break
+ }
+ if e1 != nil || e2 != nil {
+ return false, fmt.Sprintf("Got unexpected error values: %v == %v", e1, e2)
+ }
+ }
+ return true, ""
+}
+
+func TestCmpReaders(t *testing.T) {
+ {
+ r1 := bytes.NewReader([]byte("abc"))
+ r2 := bytes.NewReader([]byte("abc"))
+ ok, msg := cmpReaders(r1, r2)
+ if !ok || msg != "" {
+ t.Fatalf("unexpected")
+ }
+ }
+
+ {
+ r1 := bytes.NewReader([]byte("abc"))
+ r2 := bytes.NewReader([]byte("abcd"))
+ ok, _ := cmpReaders(r1, r2)
+ if ok {
+ t.Fatalf("unexpected")
+ }
+ }
+}
diff --git a/cmd/dummy-handlers.go b/cmd/dummy-handlers.go
new file mode 100644
index 00000000..685b7925
--- /dev/null
+++ b/cmd/dummy-handlers.go
@@ -0,0 +1,257 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "net/http"
+
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/mux"
+ "github.com/minio/pkg/v3/policy"
+)
+
+// Data types used for returning dummy tagging XML.
+// These variables shouldn't be used elsewhere.
+// They are only defined to be used in this file alone.
+
+// GetBucketWebsite - GET bucket website, a dummy api
+func (api objectAPIHandlers) GetBucketWebsiteHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetBucketWebsite")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ // Allow GetBucketWebsite if policy action is set, since this is a dummy call
+ // we are simply re-purposing the bucketPolicyAction.
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketPolicyAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Validate if bucket exists, before proceeding further...
+ _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNoSuchWebsiteConfiguration), r.URL)
+}
+
+// GetBucketAccelerate - GET bucket accelerate, a dummy api
+func (api objectAPIHandlers) GetBucketAccelerateHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetBucketAccelerate")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ // Allow GetBucketAccelerate if policy action is set, since this is a dummy call
+ // we are simply re-purposing the bucketPolicyAction.
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketPolicyAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Validate if bucket exists, before proceeding further...
+ _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ const accelerateDefaultConfig = ``
+ writeSuccessResponseXML(w, []byte(accelerateDefaultConfig))
+}
+
+// GetBucketRequestPaymentHandler - GET bucket requestPayment, a dummy api
+func (api objectAPIHandlers) GetBucketRequestPaymentHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetBucketRequestPayment")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ // Allow GetBucketRequestPaymentHandler if policy action is set, since this is a dummy call
+ // we are simply re-purposing the bucketPolicyAction.
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketPolicyAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Validate if bucket exists, before proceeding further...
+ _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ const requestPaymentDefaultConfig = `BucketOwner`
+
+ writeSuccessResponseXML(w, []byte(requestPaymentDefaultConfig))
+}
+
+// GetBucketLoggingHandler - GET bucket logging, a dummy api
+func (api objectAPIHandlers) GetBucketLoggingHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetBucketLogging")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ // Allow GetBucketLoggingHandler if policy action is set, since this is a dummy call
+ // we are simply re-purposing the bucketPolicyAction.
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketPolicyAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Validate if bucket exists, before proceeding further...
+ _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ const loggingDefaultConfig = ``
+ writeSuccessResponseXML(w, []byte(loggingDefaultConfig))
+}
+
+// DeleteBucketWebsiteHandler - DELETE bucket website, a dummy api
+func (api objectAPIHandlers) DeleteBucketWebsiteHandler(w http.ResponseWriter, r *http.Request) {
+ writeSuccessResponseHeadersOnly(w)
+}
+
+// GetBucketCorsHandler - GET bucket cors, a dummy api
+func (api objectAPIHandlers) GetBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetBucketCors")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketCorsAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Validate if bucket exists, before proceeding further...
+ _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNoSuchCORSConfiguration), r.URL)
+}
+
+// PutBucketCorsHandler - PUT bucket cors, a dummy api
+func (api objectAPIHandlers) PutBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "PutBucketCors")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketCorsAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Validate if bucket exists, before proceeding further...
+ _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+}
+
+// DeleteBucketCorsHandler - DELETE bucket cors, a dummy api
+func (api objectAPIHandlers) DeleteBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "DeleteBucketCors")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.DeleteBucketCorsAction, bucket, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Validate if bucket exists, before proceeding further...
+ _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+}
diff --git a/cmd/dynamic-timeouts.go b/cmd/dynamic-timeouts.go
new file mode 100644
index 00000000..9c4f297b
--- /dev/null
+++ b/cmd/dynamic-timeouts.go
@@ -0,0 +1,155 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "math"
+ "sync"
+ "sync/atomic"
+ "time"
+)
+
+const (
+ dynamicTimeoutIncreaseThresholdPct = 0.33 // Upper threshold for failures in order to increase timeout
+ dynamicTimeoutDecreaseThresholdPct = 0.10 // Lower threshold for failures in order to decrease timeout
+ dynamicTimeoutLogSize = 16
+ maxDuration = math.MaxInt64
+ maxDynamicTimeout = 24 * time.Hour // Never set timeout bigger than this.
+)
+
+// timeouts that are dynamically adapted based on actual usage results
+type dynamicTimeout struct {
+ timeout int64
+ minimum int64
+ entries int64
+ log [dynamicTimeoutLogSize]time.Duration
+ mutex sync.Mutex
+ retryInterval time.Duration
+}
+
+type dynamicTimeoutOpts struct {
+ timeout time.Duration
+ minimum time.Duration
+ retryInterval time.Duration
+}
+
+func newDynamicTimeoutWithOpts(opts dynamicTimeoutOpts) *dynamicTimeout {
+ dt := newDynamicTimeout(opts.timeout, opts.minimum)
+ dt.retryInterval = opts.retryInterval
+ return dt
+}
+
+// newDynamicTimeout returns a new dynamic timeout initialized with timeout value
+func newDynamicTimeout(timeout, minimum time.Duration) *dynamicTimeout {
+ if timeout <= 0 || minimum <= 0 {
+ panic("newDynamicTimeout: negative or zero timeout")
+ }
+ if minimum > timeout {
+ minimum = timeout
+ }
+ return &dynamicTimeout{timeout: int64(timeout), minimum: int64(minimum)}
+}
+
+// Timeout returns the current timeout value
+func (dt *dynamicTimeout) Timeout() time.Duration {
+ return time.Duration(atomic.LoadInt64(&dt.timeout))
+}
+
+func (dt *dynamicTimeout) RetryInterval() time.Duration {
+ return dt.retryInterval
+}
+
+// LogSuccess logs the duration of a successful action that
+// did not hit the timeout
+func (dt *dynamicTimeout) LogSuccess(duration time.Duration) {
+ dt.logEntry(duration)
+}
+
+// LogFailure logs an action that hit the timeout
+func (dt *dynamicTimeout) LogFailure() {
+ dt.logEntry(maxDuration)
+}
+
+// logEntry stores a log entry
+func (dt *dynamicTimeout) logEntry(duration time.Duration) {
+ if duration < 0 {
+ return
+ }
+ entries := int(atomic.AddInt64(&dt.entries, 1))
+ index := entries - 1
+ if index < dynamicTimeoutLogSize {
+ dt.mutex.Lock()
+ dt.log[index] = duration
+
+ // We leak entries while we copy
+ if entries == dynamicTimeoutLogSize {
+ // Make copy on stack in order to call adjust()
+ logCopy := [dynamicTimeoutLogSize]time.Duration{}
+ copy(logCopy[:], dt.log[:])
+
+ // reset log entries
+ atomic.StoreInt64(&dt.entries, 0)
+ dt.mutex.Unlock()
+
+ dt.adjust(logCopy)
+ return
+ }
+ dt.mutex.Unlock()
+ }
+}
+
+// adjust changes the value of the dynamic timeout based on the
+// previous results
+func (dt *dynamicTimeout) adjust(entries [dynamicTimeoutLogSize]time.Duration) {
+ failures, maxDur := 0, time.Duration(0)
+ for _, dur := range entries[:] {
+ if dur == maxDuration {
+ failures++
+ } else if dur > maxDur {
+ maxDur = dur
+ }
+ }
+
+ failPct := float64(failures) / float64(len(entries))
+
+ if failPct > dynamicTimeoutIncreaseThresholdPct {
+ // We are hitting the timeout too often, so increase the timeout by 25%
+ timeout := min(
+ // Set upper cap.
+ atomic.LoadInt64(&dt.timeout)*125/100, int64(maxDynamicTimeout))
+ // Safety, shouldn't happen
+ if timeout < dt.minimum {
+ timeout = dt.minimum
+ }
+ atomic.StoreInt64(&dt.timeout, timeout)
+ } else if failPct < dynamicTimeoutDecreaseThresholdPct {
+ // We are hitting the timeout relatively few times,
+ // so decrease the timeout towards 25 % of maximum time spent.
+ maxDur = maxDur * 125 / 100
+
+ timeout := atomic.LoadInt64(&dt.timeout)
+ if maxDur < time.Duration(timeout) {
+ // Move 50% toward the max.
+ timeout = (int64(maxDur) + timeout) / 2
+ }
+ if timeout < dt.minimum {
+ timeout = dt.minimum
+ }
+ atomic.StoreInt64(&dt.timeout, timeout)
+ }
+}
diff --git a/cmd/dynamic-timeouts_test.go b/cmd/dynamic-timeouts_test.go
new file mode 100644
index 00000000..b353b983
--- /dev/null
+++ b/cmd/dynamic-timeouts_test.go
@@ -0,0 +1,214 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "math/rand"
+ "runtime"
+ "sync"
+ "testing"
+ "time"
+)
+
+func TestDynamicTimeoutSingleIncrease(t *testing.T) {
+ timeout := newDynamicTimeout(time.Minute, time.Second)
+
+ initial := timeout.Timeout()
+
+ for range dynamicTimeoutLogSize {
+ timeout.LogFailure()
+ }
+
+ adjusted := timeout.Timeout()
+
+ if initial >= adjusted {
+ t.Errorf("Failure to increase timeout, expected %v to be more than %v", adjusted, initial)
+ }
+}
+
+func TestDynamicTimeoutDualIncrease(t *testing.T) {
+ timeout := newDynamicTimeout(time.Minute, time.Second)
+
+ initial := timeout.Timeout()
+
+ for range dynamicTimeoutLogSize {
+ timeout.LogFailure()
+ }
+
+ adjusted := timeout.Timeout()
+
+ for range dynamicTimeoutLogSize {
+ timeout.LogFailure()
+ }
+
+ adjustedAgain := timeout.Timeout()
+
+ if initial >= adjusted || adjusted >= adjustedAgain {
+ t.Errorf("Failure to increase timeout multiple times")
+ }
+}
+
+func TestDynamicTimeoutSingleDecrease(t *testing.T) {
+ timeout := newDynamicTimeout(time.Minute, time.Second)
+
+ initial := timeout.Timeout()
+
+ for range dynamicTimeoutLogSize {
+ timeout.LogSuccess(20 * time.Second)
+ }
+
+ adjusted := timeout.Timeout()
+
+ if initial <= adjusted {
+ t.Errorf("Failure to decrease timeout, expected %v to be less than %v", adjusted, initial)
+ }
+}
+
+func TestDynamicTimeoutDualDecrease(t *testing.T) {
+ timeout := newDynamicTimeout(time.Minute, time.Second)
+
+ initial := timeout.Timeout()
+
+ for range dynamicTimeoutLogSize {
+ timeout.LogSuccess(20 * time.Second)
+ }
+
+ adjusted := timeout.Timeout()
+
+ for range dynamicTimeoutLogSize {
+ timeout.LogSuccess(20 * time.Second)
+ }
+
+ adjustedAgain := timeout.Timeout()
+
+ if initial <= adjusted || adjusted <= adjustedAgain {
+ t.Errorf("Failure to decrease timeout multiple times, initial: %v, adjusted: %v, again: %v", initial, adjusted, adjustedAgain)
+ }
+}
+
+func TestDynamicTimeoutManyDecreases(t *testing.T) {
+ timeout := newDynamicTimeout(time.Minute, time.Second)
+
+ initial := timeout.Timeout()
+
+ const successTimeout = 20 * time.Second
+ for range 100 {
+ for range dynamicTimeoutLogSize {
+ timeout.LogSuccess(successTimeout)
+ }
+ }
+
+ adjusted := timeout.Timeout()
+ // Check whether eventual timeout is between initial value and success timeout
+ if initial <= adjusted || adjusted <= successTimeout {
+ t.Errorf("Failure to decrease timeout appropriately")
+ }
+}
+
+func TestDynamicTimeoutConcurrent(t *testing.T) {
+ // Race test.
+ timeout := newDynamicTimeout(time.Second, time.Millisecond)
+ var wg sync.WaitGroup
+ for i := 0; i < runtime.GOMAXPROCS(0); i++ {
+ wg.Add(1)
+ rng := rand.New(rand.NewSource(int64(i)))
+ go func() {
+ defer wg.Done()
+ for range 100 {
+ for range 100 {
+ timeout.LogSuccess(time.Duration(float64(time.Second) * rng.Float64()))
+ }
+ to := timeout.Timeout()
+ if to < time.Millisecond || to > time.Second {
+ panic(to)
+ }
+ }
+ }()
+ }
+ wg.Wait()
+}
+
+func TestDynamicTimeoutHitMinimum(t *testing.T) {
+ const minimum = 30 * time.Second
+ timeout := newDynamicTimeout(time.Minute, minimum)
+
+ initial := timeout.Timeout()
+
+ const successTimeout = 20 * time.Second
+ for range 100 {
+ for range dynamicTimeoutLogSize {
+ timeout.LogSuccess(successTimeout)
+ }
+ }
+
+ adjusted := timeout.Timeout()
+ // Check whether eventual timeout has hit the minimum value
+ if initial <= adjusted || adjusted != minimum {
+ t.Errorf("Failure to decrease timeout appropriately")
+ }
+}
+
+func testDynamicTimeoutAdjust(t *testing.T, timeout *dynamicTimeout, f func() float64) {
+ const successTimeout = 20 * time.Second
+
+ for range dynamicTimeoutLogSize {
+ rnd := f()
+ duration := max(time.Duration(float64(successTimeout)*rnd), 100*time.Millisecond)
+ if duration >= time.Minute {
+ timeout.LogFailure()
+ } else {
+ timeout.LogSuccess(duration)
+ }
+ }
+}
+
+func TestDynamicTimeoutAdjustExponential(t *testing.T) {
+ timeout := newDynamicTimeout(time.Minute, time.Second)
+
+ rand.Seed(0)
+
+ initial := timeout.Timeout()
+
+ for range 10 {
+ testDynamicTimeoutAdjust(t, timeout, rand.ExpFloat64)
+ }
+
+ adjusted := timeout.Timeout()
+ if initial <= adjusted {
+ t.Errorf("Failure to decrease timeout, expected %v to be less than %v", adjusted, initial)
+ }
+}
+
+func TestDynamicTimeoutAdjustNormalized(t *testing.T) {
+ timeout := newDynamicTimeout(time.Minute, time.Second)
+
+ rand.Seed(0)
+
+ initial := timeout.Timeout()
+
+ for range 10 {
+ testDynamicTimeoutAdjust(t, timeout, func() float64 {
+ return 1.0 + rand.NormFloat64()
+ })
+ }
+
+ adjusted := timeout.Timeout()
+ if initial <= adjusted {
+ t.Errorf("Failure to decrease timeout, expected %v to be less than %v", adjusted, initial)
+ }
+}
diff --git a/cmd/encryption-v1.go b/cmd/encryption-v1.go
new file mode 100644
index 00000000..c3da051a
--- /dev/null
+++ b/cmd/encryption-v1.go
@@ -0,0 +1,1200 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bufio"
+ "bytes"
+ "context"
+ "crypto/hmac"
+ "crypto/rand"
+ "crypto/subtle"
+ "encoding/binary"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "io"
+ "maps"
+ "net/http"
+ "path"
+ "strconv"
+ "strings"
+
+ "github.com/minio/kms-go/kes"
+ "github.com/minio/minio/internal/crypto"
+ "github.com/minio/minio/internal/etag"
+ "github.com/minio/minio/internal/hash"
+ "github.com/minio/minio/internal/hash/sha256"
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/minio/internal/kms"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/sio"
+)
+
+var (
+ // AWS errors for invalid SSE-C requests.
+ errEncryptedObject = errors.New("The object was stored using a form of SSE")
+ errInvalidSSEParameters = errors.New("The SSE-C key for key-rotation is not correct") // special access denied
+ errKMSNotConfigured = errors.New("KMS not configured for a server side encrypted objects")
+ errKMSKeyNotFound = errors.New("Unknown KMS key ID")
+ errKMSDefaultKeyAlreadyConfigured = errors.New("A default encryption already exists on KMS")
+ // Additional MinIO errors for SSE-C requests.
+ errObjectTampered = errors.New("The requested object was modified and may be compromised")
+ // error returned when invalid encryption parameters are specified
+ errInvalidEncryptionParameters = errors.New("The encryption parameters are not applicable to this object")
+ errInvalidEncryptionParametersSSEC = errors.New("SSE-C encryption parameters are not supported on this bucket")
+)
+
+const (
+ // SSECustomerKeySize is the size of valid client provided encryption keys in bytes.
+ // Currently AWS supports only AES256. So the SSE-C key size is fixed to 32 bytes.
+ SSECustomerKeySize = 32
+
+ // SSEIVSize is the size of the IV data
+ SSEIVSize = 32 // 32 bytes
+
+ // SSEDAREPackageBlockSize - SSE dare package block size.
+ SSEDAREPackageBlockSize = 64 * 1024 // 64KiB bytes
+
+ // SSEDAREPackageMetaSize - SSE dare package meta padding bytes.
+ SSEDAREPackageMetaSize = 32 // 32 bytes
+
+)
+
+// KMSKeyID returns in AWS compatible KMS KeyID() format.
+func (o *ObjectInfo) KMSKeyID() string { return kmsKeyIDFromMetadata(o.UserDefined) }
+
+// KMSKeyID returns in AWS compatible KMS KeyID() format.
+func (o *MultipartInfo) KMSKeyID() string { return kmsKeyIDFromMetadata(o.UserDefined) }
+
+// kmsKeyIDFromMetadata returns any AWS S3 KMS key ID in the
+// metadata, if any. It returns an empty ID if no key ID is
+// present.
+func kmsKeyIDFromMetadata(metadata map[string]string) string {
+ const ARNPrefix = crypto.ARNPrefix
+ if len(metadata) == 0 {
+ return ""
+ }
+ kmsID, ok := metadata[crypto.MetaKeyID]
+ if !ok {
+ return ""
+ }
+ if strings.HasPrefix(kmsID, ARNPrefix) {
+ return kmsID
+ }
+ return ARNPrefix + kmsID
+}
+
+// DecryptETags decryptes the ETag of all ObjectInfos using the KMS.
+//
+// It adjusts the size of all encrypted objects since encrypted
+// objects are slightly larger due to encryption overhead.
+// Further, it decrypts all single-part SSE-S3 encrypted objects
+// and formats ETags of SSE-C / SSE-KMS encrypted objects to
+// be AWS S3 compliant.
+//
+// DecryptETags uses a KMS bulk decryption API, if available, which
+// is more efficient than decrypting ETags sequentially.
+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.
+ var (
+ metadata = make([]map[string]string, 0, BatchSize)
+ buckets = make([]string, 0, BatchSize)
+ names = make([]string, 0, BatchSize)
+ )
+ for len(objects) > 0 {
+ N := min(len(objects), BatchSize)
+ batch := objects[:N]
+
+ // We have to decrypt only ETags of SSE-S3 single-part
+ // objects.
+ // Therefore, we remember which objects (there index)
+ // in the current batch are single-part SSE-S3 objects.
+ metadata = metadata[:0:N]
+ buckets = buckets[:0:N]
+ names = names[:0:N]
+ SSES3SinglePartObjects := make(map[int]bool)
+ for i, object := range batch {
+ if kind, ok := crypto.IsEncrypted(object.UserDefined); ok && kind == crypto.S3 && !crypto.IsMultiPart(object.UserDefined) {
+ ETag, err := etag.Parse(object.ETag)
+ if err != nil {
+ continue
+ }
+ if ETag.IsEncrypted() {
+ SSES3SinglePartObjects[i] = true
+ metadata = append(metadata, object.UserDefined)
+ buckets = append(buckets, object.Bucket)
+ names = append(names, object.Name)
+ }
+ }
+ }
+
+ // If there are no SSE-S3 single-part objects
+ // we can skip the decryption process. However,
+ // we still have to adjust the size and ETag
+ // of SSE-C and SSE-KMS objects.
+ if len(SSES3SinglePartObjects) == 0 {
+ for i := range batch {
+ size, err := batch[i].GetActualSize()
+ if err != nil {
+ return err
+ }
+ batch[i].Size = size
+
+ if _, ok := crypto.IsEncrypted(batch[i].UserDefined); ok {
+ ETag, err := etag.Parse(batch[i].ETag)
+ if err != nil {
+ return err
+ }
+ batch[i].ETag = ETag.Format().String()
+ }
+ }
+ objects = objects[N:]
+ continue
+ }
+
+ // There is at least one SSE-S3 single-part object.
+ // For all SSE-S3 single-part objects we have to
+ // fetch their decryption keys. We do this using
+ // a Bulk-Decryption API call, if available.
+ keys, err := crypto.S3.UnsealObjectKeys(ctx, k, metadata, buckets, names)
+ if err != nil {
+ return err
+ }
+
+ // Now, we have to decrypt the ETags of SSE-S3 single-part
+ // objects and adjust the size and ETags of all encrypted
+ // objects.
+ for i := range batch {
+ size, err := batch[i].GetActualSize()
+ if err != nil {
+ return err
+ }
+ batch[i].Size = size
+
+ if _, ok := crypto.IsEncrypted(batch[i].UserDefined); ok {
+ ETag, err := etag.Parse(batch[i].ETag)
+ if err != nil {
+ return err
+ }
+ if SSES3SinglePartObjects[i] {
+ ETag, err = etag.Decrypt(keys[0][:], ETag)
+ if err != nil {
+ return err
+ }
+ keys = keys[1:]
+ }
+ batch[i].ETag = ETag.Format().String()
+ }
+ }
+ objects = objects[N:]
+ }
+ return nil
+}
+
+// isMultipart returns true if the current object is
+// uploaded by the user using multipart mechanism:
+// initiate new multipart, upload part, complete upload
+func (o *ObjectInfo) isMultipart() bool {
+ _, encrypted := crypto.IsEncrypted(o.UserDefined)
+ if encrypted {
+ if !crypto.IsMultiPart(o.UserDefined) {
+ return false
+ }
+ for _, part := range o.Parts {
+ _, err := sio.DecryptedSize(uint64(part.Size))
+ if err != nil {
+ return false
+ }
+ }
+ }
+
+ // Further check if this object is uploaded using multipart mechanism
+ // by the user and it is not about Erasure internally splitting the
+ // object into parts in PutObject()
+ return len(o.ETag) != 32
+}
+
+// ParseSSECopyCustomerRequest parses the SSE-C header fields of the provided request.
+// It returns the client provided key on success.
+func ParseSSECopyCustomerRequest(h http.Header, metadata map[string]string) (key []byte, err error) {
+ if crypto.S3.IsEncrypted(metadata) && crypto.SSECopy.IsRequested(h) {
+ return nil, crypto.ErrIncompatibleEncryptionMethod
+ }
+ k, err := crypto.SSECopy.ParseHTTP(h)
+ return k[:], err
+}
+
+// ParseSSECustomerRequest parses the SSE-C header fields of the provided request.
+// It returns the client provided key on success.
+func ParseSSECustomerRequest(r *http.Request) (key []byte, err error) {
+ return ParseSSECustomerHeader(r.Header)
+}
+
+// ParseSSECustomerHeader parses the SSE-C header fields and returns
+// the client provided key on success.
+func ParseSSECustomerHeader(header http.Header) (key []byte, err error) {
+ if crypto.S3.IsRequested(header) && crypto.SSEC.IsRequested(header) {
+ return key, crypto.ErrIncompatibleEncryptionMethod
+ }
+
+ k, err := crypto.SSEC.ParseHTTP(header)
+ return k[:], err
+}
+
+// This function rotates old to new key.
+func rotateKey(ctx context.Context, oldKey []byte, newKeyID string, newKey []byte, bucket, object string, metadata map[string]string, cryptoCtx kms.Context) error {
+ kind, _ := crypto.IsEncrypted(metadata)
+ switch kind {
+ case crypto.S3:
+ if GlobalKMS == nil {
+ return errKMSNotConfigured
+ }
+ keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(metadata)
+ if err != nil {
+ return err
+ }
+ oldKey, err := GlobalKMS.Decrypt(ctx, &kms.DecryptRequest{
+ Name: keyID,
+ Ciphertext: kmsKey,
+ AssociatedData: kms.Context{bucket: path.Join(bucket, object)},
+ })
+ if err != nil {
+ return err
+ }
+ var objectKey crypto.ObjectKey
+ if err = objectKey.Unseal(oldKey, sealedKey, crypto.S3.String(), bucket, object); err != nil {
+ return err
+ }
+
+ newKey, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{
+ Name: GlobalKMS.DefaultKey,
+ AssociatedData: kms.Context{bucket: path.Join(bucket, object)},
+ })
+ if err != nil {
+ return err
+ }
+ sealedKey = objectKey.Seal(newKey.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, object)
+ crypto.S3.CreateMetadata(metadata, newKey.KeyID, newKey.Ciphertext, sealedKey)
+ return nil
+ case crypto.S3KMS:
+ if GlobalKMS == nil {
+ return errKMSNotConfigured
+ }
+ objectKey, err := crypto.S3KMS.UnsealObjectKey(GlobalKMS, metadata, bucket, object)
+ if err != nil {
+ return err
+ }
+
+ if len(cryptoCtx) == 0 {
+ _, _, _, cryptoCtx, err = crypto.S3KMS.ParseMetadata(metadata)
+ if err != nil {
+ return err
+ }
+ }
+
+ // If the context does not contain the bucket key
+ // we must add it for key generation. However,
+ // the context must be stored exactly like the
+ // client provided it. Therefore, we create a copy
+ // of the client provided context and add the bucket
+ // key, if not present.
+ kmsCtx := kms.Context{}
+ maps.Copy(kmsCtx, cryptoCtx)
+ if _, ok := kmsCtx[bucket]; !ok {
+ kmsCtx[bucket] = path.Join(bucket, object)
+ }
+ newKey, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{
+ Name: newKeyID,
+ AssociatedData: kmsCtx,
+ })
+ if err != nil {
+ return err
+ }
+
+ sealedKey := objectKey.Seal(newKey.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3KMS.String(), bucket, object)
+ crypto.S3KMS.CreateMetadata(metadata, newKey.KeyID, newKey.Ciphertext, sealedKey, cryptoCtx)
+ return nil
+ case crypto.SSEC:
+ sealedKey, err := crypto.SSEC.ParseMetadata(metadata)
+ if err != nil {
+ return err
+ }
+
+ var objectKey crypto.ObjectKey
+ if err = objectKey.Unseal(oldKey, sealedKey, crypto.SSEC.String(), bucket, object); err != nil {
+ if subtle.ConstantTimeCompare(oldKey, newKey) == 1 {
+ return errInvalidSSEParameters // AWS returns special error for equal but invalid keys.
+ }
+ return crypto.ErrInvalidCustomerKey // To provide strict AWS S3 compatibility we return: access denied.
+ }
+
+ if subtle.ConstantTimeCompare(oldKey, newKey) == 1 && sealedKey.Algorithm == crypto.SealAlgorithm {
+ return nil // don't rotate on equal keys if seal algorithm is latest
+ }
+ sealedKey = objectKey.Seal(newKey, sealedKey.IV, crypto.SSEC.String(), bucket, object)
+ crypto.SSEC.CreateMetadata(metadata, sealedKey)
+ return nil
+ default:
+ return errObjectTampered
+ }
+}
+
+func newEncryptMetadata(ctx context.Context, kind crypto.Type, keyID string, key []byte, bucket, object string, metadata map[string]string, cryptoCtx kms.Context) (crypto.ObjectKey, error) {
+ var sealedKey crypto.SealedKey
+ switch kind {
+ case crypto.S3:
+ if GlobalKMS == nil {
+ return crypto.ObjectKey{}, errKMSNotConfigured
+ }
+ key, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{
+ AssociatedData: kms.Context{bucket: path.Join(bucket, object)},
+ })
+ if err != nil {
+ return crypto.ObjectKey{}, err
+ }
+
+ objectKey := crypto.GenerateKey(key.Plaintext, rand.Reader)
+ sealedKey = objectKey.Seal(key.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, object)
+ crypto.S3.CreateMetadata(metadata, key.KeyID, key.Ciphertext, sealedKey)
+ return objectKey, nil
+ case crypto.S3KMS:
+ if GlobalKMS == nil {
+ return crypto.ObjectKey{}, errKMSNotConfigured
+ }
+
+ // If the context does not contain the bucket key
+ // we must add it for key generation. However,
+ // the context must be stored exactly like the
+ // client provided it. Therefore, we create a copy
+ // of the client provided context and add the bucket
+ // key, if not present.
+ kmsCtx := kms.Context{}
+ maps.Copy(kmsCtx, cryptoCtx)
+ if _, ok := kmsCtx[bucket]; !ok {
+ kmsCtx[bucket] = path.Join(bucket, object)
+ }
+ key, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{
+ Name: keyID,
+ AssociatedData: kmsCtx,
+ })
+ if err != nil {
+ if errors.Is(err, kes.ErrKeyNotFound) {
+ return crypto.ObjectKey{}, errKMSKeyNotFound
+ }
+ return crypto.ObjectKey{}, err
+ }
+
+ objectKey := crypto.GenerateKey(key.Plaintext, rand.Reader)
+ sealedKey = objectKey.Seal(key.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3KMS.String(), bucket, object)
+ crypto.S3KMS.CreateMetadata(metadata, key.KeyID, key.Ciphertext, sealedKey, cryptoCtx)
+ return objectKey, nil
+ case crypto.SSEC:
+ objectKey := crypto.GenerateKey(key, rand.Reader)
+ sealedKey = objectKey.Seal(key, crypto.GenerateIV(rand.Reader), crypto.SSEC.String(), bucket, object)
+ crypto.SSEC.CreateMetadata(metadata, sealedKey)
+ return objectKey, nil
+ default:
+ return crypto.ObjectKey{}, fmt.Errorf("encryption type '%v' not supported", kind)
+ }
+}
+
+func newEncryptReader(ctx context.Context, content io.Reader, kind crypto.Type, keyID string, key []byte, bucket, object string, metadata map[string]string, cryptoCtx kms.Context) (io.Reader, crypto.ObjectKey, error) {
+ objectEncryptionKey, err := newEncryptMetadata(ctx, kind, keyID, key, bucket, object, metadata, cryptoCtx)
+ if err != nil {
+ return nil, crypto.ObjectKey{}, err
+ }
+
+ reader, err := sio.EncryptReader(content, sio.Config{Key: objectEncryptionKey[:], MinVersion: sio.Version20})
+ if err != nil {
+ return nil, crypto.ObjectKey{}, crypto.ErrInvalidCustomerKey
+ }
+
+ return reader, objectEncryptionKey, nil
+}
+
+// set new encryption metadata from http request headers for SSE-C and generated key from KMS in the case of
+// SSE-S3
+func setEncryptionMetadata(r *http.Request, bucket, object string, metadata map[string]string) (err error) {
+ var (
+ key []byte
+ keyID string
+ kmsCtx kms.Context
+ )
+ kind, _ := crypto.IsRequested(r.Header)
+ switch kind {
+ case crypto.SSEC:
+ key, err = ParseSSECustomerRequest(r)
+ if err != nil {
+ return err
+ }
+ case crypto.S3KMS:
+ keyID, kmsCtx, err = crypto.S3KMS.ParseHTTP(r.Header)
+ if err != nil {
+ return err
+ }
+ }
+ _, err = newEncryptMetadata(r.Context(), kind, keyID, key, bucket, object, metadata, kmsCtx)
+ return err
+}
+
+// EncryptRequest takes the client provided content and encrypts the data
+// with the client provided key. It also marks the object as client-side-encrypted
+// and sets the correct headers.
+func EncryptRequest(content io.Reader, r *http.Request, bucket, object string, metadata map[string]string) (io.Reader, crypto.ObjectKey, error) {
+ if r.ContentLength > encryptBufferThreshold {
+ // The encryption reads in blocks of 64KB.
+ // We add a buffer on bigger files to reduce the number of syscalls upstream.
+ content = bufio.NewReaderSize(content, encryptBufferSize)
+ }
+
+ var (
+ key []byte
+ keyID string
+ ctx kms.Context
+ err error
+ )
+ kind, _ := crypto.IsRequested(r.Header)
+ if kind == crypto.SSEC {
+ key, err = ParseSSECustomerRequest(r)
+ if err != nil {
+ return nil, crypto.ObjectKey{}, err
+ }
+ }
+ if kind == crypto.S3KMS {
+ keyID, ctx, err = crypto.S3KMS.ParseHTTP(r.Header)
+ if err != nil {
+ return nil, crypto.ObjectKey{}, err
+ }
+ }
+ return newEncryptReader(r.Context(), content, kind, keyID, key, bucket, object, metadata, ctx)
+}
+
+func decryptObjectMeta(key []byte, bucket, object string, metadata map[string]string) ([]byte, error) {
+ switch kind, _ := crypto.IsEncrypted(metadata); kind {
+ case crypto.S3:
+ if GlobalKMS == nil {
+ return nil, errKMSNotConfigured
+ }
+ objectKey, err := crypto.S3.UnsealObjectKey(GlobalKMS, metadata, bucket, object)
+ if err != nil {
+ return nil, err
+ }
+ return objectKey[:], nil
+ case crypto.S3KMS:
+ if GlobalKMS == nil {
+ return nil, errKMSNotConfigured
+ }
+ objectKey, err := crypto.S3KMS.UnsealObjectKey(GlobalKMS, metadata, bucket, object)
+ if err != nil {
+ return nil, err
+ }
+ return objectKey[:], nil
+ case crypto.SSEC:
+ sealedKey, err := crypto.SSEC.ParseMetadata(metadata)
+ if err != nil {
+ return nil, err
+ }
+ var objectKey crypto.ObjectKey
+ if err = objectKey.Unseal(key, sealedKey, crypto.SSEC.String(), bucket, object); err != nil {
+ return nil, err
+ }
+ return objectKey[:], nil
+ default:
+ return nil, errObjectTampered
+ }
+}
+
+// Adding support for reader based interface
+
+// DecryptRequestWithSequenceNumberR - same as
+// DecryptRequestWithSequenceNumber but with a reader
+func DecryptRequestWithSequenceNumberR(client io.Reader, h http.Header, bucket, object string, seqNumber uint32, metadata map[string]string) (io.Reader, error) {
+ if crypto.SSEC.IsEncrypted(metadata) {
+ key, err := ParseSSECustomerHeader(h)
+ if err != nil {
+ return nil, err
+ }
+ return newDecryptReader(client, key, bucket, object, seqNumber, metadata)
+ }
+ return newDecryptReader(client, nil, bucket, object, seqNumber, metadata)
+}
+
+// DecryptCopyRequestR - same as DecryptCopyRequest, but with a
+// Reader
+func DecryptCopyRequestR(client io.Reader, h http.Header, bucket, object string, seqNumber uint32, metadata map[string]string) (io.Reader, error) {
+ var (
+ key []byte
+ err error
+ )
+ if crypto.SSECopy.IsRequested(h) {
+ key, err = ParseSSECopyCustomerRequest(h, metadata)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return newDecryptReader(client, key, bucket, object, seqNumber, metadata)
+}
+
+func newDecryptReader(client io.Reader, key []byte, bucket, object string, seqNumber uint32, metadata map[string]string) (io.Reader, error) {
+ objectEncryptionKey, err := decryptObjectMeta(key, bucket, object, metadata)
+ if err != nil {
+ return nil, err
+ }
+ return newDecryptReaderWithObjectKey(client, objectEncryptionKey, seqNumber)
+}
+
+func newDecryptReaderWithObjectKey(client io.Reader, objectEncryptionKey []byte, seqNumber uint32) (io.Reader, error) {
+ reader, err := sio.DecryptReader(client, sio.Config{
+ Key: objectEncryptionKey,
+ SequenceNumber: seqNumber,
+ })
+ if err != nil {
+ return nil, crypto.ErrInvalidCustomerKey
+ }
+ return reader, nil
+}
+
+// DecryptBlocksRequestR - same as DecryptBlocksRequest but with a
+// reader
+func DecryptBlocksRequestR(inputReader io.Reader, h http.Header, seqNumber uint32, partStart int, oi ObjectInfo, copySource bool) (io.Reader, error) {
+ bucket, object := oi.Bucket, oi.Name
+ // Single part case
+ if !oi.isMultipart() {
+ var reader io.Reader
+ var err error
+ if copySource {
+ reader, err = DecryptCopyRequestR(inputReader, h, bucket, object, seqNumber, oi.UserDefined)
+ } else {
+ reader, err = DecryptRequestWithSequenceNumberR(inputReader, h, bucket, object, seqNumber, oi.UserDefined)
+ }
+ if err != nil {
+ return nil, err
+ }
+ return reader, nil
+ }
+
+ partDecRelOffset := int64(seqNumber) * SSEDAREPackageBlockSize
+ partEncRelOffset := int64(seqNumber) * (SSEDAREPackageBlockSize + SSEDAREPackageMetaSize)
+
+ w := &DecryptBlocksReader{
+ reader: inputReader,
+ startSeqNum: seqNumber,
+ partDecRelOffset: partDecRelOffset,
+ partEncRelOffset: partEncRelOffset,
+ parts: oi.Parts,
+ partIndex: partStart,
+ }
+
+ // In case of SSE-C, we have to decrypt the OEK using the client-provided key.
+ // In case of a SSE-C server-side copy, the client might provide two keys,
+ // one for the source and one for the target. This reader is the source.
+ var ssecClientKey []byte
+ if crypto.SSEC.IsEncrypted(oi.UserDefined) {
+ if copySource && crypto.SSECopy.IsRequested(h) {
+ key, err := crypto.SSECopy.ParseHTTP(h)
+ if err != nil {
+ return nil, err
+ }
+ ssecClientKey = key[:]
+ } else {
+ key, err := crypto.SSEC.ParseHTTP(h)
+ if err != nil {
+ return nil, err
+ }
+ ssecClientKey = key[:]
+ }
+ }
+
+ // Decrypt the OEK once and reuse it for all subsequent parts.
+ objectEncryptionKey, err := decryptObjectMeta(ssecClientKey, bucket, object, oi.UserDefined)
+ if err != nil {
+ return nil, err
+ }
+ w.objectEncryptionKey = objectEncryptionKey
+
+ if err := w.buildDecrypter(w.parts[w.partIndex].Number); err != nil {
+ return nil, err
+ }
+ return w, nil
+}
+
+// DecryptBlocksReader - decrypts multipart parts, while implementing
+// a io.Reader compatible interface.
+type DecryptBlocksReader struct {
+ // Source of the encrypted content that will be decrypted
+ reader io.Reader
+ // Current decrypter for the current encrypted data block
+ decrypter io.Reader
+ // Start sequence number
+ startSeqNum uint32
+ // Current part index
+ partIndex int
+ // Parts information
+ parts []ObjectPartInfo
+
+ objectEncryptionKey []byte
+ partDecRelOffset, partEncRelOffset int64
+}
+
+func (d *DecryptBlocksReader) buildDecrypter(partID int) error {
+ var partIDbin [4]byte
+ binary.LittleEndian.PutUint32(partIDbin[:], uint32(partID)) // marshal part ID
+
+ mac := hmac.New(sha256.New, d.objectEncryptionKey) // derive part encryption key from part ID and object key
+ mac.Write(partIDbin[:])
+ partEncryptionKey := mac.Sum(nil)
+
+ // Limit the reader, so the decryptor doesn't receive bytes
+ // from the next part (different DARE stream)
+ encLenToRead := d.parts[d.partIndex].Size - d.partEncRelOffset
+ decrypter, err := newDecryptReaderWithObjectKey(io.LimitReader(d.reader, encLenToRead), partEncryptionKey, d.startSeqNum)
+ if err != nil {
+ return err
+ }
+
+ d.decrypter = decrypter
+ return nil
+}
+
+func (d *DecryptBlocksReader) Read(p []byte) (int, error) {
+ var err error
+ var n1 int
+ decPartSize, _ := sio.DecryptedSize(uint64(d.parts[d.partIndex].Size))
+ unreadPartLen := int64(decPartSize) - d.partDecRelOffset
+ if int64(len(p)) < unreadPartLen {
+ n1, err = d.decrypter.Read(p)
+ if err != nil {
+ return 0, err
+ }
+ d.partDecRelOffset += int64(n1)
+ } else {
+ n1, err = io.ReadFull(d.decrypter, p[:unreadPartLen])
+ if err != nil {
+ return 0, err
+ }
+
+ // We should now proceed to next part, reset all
+ // values appropriately.
+ d.partEncRelOffset = 0
+ d.partDecRelOffset = 0
+ d.startSeqNum = 0
+
+ d.partIndex++
+ if d.partIndex == len(d.parts) {
+ return n1, io.EOF
+ }
+
+ err = d.buildDecrypter(d.parts[d.partIndex].Number)
+ if err != nil {
+ return 0, err
+ }
+
+ n1, err = d.decrypter.Read(p[n1:])
+ if err != nil {
+ return 0, err
+ }
+
+ d.partDecRelOffset += int64(n1)
+ }
+ return len(p), nil
+}
+
+// DecryptedSize returns the size of the object after decryption in bytes.
+// It returns an error if the object is not encrypted or marked as encrypted
+// but has an invalid size.
+func (o ObjectInfo) DecryptedSize() (int64, error) {
+ if _, ok := crypto.IsEncrypted(o.UserDefined); !ok {
+ return -1, errors.New("Cannot compute decrypted size of an unencrypted object")
+ }
+
+ if !o.isMultipart() {
+ size, err := sio.DecryptedSize(uint64(o.Size))
+ if err != nil {
+ err = errObjectTampered // assign correct error type
+ }
+ return int64(size), err
+ }
+
+ var size int64
+ for _, part := range o.Parts {
+ partSize, err := sio.DecryptedSize(uint64(part.Size))
+ if err != nil {
+ return -1, errObjectTampered
+ }
+ size += int64(partSize)
+ }
+ return size, nil
+}
+
+// DecryptETag decrypts the ETag that is part of given object
+// with the given object encryption key.
+//
+// However, DecryptETag does not try to decrypt the ETag if
+// it consists of a 128 bit hex value (32 hex chars) and exactly
+// one '-' followed by a 32-bit number.
+// This special case addresses randomly-generated ETags generated
+// by the MinIO server when running in non-compat mode. These
+// random ETags are not encrypt.
+//
+// Calling DecryptETag with a non-randomly generated ETag will
+// fail.
+func DecryptETag(key crypto.ObjectKey, object ObjectInfo) (string, error) {
+ if n := strings.Count(object.ETag, "-"); n > 0 {
+ if n != 1 {
+ return "", errObjectTampered
+ }
+ i := strings.IndexByte(object.ETag, '-')
+ if len(object.ETag[:i]) != 32 {
+ return "", errObjectTampered
+ }
+ if _, err := hex.DecodeString(object.ETag[:32]); err != nil {
+ return "", errObjectTampered
+ }
+ if _, err := strconv.ParseInt(object.ETag[i+1:], 10, 32); err != nil {
+ return "", errObjectTampered
+ }
+ return object.ETag, nil
+ }
+
+ etag, err := hex.DecodeString(object.ETag)
+ if err != nil {
+ return "", err
+ }
+ etag, err = key.UnsealETag(etag)
+ if err != nil {
+ return "", err
+ }
+ return hex.EncodeToString(etag), nil
+}
+
+// For encrypted objects, the ETag sent by client if available
+// is stored in encrypted form in the backend. Decrypt the ETag
+// if ETag was previously encrypted.
+func getDecryptedETag(headers http.Header, objInfo ObjectInfo, copySource bool) (decryptedETag string) {
+ var (
+ key [32]byte
+ err error
+ )
+ // If ETag is contentMD5Sum return it as is.
+ if len(objInfo.ETag) == 32 {
+ return objInfo.ETag
+ }
+
+ if crypto.IsMultiPart(objInfo.UserDefined) {
+ return objInfo.ETag
+ }
+
+ if crypto.SSECopy.IsRequested(headers) {
+ key, err = crypto.SSECopy.ParseHTTP(headers)
+ if err != nil {
+ return objInfo.ETag
+ }
+ }
+
+ // As per AWS S3 Spec, ETag for SSE-C encrypted objects need not be MD5Sum of the data.
+ // Since server side copy with same source and dest just replaces the ETag, we save
+ // encrypted content MD5Sum as ETag for both SSE-C and SSE-KMS, we standardize the ETag
+ // encryption across SSE-C and SSE-KMS, and only return last 32 bytes for SSE-C
+ if (crypto.SSEC.IsEncrypted(objInfo.UserDefined) || crypto.S3KMS.IsEncrypted(objInfo.UserDefined)) && !copySource {
+ return objInfo.ETag[len(objInfo.ETag)-32:]
+ }
+
+ objectEncryptionKey, err := decryptObjectMeta(key[:], objInfo.Bucket, objInfo.Name, objInfo.UserDefined)
+ if err != nil {
+ return objInfo.ETag
+ }
+ return tryDecryptETag(objectEncryptionKey, objInfo.ETag, true)
+}
+
+// helper to decrypt Etag given object encryption key and encrypted ETag
+func tryDecryptETag(key []byte, encryptedETag string, sses3 bool) string {
+ // ETag for SSE-C or SSE-KMS encrypted objects need not be content MD5Sum.While encrypted
+ // md5sum is stored internally, return just the last 32 bytes of hex-encoded and
+ // encrypted md5sum string for SSE-C
+ if !sses3 {
+ return encryptedETag[len(encryptedETag)-32:]
+ }
+ var objectKey crypto.ObjectKey
+ copy(objectKey[:], key)
+ encBytes, err := hex.DecodeString(encryptedETag)
+ if err != nil {
+ return encryptedETag
+ }
+ etagBytes, err := objectKey.UnsealETag(encBytes)
+ if err != nil {
+ return encryptedETag
+ }
+ return hex.EncodeToString(etagBytes)
+}
+
+// GetDecryptedRange - To decrypt the range (off, length) of the
+// decrypted object stream, we need to read the range (encOff,
+// encLength) of the encrypted object stream to decrypt it, and
+// compute skipLen, the number of bytes to skip in the beginning of
+// the encrypted range.
+//
+// In addition we also compute the object part number for where the
+// requested range starts, along with the DARE sequence number within
+// that part. For single part objects, the partStart will be 0.
+func (o *ObjectInfo) GetDecryptedRange(rs *HTTPRangeSpec) (encOff, encLength, skipLen int64, seqNumber uint32, partStart int, err error) {
+ if _, ok := crypto.IsEncrypted(o.UserDefined); !ok {
+ err = errors.New("Object is not encrypted")
+ return encOff, encLength, skipLen, seqNumber, partStart, err
+ }
+
+ if rs == nil {
+ // No range, so offsets refer to the whole object.
+ return 0, o.Size, 0, 0, 0, nil
+ }
+
+ // Assemble slice of (decrypted) part sizes in `sizes`
+ var sizes []int64
+ var decObjSize int64 // decrypted total object size
+ if o.isMultipart() {
+ sizes = make([]int64, len(o.Parts))
+ for i, part := range o.Parts {
+ var partSize uint64
+ partSize, err = sio.DecryptedSize(uint64(part.Size))
+ if err != nil {
+ err = errObjectTampered
+ return encOff, encLength, skipLen, seqNumber, partStart, err
+ }
+ sizes[i] = int64(partSize)
+ decObjSize += int64(partSize)
+ }
+ } else {
+ var partSize uint64
+ partSize, err = sio.DecryptedSize(uint64(o.Size))
+ if err != nil {
+ err = errObjectTampered
+ return encOff, encLength, skipLen, seqNumber, partStart, err
+ }
+ sizes = []int64{int64(partSize)}
+ decObjSize = sizes[0]
+ }
+
+ var off, length int64
+ off, length, err = rs.GetOffsetLength(decObjSize)
+ if err != nil {
+ return encOff, encLength, skipLen, seqNumber, partStart, err
+ }
+
+ // At this point, we have:
+ //
+ // 1. the decrypted part sizes in `sizes` (single element for
+ // single part object) and total decrypted object size `decObjSize`
+ //
+ // 2. the (decrypted) start offset `off` and (decrypted)
+ // length to read `length`
+ //
+ // These are the inputs to the rest of the algorithm below.
+
+ // Locate the part containing the start of the required range
+ var partEnd int
+ var cumulativeSum, encCumulativeSum int64
+ for i, size := range sizes {
+ if off < cumulativeSum+size {
+ partStart = i
+ break
+ }
+ cumulativeSum += size
+ encPartSize, _ := sio.EncryptedSize(uint64(size))
+ encCumulativeSum += int64(encPartSize)
+ }
+ // partStart is always found in the loop above,
+ // because off is validated.
+
+ sseDAREEncPackageBlockSize := int64(SSEDAREPackageBlockSize + SSEDAREPackageMetaSize)
+ startPkgNum := (off - cumulativeSum) / SSEDAREPackageBlockSize
+
+ // Now we can calculate the number of bytes to skip
+ skipLen = (off - cumulativeSum) % SSEDAREPackageBlockSize
+
+ encOff = encCumulativeSum + startPkgNum*sseDAREEncPackageBlockSize
+ // Locate the part containing the end of the required range
+ endOffset := off + length - 1
+ for i1, size := range sizes[partStart:] {
+ i := partStart + i1
+ if endOffset < cumulativeSum+size {
+ partEnd = i
+ break
+ }
+ cumulativeSum += size
+ encPartSize, _ := sio.EncryptedSize(uint64(size))
+ encCumulativeSum += int64(encPartSize)
+ }
+ // partEnd is always found in the loop above, because off and
+ // length are validated.
+ endPkgNum := (endOffset - cumulativeSum) / SSEDAREPackageBlockSize
+ // Compute endEncOffset with one additional DARE package (so
+ // we read the package containing the last desired byte).
+ endEncOffset := encCumulativeSum + (endPkgNum+1)*sseDAREEncPackageBlockSize
+ // Check if the DARE package containing the end offset is a
+ // full sized package (as the last package in the part may be
+ // smaller)
+ lastPartSize, _ := sio.EncryptedSize(uint64(sizes[partEnd]))
+ if endEncOffset > encCumulativeSum+int64(lastPartSize) {
+ endEncOffset = encCumulativeSum + int64(lastPartSize)
+ }
+ encLength = endEncOffset - encOff
+ // Set the sequence number as the starting package number of
+ // the requested block
+ seqNumber = uint32(startPkgNum)
+ return encOff, encLength, skipLen, seqNumber, partStart, nil
+}
+
+// EncryptedSize returns the size of the object after encryption.
+// An encrypted object is always larger than a plain object
+// except for zero size objects.
+func (o *ObjectInfo) EncryptedSize() int64 {
+ size, err := sio.EncryptedSize(uint64(o.Size))
+ if err != nil {
+ // This cannot happen since AWS S3 allows parts to be 5GB at most
+ // sio max. size is 256 TB
+ reqInfo := (&logger.ReqInfo{}).AppendTags("size", strconv.FormatUint(size, 10))
+ ctx := logger.SetReqInfo(GlobalContext, reqInfo)
+ logger.CriticalIf(ctx, err)
+ }
+ return int64(size)
+}
+
+// DecryptObjectInfo tries to decrypt the provided object if it is encrypted.
+// It fails if the object is encrypted and the HTTP headers don't contain
+// SSE-C headers or the object is not encrypted but SSE-C headers are provided. (AWS behavior)
+// DecryptObjectInfo returns 'ErrNone' if the object is not encrypted or the
+// decryption succeeded.
+//
+// DecryptObjectInfo also returns whether the object is encrypted or not.
+func DecryptObjectInfo(info *ObjectInfo, r *http.Request) (encrypted bool, err error) {
+ // Directories are never encrypted.
+ if info.IsDir {
+ return false, nil
+ }
+ if r == nil {
+ return false, errInvalidArgument
+ }
+
+ headers := r.Header
+
+ // disallow X-Amz-Server-Side-Encryption header on HEAD and GET
+ switch r.Method {
+ case http.MethodGet, http.MethodHead:
+ if crypto.S3.IsRequested(headers) || crypto.S3KMS.IsRequested(headers) {
+ return false, errInvalidEncryptionParameters
+ }
+ }
+
+ _, encrypted = crypto.IsEncrypted(info.UserDefined)
+ if !encrypted && crypto.SSEC.IsRequested(headers) && r.Header.Get(xhttp.AmzCopySource) == "" {
+ return false, errInvalidEncryptionParameters
+ }
+
+ if encrypted {
+ if crypto.SSEC.IsEncrypted(info.UserDefined) {
+ if !crypto.SSEC.IsRequested(headers) && !crypto.SSECopy.IsRequested(headers) {
+ if r.Header.Get(xhttp.MinIOSourceReplicationRequest) != "true" {
+ return encrypted, errEncryptedObject
+ }
+ }
+ }
+
+ if crypto.S3.IsEncrypted(info.UserDefined) && r.Header.Get(xhttp.AmzCopySource) == "" {
+ if crypto.SSEC.IsRequested(headers) || crypto.SSECopy.IsRequested(headers) {
+ return encrypted, errEncryptedObject
+ }
+ }
+
+ if crypto.S3KMS.IsEncrypted(info.UserDefined) && r.Header.Get(xhttp.AmzCopySource) == "" {
+ if crypto.SSEC.IsRequested(headers) || crypto.SSECopy.IsRequested(headers) {
+ return encrypted, errEncryptedObject
+ }
+ }
+
+ if _, err = info.DecryptedSize(); err != nil {
+ return encrypted, err
+ }
+
+ if _, ok := crypto.IsEncrypted(info.UserDefined); ok && !crypto.IsMultiPart(info.UserDefined) {
+ info.ETag = getDecryptedETag(headers, *info, false)
+ }
+ }
+
+ return encrypted, nil
+}
+
+type (
+ objectMetaEncryptFn func(baseKey string, data []byte) []byte
+ objectMetaDecryptFn func(baseKey string, data []byte) ([]byte, error)
+)
+
+// metadataEncrypter returns a function that will read data from input,
+// encrypt it using the provided key and return the result.
+// 0 sized inputs are passed through.
+func metadataEncrypter(key crypto.ObjectKey) objectMetaEncryptFn {
+ return func(baseKey string, data []byte) []byte {
+ if len(data) == 0 {
+ return data
+ }
+ var buffer bytes.Buffer
+ mac := hmac.New(sha256.New, key[:])
+ mac.Write([]byte(baseKey))
+ if _, err := sio.Encrypt(&buffer, bytes.NewReader(data), sio.Config{Key: mac.Sum(nil)}); err != nil {
+ logger.CriticalIf(context.Background(), errors.New("unable to encrypt using object key"))
+ }
+ return buffer.Bytes()
+ }
+}
+
+// metadataDecrypter reverses metadataEncrypter.
+func (o *ObjectInfo) metadataDecrypter(h http.Header) objectMetaDecryptFn {
+ return func(baseKey string, input []byte) ([]byte, error) {
+ if len(input) == 0 {
+ return input, nil
+ }
+ var key []byte
+ if crypto.SSECopy.IsRequested(h) {
+ sseCopyKey, err := crypto.SSECopy.ParseHTTP(h)
+ if err != nil {
+ return nil, err
+ }
+ key = sseCopyKey[:]
+ } else {
+ if k, err := crypto.SSEC.ParseHTTP(h); err == nil {
+ key = k[:]
+ }
+ }
+ key, err := decryptObjectMeta(key, o.Bucket, o.Name, o.UserDefined)
+ if err != nil {
+ return nil, err
+ }
+ mac := hmac.New(sha256.New, key)
+ mac.Write([]byte(baseKey))
+ return sio.DecryptBuffer(nil, input, sio.Config{Key: mac.Sum(nil)})
+ }
+}
+
+// decryptPartsChecksums will attempt to decrypt and decode part checksums, and save
+// only the decrypted part checksum values on ObjectInfo directly.
+// if part > 0, and we have the checksum for the part that will be returned.
+func (o *ObjectInfo) decryptPartsChecksums(h http.Header) {
+ data := o.Checksum
+ if len(data) == 0 {
+ return
+ }
+ if _, encrypted := crypto.IsEncrypted(o.UserDefined); encrypted {
+ decrypted, err := o.metadataDecrypter(h)("object-checksum", data)
+ if err != nil {
+ if !errors.Is(err, crypto.ErrSecretKeyMismatch) {
+ encLogIf(GlobalContext, err)
+ }
+ return
+ }
+ data = decrypted
+ }
+ cs := hash.ReadPartCheckSums(data)
+ if len(cs) == len(o.Parts) {
+ for i := range o.Parts {
+ o.Parts[i].Checksums = cs[i]
+ }
+ }
+}
+
+// decryptChecksum will attempt to decrypt the ObjectInfo.Checksum, returns the decrypted value
+// An error is only returned if it was encrypted and the decryption failed.
+func (o *ObjectInfo) decryptChecksum(h http.Header) ([]byte, error) {
+ data := o.Checksum
+ if len(data) == 0 {
+ return data, nil
+ }
+ if _, encrypted := crypto.IsEncrypted(o.UserDefined); encrypted {
+ decrypted, err := o.metadataDecrypter(h)("object-checksum", data)
+ if err != nil {
+ return nil, err
+ }
+ data = decrypted
+ }
+ return data, nil
+}
+
+// metadataEncryptFn provides an encryption function for metadata.
+// Will return nil, nil if unencrypted.
+func (o *ObjectInfo) metadataEncryptFn(headers http.Header) (objectMetaEncryptFn, error) {
+ kind, _ := crypto.IsEncrypted(o.UserDefined)
+ switch kind {
+ case crypto.SSEC:
+ if crypto.SSECopy.IsRequested(headers) {
+ key, err := crypto.SSECopy.ParseHTTP(headers)
+ if err != nil {
+ return nil, err
+ }
+ objectEncryptionKey, err := decryptObjectMeta(key[:], o.Bucket, o.Name, o.UserDefined)
+ if err != nil {
+ return nil, err
+ }
+ if len(objectEncryptionKey) == 32 {
+ var key crypto.ObjectKey
+ copy(key[:], objectEncryptionKey)
+ return metadataEncrypter(key), nil
+ }
+ return nil, errors.New("metadataEncryptFn: unexpected key size")
+ }
+ case crypto.S3, crypto.S3KMS:
+ objectEncryptionKey, err := decryptObjectMeta(nil, o.Bucket, o.Name, o.UserDefined)
+ if err != nil {
+ return nil, err
+ }
+ if len(objectEncryptionKey) == 32 {
+ var key crypto.ObjectKey
+ copy(key[:], objectEncryptionKey)
+ return metadataEncrypter(key), nil
+ }
+ return nil, errors.New("metadataEncryptFn: unexpected key size")
+ }
+
+ return nil, nil
+}
+
+// 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.
+// 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
+ if len(data) == 0 {
+ return nil, false
+ }
+ if part > 0 && !crypto.SSEC.IsEncrypted(o.UserDefined) {
+ // already decrypted in ToObjectInfo for multipart objects
+ for _, pi := range o.Parts {
+ if pi.Number == part {
+ return pi.Checksums, true
+ }
+ }
+ }
+ if _, encrypted := crypto.IsEncrypted(o.UserDefined); encrypted {
+ decrypted, err := o.metadataDecrypter(h)("object-checksum", data)
+ if err != nil {
+ if err != crypto.ErrSecretKeyMismatch {
+ encLogIf(GlobalContext, err)
+ }
+ return nil, part > 0
+ }
+ data = decrypted
+ }
+ return hash.ReadCheckSums(data, part)
+}
diff --git a/cmd/encryption-v1_test.go b/cmd/encryption-v1_test.go
new file mode 100644
index 00000000..95f78ef8
--- /dev/null
+++ b/cmd/encryption-v1_test.go
@@ -0,0 +1,648 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "encoding/base64"
+ "net/http"
+ "testing"
+
+ humanize "github.com/dustin/go-humanize"
+ "github.com/minio/minio-go/v7/pkg/encrypt"
+ "github.com/minio/minio/internal/crypto"
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/sio"
+)
+
+var encryptRequestTests = []struct {
+ header map[string]string
+ metadata map[string]string
+}{
+ {
+ header: map[string]string{
+ xhttp.AmzServerSideEncryptionCustomerAlgorithm: "AES256",
+ xhttp.AmzServerSideEncryptionCustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
+ xhttp.AmzServerSideEncryptionCustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
+ },
+ metadata: map[string]string{},
+ },
+ {
+ header: map[string]string{
+ xhttp.AmzServerSideEncryptionCustomerAlgorithm: "AES256",
+ xhttp.AmzServerSideEncryptionCustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
+ xhttp.AmzServerSideEncryptionCustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
+ },
+ metadata: map[string]string{
+ xhttp.AmzServerSideEncryptionCustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
+ },
+ },
+}
+
+func TestEncryptRequest(t *testing.T) {
+ defer func(flag bool) { globalIsTLS = flag }(globalIsTLS)
+ globalIsTLS = true
+ for i, test := range encryptRequestTests {
+ content := bytes.NewReader(make([]byte, 64))
+ req := &http.Request{Header: http.Header{}}
+ for k, v := range test.header {
+ req.Header.Set(k, v)
+ }
+ _, _, err := EncryptRequest(content, req, "bucket", "object", test.metadata)
+ if err != nil {
+ t.Fatalf("Test %d: Failed to encrypt request: %v", i, err)
+ }
+ if kdf, ok := test.metadata[crypto.MetaAlgorithm]; !ok {
+ t.Errorf("Test %d: ServerSideEncryptionKDF must be part of metadata: %v", i, kdf)
+ }
+ if iv, ok := test.metadata[crypto.MetaIV]; !ok {
+ t.Errorf("Test %d: crypto.SSEIV must be part of metadata: %v", i, iv)
+ }
+ if mac, ok := test.metadata[crypto.MetaSealedKeySSEC]; !ok {
+ t.Errorf("Test %d: ServerSideEncryptionKeyMAC must be part of metadata: %v", i, mac)
+ }
+ }
+}
+
+var decryptObjectMetaTests = []struct {
+ info ObjectInfo
+ request *http.Request
+ expErr error
+}{
+ {
+ info: ObjectInfo{Size: 100},
+ request: &http.Request{Header: http.Header{}},
+ expErr: nil,
+ },
+ {
+ info: ObjectInfo{Size: 100, UserDefined: map[string]string{crypto.MetaAlgorithm: crypto.InsecureSealAlgorithm}},
+ request: &http.Request{Header: http.Header{xhttp.AmzServerSideEncryption: []string{xhttp.AmzEncryptionAES}}},
+ expErr: nil,
+ },
+ {
+ info: ObjectInfo{Size: 0, UserDefined: map[string]string{crypto.MetaAlgorithm: crypto.InsecureSealAlgorithm}},
+ request: &http.Request{Header: http.Header{xhttp.AmzServerSideEncryption: []string{xhttp.AmzEncryptionAES}}},
+ expErr: nil,
+ },
+ {
+ info: ObjectInfo{Size: 100, UserDefined: map[string]string{crypto.MetaSealedKeySSEC: "EAAfAAAAAAD7v1hQq3PFRUHsItalxmrJqrOq6FwnbXNarxOOpb8jTWONPPKyM3Gfjkjyj6NCf+aB/VpHCLCTBA=="}},
+ request: &http.Request{Header: http.Header{}},
+ expErr: errEncryptedObject,
+ },
+ {
+ info: ObjectInfo{Size: 100, UserDefined: map[string]string{}},
+ request: &http.Request{Method: http.MethodGet, Header: http.Header{xhttp.AmzServerSideEncryptionCustomerAlgorithm: []string{xhttp.AmzEncryptionAES}}},
+ expErr: errInvalidEncryptionParameters,
+ },
+ {
+ info: ObjectInfo{Size: 100, UserDefined: map[string]string{}},
+ request: &http.Request{Method: http.MethodHead, Header: http.Header{xhttp.AmzServerSideEncryptionCustomerAlgorithm: []string{xhttp.AmzEncryptionAES}}},
+ expErr: errInvalidEncryptionParameters,
+ },
+ {
+ info: ObjectInfo{Size: 31, UserDefined: map[string]string{crypto.MetaAlgorithm: crypto.InsecureSealAlgorithm}},
+ request: &http.Request{Header: http.Header{xhttp.AmzServerSideEncryptionCustomerAlgorithm: []string{xhttp.AmzEncryptionAES}}},
+ expErr: errObjectTampered,
+ },
+}
+
+func TestDecryptObjectInfo(t *testing.T) {
+ for i, test := range decryptObjectMetaTests {
+ if encrypted, err := DecryptObjectInfo(&test.info, test.request); err != test.expErr {
+ t.Errorf("Test %d: Decryption returned wrong error code: got %d , want %d", i, err, test.expErr)
+ } else if _, enc := crypto.IsEncrypted(test.info.UserDefined); encrypted && enc != encrypted {
+ t.Errorf("Test %d: Decryption thinks object is encrypted but it is not", i)
+ } else if !encrypted && enc != encrypted {
+ t.Errorf("Test %d: Decryption thinks object is not encrypted but it is", i)
+ }
+ }
+}
+
+var decryptETagTests = []struct {
+ ObjectKey crypto.ObjectKey
+ ObjectInfo ObjectInfo
+ ShouldFail bool
+ ETag string
+}{
+ {
+ ObjectKey: [32]byte{},
+ ObjectInfo: ObjectInfo{ETag: "20000f00f27834c9a2654927546df57f9e998187496394d4ee80f3d9978f85f3c7d81f72600cdbe03d80dc5a13d69354"},
+ ETag: "8ad3fe6b84bf38489e95c701c84355b6",
+ },
+ {
+ ObjectKey: [32]byte{},
+ ObjectInfo: ObjectInfo{ETag: "20000f00f27834c9a2654927546df57f9e998187496394d4ee80f3d9978f85f3c7d81f72600cdbe03d80dc5a13d6935"},
+ ETag: "",
+ ShouldFail: true, // ETag is not a valid hex value
+ },
+ {
+ ObjectKey: [32]byte{},
+ ObjectInfo: ObjectInfo{ETag: "00000f00f27834c9a2654927546df57f9e998187496394d4ee80f3d9978f85f3c7d81f72600cdbe03d80dc5a13d69354"},
+ ETag: "",
+ ShouldFail: true, // modified ETag
+ },
+
+ // Special tests for ETags that end with a '-x'
+ {
+ ObjectKey: [32]byte{},
+ ObjectInfo: ObjectInfo{ETag: "916516b396f0f4d4f2a0e7177557bec4-1"},
+ ETag: "916516b396f0f4d4f2a0e7177557bec4-1",
+ },
+ {
+ ObjectKey: [32]byte{},
+ ObjectInfo: ObjectInfo{ETag: "916516b396f0f4d4f2a0e7177557bec4-738"},
+ ETag: "916516b396f0f4d4f2a0e7177557bec4-738",
+ },
+ {
+ ObjectKey: [32]byte{},
+ ObjectInfo: ObjectInfo{ETag: "916516b396f0f4d4f2a0e7177557bec4-Q"},
+ ETag: "",
+ ShouldFail: true, // Q is not a number
+ },
+ {
+ ObjectKey: [32]byte{},
+ ObjectInfo: ObjectInfo{ETag: "16516b396f0f4d4f2a0e7177557bec4-1"},
+ ETag: "",
+ ShouldFail: true, // ETag prefix is not a valid hex value
+ },
+ {
+ ObjectKey: [32]byte{},
+ ObjectInfo: ObjectInfo{ETag: "16516b396f0f4d4f2a0e7177557bec4-1-2"},
+ ETag: "",
+ ShouldFail: true, // ETag contains multiple: -
+ },
+}
+
+func TestDecryptETag(t *testing.T) {
+ for i, test := range decryptETagTests {
+ etag, err := DecryptETag(test.ObjectKey, test.ObjectInfo)
+ if err != nil && !test.ShouldFail {
+ t.Fatalf("Test %d: should succeed but failed: %v", i, err)
+ }
+ if err == nil && test.ShouldFail {
+ t.Fatalf("Test %d: should fail but succeeded", i)
+ }
+ if err == nil {
+ if etag != test.ETag {
+ t.Fatalf("Test %d: ETag mismatch: got %s - want %s", i, etag, test.ETag)
+ }
+ }
+ }
+}
+
+// Tests for issue reproduced when getting the right encrypted
+// offset of the object.
+func TestGetDecryptedRange_Issue50(t *testing.T) {
+ rs, err := parseRequestRangeSpec("bytes=594870256-594870263")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ objInfo := ObjectInfo{
+ Bucket: "bucket",
+ Name: "object",
+ Size: 595160760,
+ UserDefined: map[string]string{
+ crypto.MetaMultipart: "",
+ crypto.MetaIV: "HTexa=",
+ crypto.MetaAlgorithm: "DAREv2-HMAC-SHA256",
+ crypto.MetaSealedKeySSEC: "IAA8PGAA==",
+ ReservedMetadataPrefix + "actual-size": "594870264",
+ "content-type": "application/octet-stream",
+ "etag": "166b1545b4c1535294ee0686678bea8c-2",
+ },
+ Parts: []ObjectPartInfo{
+ {
+ Number: 1,
+ Size: 297580380,
+ ActualSize: 297435132,
+ },
+ {
+ Number: 2,
+ Size: 297580380,
+ ActualSize: 297435132,
+ },
+ },
+ }
+
+ encOff, encLength, skipLen, seqNumber, partStart, err := objInfo.GetDecryptedRange(rs)
+ if err != nil {
+ t.Fatalf("Test: failed %s", err)
+ }
+ if encOff != 595127964 {
+ t.Fatalf("Test: expected %d, got %d", 595127964, encOff)
+ }
+ if encLength != 32796 {
+ t.Fatalf("Test: expected %d, got %d", 32796, encLength)
+ }
+ if skipLen != 32756 {
+ t.Fatalf("Test: expected %d, got %d", 32756, skipLen)
+ }
+ if seqNumber != 4538 {
+ t.Fatalf("Test: expected %d, got %d", 4538, seqNumber)
+ }
+ if partStart != 1 {
+ t.Fatalf("Test: expected %d, got %d", 1, partStart)
+ }
+}
+
+func TestGetDecryptedRange(t *testing.T) {
+ var (
+ pkgSz = int64(64) * humanize.KiByte
+ minPartSz = int64(5) * humanize.MiByte
+ maxPartSz = int64(5) * humanize.GiByte
+
+ getEncSize = func(s int64) int64 {
+ v, _ := sio.EncryptedSize(uint64(s))
+ return int64(v)
+ }
+ udMap = func(isMulti bool) map[string]string {
+ m := map[string]string{
+ crypto.MetaAlgorithm: crypto.InsecureSealAlgorithm,
+ crypto.MetaMultipart: "1",
+ }
+ if !isMulti {
+ delete(m, crypto.MetaMultipart)
+ }
+ return m
+ }
+ )
+
+ // Single part object tests
+
+ mkSPObj := func(s int64) ObjectInfo {
+ return ObjectInfo{
+ Size: getEncSize(s),
+ UserDefined: udMap(false),
+ }
+ }
+
+ testSP := []struct {
+ decSz int64
+ oi ObjectInfo
+ }{
+ {0, mkSPObj(0)},
+ {1, mkSPObj(1)},
+ {pkgSz - 1, mkSPObj(pkgSz - 1)},
+ {pkgSz, mkSPObj(pkgSz)},
+ {2*pkgSz - 1, mkSPObj(2*pkgSz - 1)},
+ {minPartSz, mkSPObj(minPartSz)},
+ {maxPartSz, mkSPObj(maxPartSz)},
+ }
+
+ for i, test := range testSP {
+ {
+ // nil range
+ o, l, skip, sn, ps, err := test.oi.GetDecryptedRange(nil)
+ if err != nil {
+ t.Errorf("Case %d: unexpected err: %v", i, err)
+ }
+ if skip != 0 || sn != 0 || ps != 0 || o != 0 || l != getEncSize(test.decSz) {
+ t.Errorf("Case %d: test failed: %d %d %d %d %d", i, o, l, skip, sn, ps)
+ }
+ }
+
+ if test.decSz >= 10 {
+ // first 10 bytes
+ o, l, skip, sn, ps, err := test.oi.GetDecryptedRange(&HTTPRangeSpec{false, 0, 9})
+ if err != nil {
+ t.Errorf("Case %d: unexpected err: %v", i, err)
+ }
+ rLen := pkgSz + 32
+ if test.decSz < pkgSz {
+ rLen = test.decSz + 32
+ }
+ if skip != 0 || sn != 0 || ps != 0 || o != 0 || l != rLen {
+ t.Errorf("Case %d: test failed: %d %d %d %d %d", i, o, l, skip, sn, ps)
+ }
+ }
+
+ kb32 := int64(32) * humanize.KiByte
+ if test.decSz >= (64+32)*humanize.KiByte {
+ // Skip the first 32Kib, and read the next 64Kib
+ o, l, skip, sn, ps, err := test.oi.GetDecryptedRange(&HTTPRangeSpec{false, kb32, 3*kb32 - 1})
+ if err != nil {
+ t.Errorf("Case %d: unexpected err: %v", i, err)
+ }
+ rLen := (pkgSz + 32) * 2
+ if test.decSz < 2*pkgSz {
+ rLen = (pkgSz + 32) + (test.decSz - pkgSz + 32)
+ }
+ if skip != kb32 || sn != 0 || ps != 0 || o != 0 || l != rLen {
+ t.Errorf("Case %d: test failed: %d %d %d %d %d", i, o, l, skip, sn, ps)
+ }
+ }
+
+ if test.decSz >= (64*2+32)*humanize.KiByte {
+ // Skip the first 96Kib and read the next 64Kib
+ o, l, skip, sn, ps, err := test.oi.GetDecryptedRange(&HTTPRangeSpec{false, 3 * kb32, 5*kb32 - 1})
+ if err != nil {
+ t.Errorf("Case %d: unexpected err: %v", i, err)
+ }
+ rLen := (pkgSz + 32) * 2
+ if test.decSz-pkgSz < 2*pkgSz {
+ rLen = (pkgSz + 32) + (test.decSz - pkgSz + 32*2)
+ }
+ if skip != kb32 || sn != 1 || ps != 0 || o != pkgSz+32 || l != rLen {
+ t.Errorf("Case %d: test failed: %d %d %d %d %d", i, o, l, skip, sn, ps)
+ }
+ }
+ }
+
+ // Multipart object tests
+ var (
+ // make a multipart object-info given part sizes
+ mkMPObj = func(sizes []int64) ObjectInfo {
+ r := make([]ObjectPartInfo, len(sizes))
+ sum := int64(0)
+ for i, s := range sizes {
+ r[i].Number = i
+ r[i].Size = getEncSize(s)
+ sum += r[i].Size
+ }
+ return ObjectInfo{
+ Size: sum,
+ UserDefined: udMap(true),
+ Parts: r,
+ }
+ }
+ // Simple useful utilities
+ repeat = func(k int64, n int) []int64 {
+ a := []int64{}
+ for range n {
+ a = append(a, k)
+ }
+ return a
+ }
+ lsum = func(s []int64) int64 {
+ sum := int64(0)
+ for _, i := range s {
+ if i < 0 {
+ return -1
+ }
+ sum += i
+ }
+ return sum
+ }
+ esum = func(oi ObjectInfo) int64 {
+ sum := int64(0)
+ for _, i := range oi.Parts {
+ sum += i.Size
+ }
+ return sum
+ }
+ )
+
+ s1 := []int64{5487701, 5487799, 3}
+ s2 := repeat(5487701, 5)
+ s3 := repeat(maxPartSz, 10000)
+ testMPs := []struct {
+ decSizes []int64
+ oi ObjectInfo
+ }{
+ {s1, mkMPObj(s1)},
+ {s2, mkMPObj(s2)},
+ {s3, mkMPObj(s3)},
+ }
+
+ // This function is a reference (re-)implementation of
+ // decrypted range computation, written solely for the purpose
+ // of the unit tests.
+ //
+ // `s` gives the decrypted part sizes, and the other
+ // parameters describe the desired read segment. When
+ // `isFromEnd` is true, `skipLen` argument is ignored.
+ decryptedRangeRef := func(s []int64, skipLen, readLen int64, isFromEnd bool) (o, l, skip int64, sn uint32, ps int) {
+ oSize := lsum(s)
+ if isFromEnd {
+ skipLen = oSize - readLen
+ }
+ if skipLen < 0 || readLen < 0 || oSize < 0 || skipLen+readLen > oSize {
+ t.Fatalf("Impossible read specified: %d %d %d", skipLen, readLen, oSize)
+ }
+
+ var cumulativeSum, cumulativeEncSum int64
+ toRead := readLen
+ readStart := false
+ for i, v := range s {
+ partOffset := int64(0)
+ partDarePkgOffset := int64(0)
+ if !readStart && cumulativeSum+v > skipLen {
+ // Read starts at the current part
+ readStart = true
+
+ partOffset = skipLen - cumulativeSum
+
+ // All return values except `l` are
+ // calculated here.
+ sn = uint32(partOffset / pkgSz)
+ skip = partOffset % pkgSz
+ ps = i
+ o = cumulativeEncSum + int64(sn)*(pkgSz+32)
+
+ partDarePkgOffset = partOffset - skip
+ }
+ if readStart {
+ currentPartBytes := v - partOffset
+ currentPartDareBytes := v - partDarePkgOffset
+ if currentPartBytes < toRead {
+ toRead -= currentPartBytes
+ l += getEncSize(currentPartDareBytes)
+ } else {
+ // current part has the last
+ // byte required
+ lbPartOffset := partOffset + toRead - 1
+
+ // round up the lbPartOffset
+ // to the end of the
+ // corresponding DARE package
+ lbPkgEndOffset := min(lbPartOffset-(lbPartOffset%pkgSz)+pkgSz, v)
+ bytesToDrop := v - lbPkgEndOffset
+
+ // Last segment to update `l`
+ l += getEncSize(currentPartDareBytes - bytesToDrop)
+ break
+ }
+ }
+
+ cumulativeSum += v
+ cumulativeEncSum += getEncSize(v)
+ }
+ return o, l, skip, sn, ps
+ }
+
+ for i, test := range testMPs {
+ {
+ // nil range
+ o, l, skip, sn, ps, err := test.oi.GetDecryptedRange(nil)
+ if err != nil {
+ t.Errorf("Case %d: unexpected err: %v", i, err)
+ }
+ if o != 0 || l != esum(test.oi) || skip != 0 || sn != 0 || ps != 0 {
+ t.Errorf("Case %d: test failed: %d %d %d %d %d", i, o, l, skip, sn, ps)
+ }
+ }
+
+ // Skip 1Mib and read 1Mib (in the decrypted object)
+ //
+ // The check below ensures the object is large enough
+ // for the read.
+ if lsum(test.decSizes) >= 2*humanize.MiByte {
+ skipLen, readLen := int64(1)*humanize.MiByte, int64(1)*humanize.MiByte
+ o, l, skip, sn, ps, err := test.oi.GetDecryptedRange(&HTTPRangeSpec{false, skipLen, skipLen + readLen - 1})
+ if err != nil {
+ t.Errorf("Case %d: unexpected err: %v", i, err)
+ }
+
+ oRef, lRef, skipRef, snRef, psRef := decryptedRangeRef(test.decSizes, skipLen, readLen, false)
+ if o != oRef || l != lRef || skip != skipRef || sn != snRef || ps != psRef {
+ t.Errorf("Case %d: test failed: %d %d %d %d %d (Ref: %d %d %d %d %d)",
+ i, o, l, skip, sn, ps, oRef, lRef, skipRef, snRef, psRef)
+ }
+ }
+
+ // Read the last 6Mib+1 bytes of the (decrypted)
+ // object
+ //
+ // The check below ensures the object is large enough
+ // for the read.
+ readLen := int64(6)*humanize.MiByte + 1
+ if lsum(test.decSizes) >= readLen {
+ o, l, skip, sn, ps, err := test.oi.GetDecryptedRange(&HTTPRangeSpec{true, -readLen, -1})
+ if err != nil {
+ t.Errorf("Case %d: unexpected err: %v", i, err)
+ }
+
+ oRef, lRef, skipRef, snRef, psRef := decryptedRangeRef(test.decSizes, 0, readLen, true)
+ if o != oRef || l != lRef || skip != skipRef || sn != snRef || ps != psRef {
+ t.Errorf("Case %d: test failed: %d %d %d %d %d (Ref: %d %d %d %d %d)",
+ i, o, l, skip, sn, ps, oRef, lRef, skipRef, snRef, psRef)
+ }
+ }
+ }
+}
+
+var getDefaultOptsTests = []struct {
+ headers http.Header
+ copySource bool
+ metadata map[string]string
+ encryptionType encrypt.Type
+ err error
+}{
+ {
+ headers: http.Header{
+ xhttp.AmzServerSideEncryptionCustomerAlgorithm: []string{"AES256"},
+ xhttp.AmzServerSideEncryptionCustomerKey: []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
+ xhttp.AmzServerSideEncryptionCustomerKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="},
+ },
+ copySource: false,
+ metadata: nil,
+ encryptionType: encrypt.SSEC,
+ err: nil,
+ }, // 0
+ {
+ headers: http.Header{
+ xhttp.AmzServerSideEncryptionCustomerAlgorithm: []string{"AES256"},
+ xhttp.AmzServerSideEncryptionCustomerKey: []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
+ xhttp.AmzServerSideEncryptionCustomerKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="},
+ },
+ copySource: true,
+ metadata: nil,
+ encryptionType: "",
+ err: nil,
+ }, // 1
+ {
+ headers: http.Header{
+ xhttp.AmzServerSideEncryptionCustomerAlgorithm: []string{"AES256"},
+ xhttp.AmzServerSideEncryptionCustomerKey: []string{"Mz"},
+ xhttp.AmzServerSideEncryptionCustomerKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="},
+ },
+ copySource: false,
+ metadata: nil,
+ encryptionType: "",
+ err: crypto.ErrInvalidCustomerKey,
+ }, // 2
+ {
+ headers: http.Header{xhttp.AmzServerSideEncryption: []string{"AES256"}},
+ copySource: false,
+ metadata: nil,
+ encryptionType: encrypt.S3,
+ err: nil,
+ }, // 3
+ {
+ headers: http.Header{},
+ copySource: false,
+ metadata: map[string]string{
+ crypto.MetaSealedKeyS3: base64.StdEncoding.EncodeToString(make([]byte, 64)),
+ crypto.MetaKeyID: "kms-key",
+ crypto.MetaDataEncryptionKey: "m-key",
+ },
+ encryptionType: encrypt.S3,
+ err: nil,
+ }, // 4
+ {
+ headers: http.Header{},
+ copySource: true,
+ metadata: map[string]string{
+ crypto.MetaSealedKeyS3: base64.StdEncoding.EncodeToString(make([]byte, 64)),
+ crypto.MetaKeyID: "kms-key",
+ crypto.MetaDataEncryptionKey: "m-key",
+ },
+ encryptionType: "",
+ err: nil,
+ }, // 5
+ {
+ headers: http.Header{
+ xhttp.AmzServerSideEncryptionCopyCustomerAlgorithm: []string{"AES256"},
+ xhttp.AmzServerSideEncryptionCopyCustomerKey: []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
+ xhttp.AmzServerSideEncryptionCopyCustomerKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="},
+ },
+ copySource: true,
+ metadata: nil,
+ encryptionType: encrypt.SSEC,
+ err: nil,
+ }, // 6
+ {
+ headers: http.Header{
+ xhttp.AmzServerSideEncryptionCopyCustomerAlgorithm: []string{"AES256"},
+ xhttp.AmzServerSideEncryptionCopyCustomerKey: []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
+ xhttp.AmzServerSideEncryptionCopyCustomerKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="},
+ },
+ copySource: false,
+ metadata: nil,
+ encryptionType: "",
+ err: nil,
+ }, // 7
+}
+
+func TestGetDefaultOpts(t *testing.T) {
+ for i, test := range getDefaultOptsTests {
+ opts, err := getDefaultOpts(test.headers, test.copySource, test.metadata)
+ if test.err != err {
+ t.Errorf("Case %d: expected err: %v , actual err: %v", i, test.err, err)
+ }
+ if err == nil {
+ if opts.ServerSideEncryption == nil && test.encryptionType != "" {
+ t.Errorf("Case %d: expected opts to be of %v encryption type", i, test.encryptionType)
+ }
+ if opts.ServerSideEncryption != nil && test.encryptionType != opts.ServerSideEncryption.Type() {
+ t.Errorf("Case %d: expected opts to have encryption type %v but was %v ", i, test.encryptionType, opts.ServerSideEncryption.Type())
+ }
+ }
+ }
+}
diff --git a/cmd/endpoint-ellipses.go b/cmd/endpoint-ellipses.go
new file mode 100644
index 00000000..f1924945
--- /dev/null
+++ b/cmd/endpoint-ellipses.go
@@ -0,0 +1,523 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "errors"
+ "fmt"
+ "net/url"
+ "runtime"
+ "slices"
+ "strings"
+
+ "github.com/cespare/xxhash/v2"
+ "github.com/minio/minio-go/v7/pkg/set"
+ "github.com/minio/minio/internal/config"
+ "github.com/minio/pkg/v3/ellipses"
+ "github.com/minio/pkg/v3/env"
+)
+
+// This file implements and supports ellipses pattern for
+// `minio server` command line arguments.
+
+// Endpoint set represents parsed ellipses values, also provides
+// methods to get the sets of endpoints.
+type endpointSet struct {
+ argPatterns []ellipses.ArgPattern
+ endpoints []string // Endpoints saved from previous GetEndpoints().
+ setIndexes [][]uint64 // All the sets.
+}
+
+// Supported set sizes this is used to find the optimal
+// single set size.
+var setSizes = []uint64{2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
+
+// getDivisibleSize - returns a greatest common divisor of
+// all the ellipses sizes.
+func getDivisibleSize(totalSizes []uint64) (result uint64) {
+ gcd := func(x, y uint64) uint64 {
+ for y != 0 {
+ x, y = y, x%y
+ }
+ return x
+ }
+ result = totalSizes[0]
+ for i := 1; i < len(totalSizes); i++ {
+ result = gcd(result, totalSizes[i])
+ }
+ return result
+}
+
+// isValidSetSize - checks whether given count is a valid set size for erasure coding.
+var isValidSetSize = func(count uint64) bool {
+ return (count >= setSizes[0] && count <= setSizes[len(setSizes)-1])
+}
+
+func commonSetDriveCount(divisibleSize uint64, setCounts []uint64) (setSize uint64) {
+ // prefers setCounts to be sorted for optimal behavior.
+ if divisibleSize < setCounts[len(setCounts)-1] {
+ return divisibleSize
+ }
+
+ // Figure out largest value of total_drives_in_erasure_set which results
+ // in least number of total_drives/total_drives_erasure_set ratio.
+ prevD := divisibleSize / setCounts[0]
+ for _, cnt := range setCounts {
+ if divisibleSize%cnt == 0 {
+ d := divisibleSize / cnt
+ if d <= prevD {
+ prevD = d
+ setSize = cnt
+ }
+ }
+ }
+ return setSize
+}
+
+// possibleSetCountsWithSymmetry returns symmetrical setCounts based on the
+// input argument patterns, the symmetry calculation is to ensure that
+// we also use uniform number of drives common across all ellipses patterns.
+func possibleSetCountsWithSymmetry(setCounts []uint64, argPatterns []ellipses.ArgPattern) []uint64 {
+ newSetCounts := make(map[uint64]struct{})
+ for _, ss := range setCounts {
+ var symmetry bool
+ for _, argPattern := range argPatterns {
+ for _, p := range argPattern {
+ if uint64(len(p.Seq)) > ss {
+ symmetry = uint64(len(p.Seq))%ss == 0
+ } else {
+ symmetry = ss%uint64(len(p.Seq)) == 0
+ }
+ }
+ }
+ // With no arg patterns, it is expected that user knows
+ // the right symmetry, so either ellipses patterns are
+ // provided (recommended) or no ellipses patterns.
+ if _, ok := newSetCounts[ss]; !ok && (symmetry || argPatterns == nil) {
+ newSetCounts[ss] = struct{}{}
+ }
+ }
+
+ setCounts = []uint64{}
+ for setCount := range newSetCounts {
+ setCounts = append(setCounts, setCount)
+ }
+
+ // Not necessarily needed but it ensures to the readers
+ // eyes that we prefer a sorted setCount slice for the
+ // subsequent function to figure out the right common
+ // divisor, it avoids loops.
+ slices.Sort(setCounts)
+
+ return setCounts
+}
+
+// getSetIndexes returns list of indexes which provides the set size
+// on each index, this function also determines the final set size
+// The final set size has the affinity towards choosing smaller
+// indexes (total sets)
+func getSetIndexes(args []string, totalSizes []uint64, setDriveCount uint64, argPatterns []ellipses.ArgPattern) (setIndexes [][]uint64, err error) {
+ if len(totalSizes) == 0 || len(args) == 0 {
+ return nil, errInvalidArgument
+ }
+
+ setIndexes = make([][]uint64, len(totalSizes))
+ for _, totalSize := range totalSizes {
+ // Check if totalSize has minimum range upto setSize
+ if totalSize < setSizes[0] || totalSize < setDriveCount {
+ msg := fmt.Sprintf("Incorrect number of endpoints provided %s", args)
+ return nil, config.ErrInvalidNumberOfErasureEndpoints(nil).Msg(msg)
+ }
+ }
+
+ commonSize := getDivisibleSize(totalSizes)
+ possibleSetCounts := func(setSize uint64) (ss []uint64) {
+ for _, s := range setSizes {
+ if setSize%s == 0 {
+ ss = append(ss, s)
+ }
+ }
+ return ss
+ }
+
+ setCounts := possibleSetCounts(commonSize)
+ if len(setCounts) == 0 {
+ msg := fmt.Sprintf("Incorrect number of endpoints provided %s, number of drives %d is not divisible by any supported erasure set sizes %d", args, commonSize, setSizes)
+ return nil, config.ErrInvalidNumberOfErasureEndpoints(nil).Msg(msg)
+ }
+
+ var setSize uint64
+ // Custom set drive count allows to override automatic distribution.
+ // only meant if you want to further optimize drive distribution.
+ if setDriveCount > 0 {
+ msg := fmt.Sprintf("Invalid set drive count. Acceptable values for %d number drives are %d", commonSize, setCounts)
+ var found bool
+ for _, ss := range setCounts {
+ if ss == setDriveCount {
+ found = true
+ }
+ }
+ if !found {
+ return nil, config.ErrInvalidErasureSetSize(nil).Msg(msg)
+ }
+
+ // No automatic symmetry calculation expected, user is on their own
+ setSize = setDriveCount
+ } else {
+ // Returns possible set counts with symmetry.
+ setCounts = possibleSetCountsWithSymmetry(setCounts, argPatterns)
+
+ if len(setCounts) == 0 {
+ msg := fmt.Sprintf("No symmetric distribution detected with input endpoints provided %s, drives %d cannot be spread symmetrically by any supported erasure set sizes %d", args, commonSize, setSizes)
+ return nil, config.ErrInvalidNumberOfErasureEndpoints(nil).Msg(msg)
+ }
+
+ // Final set size with all the symmetry accounted for.
+ setSize = commonSetDriveCount(commonSize, setCounts)
+ }
+
+ // Check whether setSize is with the supported range.
+ if !isValidSetSize(setSize) {
+ msg := fmt.Sprintf("Incorrect number of endpoints provided %s, number of drives %d is not divisible by any supported erasure set sizes %d", args, commonSize, setSizes)
+ return nil, config.ErrInvalidNumberOfErasureEndpoints(nil).Msg(msg)
+ }
+
+ for i := range totalSizes {
+ for j := uint64(0); j < totalSizes[i]/setSize; j++ {
+ setIndexes[i] = append(setIndexes[i], setSize)
+ }
+ }
+
+ return setIndexes, nil
+}
+
+// Returns all the expanded endpoints, each argument is expanded separately.
+func (s *endpointSet) getEndpoints() (endpoints []string) {
+ if len(s.endpoints) != 0 {
+ return s.endpoints
+ }
+ for _, argPattern := range s.argPatterns {
+ for _, lbls := range argPattern.Expand() {
+ endpoints = append(endpoints, strings.Join(lbls, ""))
+ }
+ }
+ s.endpoints = endpoints
+ return endpoints
+}
+
+// Get returns the sets representation of the endpoints
+// this function also intelligently decides on what will
+// be the right set size etc.
+func (s endpointSet) Get() (sets [][]string) {
+ k := uint64(0)
+ endpoints := s.getEndpoints()
+ for i := range s.setIndexes {
+ for j := range s.setIndexes[i] {
+ sets = append(sets, endpoints[k:s.setIndexes[i][j]+k])
+ k = s.setIndexes[i][j] + k
+ }
+ }
+
+ return sets
+}
+
+// Return the total size for each argument patterns.
+func getTotalSizes(argPatterns []ellipses.ArgPattern) []uint64 {
+ var totalSizes []uint64
+ for _, argPattern := range argPatterns {
+ var totalSize uint64 = 1
+ for _, p := range argPattern {
+ totalSize *= uint64(len(p.Seq))
+ }
+ totalSizes = append(totalSizes, totalSize)
+ }
+ return totalSizes
+}
+
+// Parses all arguments and returns an endpointSet which is a collection
+// of endpoints following the ellipses pattern, this is what is used
+// by the object layer for initializing itself.
+func parseEndpointSet(setDriveCount uint64, args ...string) (ep endpointSet, err error) {
+ argPatterns := make([]ellipses.ArgPattern, len(args))
+ for i, arg := range args {
+ patterns, perr := ellipses.FindEllipsesPatterns(arg)
+ if perr != nil {
+ return endpointSet{}, config.ErrInvalidErasureEndpoints(nil).Msg(perr.Error())
+ }
+ argPatterns[i] = patterns
+ }
+
+ ep.setIndexes, err = getSetIndexes(args, getTotalSizes(argPatterns), setDriveCount, argPatterns)
+ if err != nil {
+ return endpointSet{}, config.ErrInvalidErasureEndpoints(nil).Msg(err.Error())
+ }
+
+ ep.argPatterns = argPatterns
+
+ return ep, nil
+}
+
+// GetAllSets - parses all ellipses input arguments, expands them into
+// corresponding list of endpoints chunked evenly in accordance with a
+// specific set size.
+// For example: {1...64} is divided into 4 sets each of size 16.
+// This applies to even distributed setup syntax as well.
+func GetAllSets(setDriveCount uint64, args ...string) ([][]string, error) {
+ var setArgs [][]string
+ if !ellipses.HasEllipses(args...) {
+ var setIndexes [][]uint64
+ // Check if we have more one args.
+ if len(args) > 1 {
+ var err error
+ setIndexes, err = getSetIndexes(args, []uint64{uint64(len(args))}, setDriveCount, nil)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ // We are in FS setup, proceed forward.
+ setIndexes = [][]uint64{{uint64(len(args))}}
+ }
+ s := endpointSet{
+ endpoints: args,
+ setIndexes: setIndexes,
+ }
+ setArgs = s.Get()
+ } else {
+ s, err := parseEndpointSet(setDriveCount, args...)
+ if err != nil {
+ return nil, err
+ }
+ setArgs = s.Get()
+ }
+
+ uniqueArgs := set.NewStringSet()
+ for _, sargs := range setArgs {
+ for _, arg := range sargs {
+ if uniqueArgs.Contains(arg) {
+ return nil, config.ErrInvalidErasureEndpoints(nil).Msgf("Input args (%s) has duplicate ellipses", args)
+ }
+ uniqueArgs.Add(arg)
+ }
+ }
+
+ return setArgs, nil
+}
+
+// Override set drive count for manual distribution.
+const (
+ EnvErasureSetDriveCount = "MINIO_ERASURE_SET_DRIVE_COUNT"
+)
+
+type node struct {
+ nodeName string
+ disks []string
+}
+
+type endpointsList []node
+
+func (el *endpointsList) add(arg string) error {
+ u, err := url.Parse(arg)
+ if err != nil {
+ return err
+ }
+ found := false
+ list := *el
+ for i := range list {
+ if list[i].nodeName == u.Host {
+ list[i].disks = append(list[i].disks, u.String())
+ found = true
+ break
+ }
+ }
+ if !found {
+ list = append(list, node{nodeName: u.Host, disks: []string{u.String()}})
+ }
+ *el = list
+ return nil
+}
+
+type poolArgs struct {
+ args []string
+ setDriveCount uint64
+}
+
+// buildDisksLayoutFromConfFile supports with and without ellipses transparently.
+func buildDisksLayoutFromConfFile(pools []poolArgs) (layout disksLayout, err error) {
+ if len(pools) == 0 {
+ return layout, errInvalidArgument
+ }
+
+ for _, list := range pools {
+ var endpointsList endpointsList
+
+ for _, arg := range list.args {
+ switch {
+ case ellipses.HasList(arg):
+ patterns, err := ellipses.FindListPatterns(arg)
+ if err != nil {
+ return layout, err
+ }
+ for _, exp := range patterns.Expand() {
+ for _, ep := range exp {
+ if err := endpointsList.add(ep); err != nil {
+ return layout, err
+ }
+ }
+ }
+ case ellipses.HasEllipses(arg):
+ patterns, err := ellipses.FindEllipsesPatterns(arg)
+ if err != nil {
+ return layout, err
+ }
+ for _, exp := range patterns.Expand() {
+ if err := endpointsList.add(strings.Join(exp, "")); err != nil {
+ return layout, err
+ }
+ }
+ default:
+ if err := endpointsList.add(arg); err != nil {
+ return layout, err
+ }
+ }
+ }
+
+ var stopping bool
+ var singleNode bool
+ var eps []string
+
+ for i := 0; ; i++ {
+ for _, node := range endpointsList {
+ if node.nodeName == "" {
+ singleNode = true
+ }
+
+ if len(node.disks) <= i {
+ stopping = true
+ continue
+ }
+ if stopping {
+ return layout, errors.New("number of disks per node does not match")
+ }
+ eps = append(eps, node.disks[i])
+ }
+ if stopping {
+ break
+ }
+ }
+
+ for _, node := range endpointsList {
+ if node.nodeName != "" && singleNode {
+ return layout, errors.New("all arguments must but either single node or distributed")
+ }
+ }
+
+ setArgs, err := GetAllSets(list.setDriveCount, eps...)
+ if err != nil {
+ return layout, err
+ }
+
+ h := xxhash.New()
+ for _, s := range setArgs {
+ for _, d := range s {
+ h.WriteString(d)
+ }
+ }
+
+ layout.pools = append(layout.pools, poolDisksLayout{
+ cmdline: fmt.Sprintf("hash:%x", h.Sum(nil)),
+ layout: setArgs,
+ })
+ }
+ return layout, err
+}
+
+// mergeDisksLayoutFromArgs supports with and without ellipses transparently.
+func mergeDisksLayoutFromArgs(args []string, ctxt *serverCtxt) (err error) {
+ if len(args) == 0 {
+ return errInvalidArgument
+ }
+
+ ok := true
+ for _, arg := range args {
+ ok = ok && !ellipses.HasEllipses(arg)
+ }
+
+ var setArgs [][]string
+
+ v, err := env.GetInt(EnvErasureSetDriveCount, 0)
+ if err != nil {
+ return err
+ }
+ setDriveCount := uint64(v)
+
+ // None of the args have ellipses use the old style.
+ if ok {
+ setArgs, err = GetAllSets(setDriveCount, args...)
+ if err != nil {
+ return err
+ }
+ ctxt.Layout = disksLayout{
+ legacy: true,
+ pools: []poolDisksLayout{{layout: setArgs, cmdline: strings.Join(args, " ")}},
+ }
+ return err
+ }
+
+ for _, arg := range args {
+ if !ellipses.HasEllipses(arg) && len(args) > 1 {
+ // TODO: support SNSD deployments to be decommissioned in future
+ return fmt.Errorf("all args must have ellipses for pool expansion (%w) args: %s", errInvalidArgument, args)
+ }
+ setArgs, err = GetAllSets(setDriveCount, arg)
+ if err != nil {
+ return err
+ }
+ ctxt.Layout.pools = append(ctxt.Layout.pools, poolDisksLayout{cmdline: arg, layout: setArgs})
+ }
+ return err
+}
+
+// CreateServerEndpoints - validates and creates new endpoints from input args, supports
+// both ellipses and without ellipses transparently.
+func createServerEndpoints(serverAddr string, poolArgs []poolDisksLayout, legacy bool) (
+ endpointServerPools EndpointServerPools, setupType SetupType, err error,
+) {
+ if len(poolArgs) == 0 {
+ return nil, -1, errInvalidArgument
+ }
+
+ poolEndpoints, setupType, err := CreatePoolEndpoints(serverAddr, poolArgs...)
+ if err != nil {
+ return nil, -1, err
+ }
+
+ for i, endpointList := range poolEndpoints {
+ if err = endpointServerPools.Add(PoolEndpoints{
+ Legacy: legacy,
+ SetCount: len(poolArgs[i].layout),
+ DrivesPerSet: len(poolArgs[i].layout[0]),
+ Endpoints: endpointList,
+ Platform: fmt.Sprintf("OS: %s | Arch: %s", runtime.GOOS, runtime.GOARCH),
+ CmdLine: poolArgs[i].cmdline,
+ }); err != nil {
+ return nil, -1, err
+ }
+ }
+
+ return endpointServerPools, setupType, nil
+}
diff --git a/cmd/endpoint-ellipses_test.go b/cmd/endpoint-ellipses_test.go
new file mode 100644
index 00000000..6caaebbb
--- /dev/null
+++ b/cmd/endpoint-ellipses_test.go
@@ -0,0 +1,649 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "fmt"
+ "reflect"
+ "testing"
+
+ "github.com/minio/pkg/v3/ellipses"
+)
+
+// Tests create endpoints with ellipses and without.
+func TestCreateServerEndpoints(t *testing.T) {
+ testCases := []struct {
+ serverAddr string
+ args []string
+ success bool
+ }{
+ // Invalid input.
+ {"", []string{}, false},
+ // Range cannot be negative.
+ {":9000", []string{"/export1{-1...1}"}, false},
+ // Range cannot start bigger than end.
+ {":9000", []string{"/export1{64...1}"}, false},
+ // Range can only be numeric.
+ {":9000", []string{"/export1{a...z}"}, false},
+ // Duplicate disks not allowed.
+ {":9000", []string{"/export1{1...32}", "/export1{1...32}"}, false},
+ // Same host cannot export same disk on two ports - special case localhost.
+ {":9001", []string{"http://localhost:900{1...2}/export{1...64}"}, false},
+ // Valid inputs.
+ {":9000", []string{"/export1"}, true},
+ {":9000", []string{"/export1", "/export2", "/export3", "/export4"}, true},
+ {":9000", []string{"/export1{1...64}"}, true},
+ {":9000", []string{"/export1{01...64}"}, true},
+ {":9000", []string{"/export1{1...32}", "/export1{33...64}"}, true},
+ {":9001", []string{"http://localhost:9001/export{1...64}"}, true},
+ {":9001", []string{"http://localhost:9001/export{01...64}"}, true},
+ }
+
+ for i, testCase := range testCases {
+ t.Run("", func(t *testing.T) {
+ srvCtxt := serverCtxt{}
+ err := mergeDisksLayoutFromArgs(testCase.args, &srvCtxt)
+ if err != nil && testCase.success {
+ t.Fatalf("Test %d: unexpected error: %v", i+1, err)
+ }
+ _, _, err = createServerEndpoints(testCase.serverAddr, srvCtxt.Layout.pools, srvCtxt.Layout.legacy)
+ if err != nil && testCase.success {
+ t.Errorf("Test %d: Expected success but failed instead %s", i+1, err)
+ }
+ if err == nil && !testCase.success {
+ t.Errorf("Test %d: Expected failure but passed instead", i+1)
+ }
+ })
+ }
+}
+
+func TestGetDivisibleSize(t *testing.T) {
+ testCases := []struct {
+ totalSizes []uint64
+ result uint64
+ }{
+ {[]uint64{24, 32, 16}, 8},
+ {[]uint64{32, 8, 4}, 4},
+ {[]uint64{8, 8, 8}, 8},
+ {[]uint64{24}, 24},
+ }
+
+ for _, testCase := range testCases {
+ t.Run("", func(t *testing.T) {
+ gotGCD := getDivisibleSize(testCase.totalSizes)
+ if testCase.result != gotGCD {
+ t.Errorf("Expected %v, got %v", testCase.result, gotGCD)
+ }
+ })
+ }
+}
+
+// Test tests calculating set indexes with ENV override for drive count.
+func TestGetSetIndexesEnvOverride(t *testing.T) {
+ testCases := []struct {
+ args []string
+ totalSizes []uint64
+ indexes [][]uint64
+ envOverride uint64
+ success bool
+ }{
+ {
+ []string{"data{1...64}"},
+ []uint64{64},
+ [][]uint64{{8, 8, 8, 8, 8, 8, 8, 8}},
+ 8,
+ true,
+ },
+ {
+ []string{"http://host{1...2}/data{1...180}"},
+ []uint64{360},
+ [][]uint64{{15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15}},
+ 15,
+ true,
+ },
+ {
+ []string{"http://host{1...12}/data{1...12}"},
+ []uint64{144},
+ [][]uint64{{12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12}},
+ 12,
+ true,
+ },
+ {
+ []string{"http://host{0...5}/data{1...28}"},
+ []uint64{168},
+ [][]uint64{{12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12}},
+ 12,
+ true,
+ },
+ // Incorrect custom set drive count.
+ {
+ []string{"http://host{0...5}/data{1...28}"},
+ []uint64{168},
+ nil,
+ 10,
+ false,
+ },
+ // Failure not divisible number of disks.
+ {
+ []string{"http://host{1...11}/data{1...11}"},
+ []uint64{121},
+ [][]uint64{{11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11}},
+ 11,
+ true,
+ },
+ {
+ []string{"data{1...60}"},
+ nil,
+ nil,
+ 8,
+ false,
+ },
+ {
+ []string{"data{1...64}"},
+ nil,
+ nil,
+ 64,
+ false,
+ },
+ {
+ []string{"data{1...64}"},
+ nil,
+ nil,
+ 2,
+ false,
+ },
+ }
+
+ for _, testCase := range testCases {
+ t.Run("", func(t *testing.T) {
+ argPatterns := make([]ellipses.ArgPattern, len(testCase.args))
+ for i, arg := range testCase.args {
+ patterns, err := ellipses.FindEllipsesPatterns(arg)
+ if err != nil {
+ t.Fatalf("Unexpected failure %s", err)
+ }
+ argPatterns[i] = patterns
+ }
+
+ gotIndexes, err := getSetIndexes(testCase.args, testCase.totalSizes, testCase.envOverride, argPatterns)
+ if err != nil && testCase.success {
+ t.Errorf("Expected success but failed instead %s", err)
+ }
+ if err == nil && !testCase.success {
+ t.Errorf("Expected failure but passed instead")
+ }
+ if !reflect.DeepEqual(testCase.indexes, gotIndexes) {
+ t.Errorf("Expected %v, got %v", testCase.indexes, gotIndexes)
+ }
+ })
+ }
+}
+
+// Test tests calculating set indexes.
+func TestGetSetIndexes(t *testing.T) {
+ testCases := []struct {
+ args []string
+ totalSizes []uint64
+ indexes [][]uint64
+ success bool
+ }{
+ // Invalid inputs.
+ {
+ []string{"data{1...17}/export{1...52}"},
+ []uint64{14144},
+ nil,
+ false,
+ },
+ // Valid inputs.
+ {
+ []string{"data{1...3}"},
+ []uint64{3},
+ [][]uint64{{3}},
+ true,
+ },
+ {
+ []string{"data/controller1/export{1...2}, data/controller2/export{1...4}, data/controller3/export{1...8}"},
+ []uint64{2, 4, 8},
+ [][]uint64{{2}, {2, 2}, {2, 2, 2, 2}},
+ true,
+ },
+ {
+ []string{"data{1...27}"},
+ []uint64{27},
+ [][]uint64{{9, 9, 9}},
+ true,
+ },
+ {
+ []string{"http://host{1...3}/data{1...180}"},
+ []uint64{540},
+ [][]uint64{{15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15}},
+ true,
+ },
+ {
+ []string{"http://host{1...2}.rack{1...4}/data{1...180}"},
+ []uint64{1440},
+ [][]uint64{{16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16}},
+ true,
+ },
+ {
+ []string{"http://host{1...2}/data{1...180}"},
+ []uint64{360},
+ [][]uint64{{12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12}},
+ true,
+ },
+ {
+ []string{"data/controller1/export{1...4}, data/controller2/export{1...8}, data/controller3/export{1...12}"},
+ []uint64{4, 8, 12},
+ [][]uint64{{4}, {4, 4}, {4, 4, 4}},
+ true,
+ },
+ {
+ []string{"data{1...64}"},
+ []uint64{64},
+ [][]uint64{{16, 16, 16, 16}},
+ true,
+ },
+ {
+ []string{"data{1...24}"},
+ []uint64{24},
+ [][]uint64{{12, 12}},
+ true,
+ },
+ {
+ []string{"data/controller{1...11}/export{1...8}"},
+ []uint64{88},
+ [][]uint64{{11, 11, 11, 11, 11, 11, 11, 11}},
+ true,
+ },
+ {
+ []string{"data{1...4}"},
+ []uint64{4},
+ [][]uint64{{4}},
+ true,
+ },
+ {
+ []string{"data/controller1/export{1...10}, data/controller2/export{1...10}, data/controller3/export{1...10}"},
+ []uint64{10, 10, 10},
+ [][]uint64{{10}, {10}, {10}},
+ true,
+ },
+ {
+ []string{"data{1...16}/export{1...52}"},
+ []uint64{832},
+ [][]uint64{{16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16}},
+ true,
+ },
+ }
+
+ for _, testCase := range testCases {
+ t.Run("", func(t *testing.T) {
+ argPatterns := make([]ellipses.ArgPattern, len(testCase.args))
+ for i, arg := range testCase.args {
+ patterns, err := ellipses.FindEllipsesPatterns(arg)
+ if err != nil {
+ t.Fatalf("Unexpected failure %s", err)
+ }
+ argPatterns[i] = patterns
+ }
+ gotIndexes, err := getSetIndexes(testCase.args, testCase.totalSizes, 0, argPatterns)
+ if err != nil && testCase.success {
+ t.Errorf("Expected success but failed instead %s", err)
+ }
+ if err == nil && !testCase.success {
+ t.Errorf("Expected failure but passed instead")
+ }
+ if !reflect.DeepEqual(testCase.indexes, gotIndexes) {
+ t.Errorf("Expected %v, got %v", testCase.indexes, gotIndexes)
+ }
+ })
+ }
+}
+
+func getHexSequences(start int, number int, paddinglen int) (seq []string) {
+ for i := start; i <= number; i++ {
+ if paddinglen == 0 {
+ seq = append(seq, fmt.Sprintf("%x", i))
+ } else {
+ seq = append(seq, fmt.Sprintf(fmt.Sprintf("%%0%dx", paddinglen), i))
+ }
+ }
+ return seq
+}
+
+func getSequences(start int, number int, paddinglen int) (seq []string) {
+ for i := start; i <= number; i++ {
+ if paddinglen == 0 {
+ seq = append(seq, fmt.Sprintf("%d", i))
+ } else {
+ seq = append(seq, fmt.Sprintf(fmt.Sprintf("%%0%dd", paddinglen), i))
+ }
+ }
+ return seq
+}
+
+// Test tests parses endpoint ellipses input pattern.
+func TestParseEndpointSet(t *testing.T) {
+ testCases := []struct {
+ arg string
+ es endpointSet
+ success bool
+ }{
+ // Tests invalid inputs.
+ {
+ "...",
+ endpointSet{},
+ false,
+ },
+ // No range specified.
+ {
+ "{...}",
+ endpointSet{},
+ false,
+ },
+ // Invalid range.
+ {
+ "http://minio{2...3}/export/set{1...0}",
+ endpointSet{},
+ false,
+ },
+ // Range cannot be smaller than 4 minimum.
+ {
+ "/export{1..2}",
+ endpointSet{},
+ false,
+ },
+ // Unsupported characters.
+ {
+ "/export/test{1...2O}",
+ endpointSet{},
+ false,
+ },
+ // Tests valid inputs.
+ {
+ "{1...27}",
+ endpointSet{
+ []ellipses.ArgPattern{
+ []ellipses.Pattern{
+ {
+ Prefix: "",
+ Suffix: "",
+ Seq: getSequences(1, 27, 0),
+ },
+ },
+ },
+ nil,
+ [][]uint64{{9, 9, 9}},
+ },
+ true,
+ },
+ {
+ "/export/set{1...64}",
+ endpointSet{
+ []ellipses.ArgPattern{
+ []ellipses.Pattern{
+ {
+ Prefix: "/export/set",
+ Suffix: "",
+ Seq: getSequences(1, 64, 0),
+ },
+ },
+ },
+ nil,
+ [][]uint64{{16, 16, 16, 16}},
+ },
+ true,
+ },
+ // Valid input for distributed setup.
+ {
+ "http://minio{2...3}/export/set{1...64}",
+ endpointSet{
+ []ellipses.ArgPattern{
+ []ellipses.Pattern{
+ {
+ Prefix: "",
+ Suffix: "",
+ Seq: getSequences(1, 64, 0),
+ },
+ {
+ Prefix: "http://minio",
+ Suffix: "/export/set",
+ Seq: getSequences(2, 3, 0),
+ },
+ },
+ },
+ nil,
+ [][]uint64{{16, 16, 16, 16, 16, 16, 16, 16}},
+ },
+ true,
+ },
+ // Supporting some advanced cases.
+ {
+ "http://minio{1...64}.mydomain.net/data",
+ endpointSet{
+ []ellipses.ArgPattern{
+ []ellipses.Pattern{
+ {
+ Prefix: "http://minio",
+ Suffix: ".mydomain.net/data",
+ Seq: getSequences(1, 64, 0),
+ },
+ },
+ },
+ nil,
+ [][]uint64{{16, 16, 16, 16}},
+ },
+ true,
+ },
+ {
+ "http://rack{1...4}.mydomain.minio{1...16}/data",
+ endpointSet{
+ []ellipses.ArgPattern{
+ []ellipses.Pattern{
+ {
+ Prefix: "",
+ Suffix: "/data",
+ Seq: getSequences(1, 16, 0),
+ },
+ {
+ Prefix: "http://rack",
+ Suffix: ".mydomain.minio",
+ Seq: getSequences(1, 4, 0),
+ },
+ },
+ },
+ nil,
+ [][]uint64{{16, 16, 16, 16}},
+ },
+ true,
+ },
+ // Supporting kubernetes cases.
+ {
+ "http://minio{0...15}.mydomain.net/data{0...1}",
+ endpointSet{
+ []ellipses.ArgPattern{
+ []ellipses.Pattern{
+ {
+ Prefix: "",
+ Suffix: "",
+ Seq: getSequences(0, 1, 0),
+ },
+ {
+ Prefix: "http://minio",
+ Suffix: ".mydomain.net/data",
+ Seq: getSequences(0, 15, 0),
+ },
+ },
+ },
+ nil,
+ [][]uint64{{16, 16}},
+ },
+ true,
+ },
+ // No host regex, just disks.
+ {
+ "http://server1/data{1...32}",
+ endpointSet{
+ []ellipses.ArgPattern{
+ []ellipses.Pattern{
+ {
+ Prefix: "http://server1/data",
+ Suffix: "",
+ Seq: getSequences(1, 32, 0),
+ },
+ },
+ },
+ nil,
+ [][]uint64{{16, 16}},
+ },
+ true,
+ },
+ // No host regex, just disks with two position numerics.
+ {
+ "http://server1/data{01...32}",
+ endpointSet{
+ []ellipses.ArgPattern{
+ []ellipses.Pattern{
+ {
+ Prefix: "http://server1/data",
+ Suffix: "",
+ Seq: getSequences(1, 32, 2),
+ },
+ },
+ },
+ nil,
+ [][]uint64{{16, 16}},
+ },
+ true,
+ },
+ // More than 2 ellipses are supported as well.
+ {
+ "http://minio{2...3}/export/set{1...64}/test{1...2}",
+ endpointSet{
+ []ellipses.ArgPattern{
+ []ellipses.Pattern{
+ {
+ Prefix: "",
+ Suffix: "",
+ Seq: getSequences(1, 2, 0),
+ },
+ {
+ Prefix: "",
+ Suffix: "/test",
+ Seq: getSequences(1, 64, 0),
+ },
+ {
+ Prefix: "http://minio",
+ Suffix: "/export/set",
+ Seq: getSequences(2, 3, 0),
+ },
+ },
+ },
+ nil,
+ [][]uint64{{
+ 16, 16, 16, 16, 16, 16, 16, 16,
+ 16, 16, 16, 16, 16, 16, 16, 16,
+ }},
+ },
+ true,
+ },
+ // More than 1 ellipses per argument for standalone setup.
+ {
+ "/export{1...10}/disk{1...10}",
+ endpointSet{
+ []ellipses.ArgPattern{
+ []ellipses.Pattern{
+ {
+ Prefix: "",
+ Suffix: "",
+ Seq: getSequences(1, 10, 0),
+ },
+ {
+ Prefix: "/export",
+ Suffix: "/disk",
+ Seq: getSequences(1, 10, 0),
+ },
+ },
+ },
+ nil,
+ [][]uint64{{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}},
+ },
+ true,
+ },
+ // IPv6 ellipses with hexadecimal expansion
+ {
+ "http://[2001:3984:3989::{1...a}]/disk{1...10}",
+ endpointSet{
+ []ellipses.ArgPattern{
+ []ellipses.Pattern{
+ {
+ Prefix: "",
+ Suffix: "",
+ Seq: getSequences(1, 10, 0),
+ },
+ {
+ Prefix: "http://[2001:3984:3989::",
+ Suffix: "]/disk",
+ Seq: getHexSequences(1, 10, 0),
+ },
+ },
+ },
+ nil,
+ [][]uint64{{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}},
+ },
+ true,
+ },
+ // IPv6 ellipses with hexadecimal expansion with 3 position numerics.
+ {
+ "http://[2001:3984:3989::{001...00a}]/disk{1...10}",
+ endpointSet{
+ []ellipses.ArgPattern{
+ []ellipses.Pattern{
+ {
+ Prefix: "",
+ Suffix: "",
+ Seq: getSequences(1, 10, 0),
+ },
+ {
+ Prefix: "http://[2001:3984:3989::",
+ Suffix: "]/disk",
+ Seq: getHexSequences(1, 10, 3),
+ },
+ },
+ },
+ nil,
+ [][]uint64{{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}},
+ },
+ true,
+ },
+ }
+
+ for _, testCase := range testCases {
+ t.Run("", func(t *testing.T) {
+ gotEs, err := parseEndpointSet(0, testCase.arg)
+ if err != nil && testCase.success {
+ t.Errorf("Expected success but failed instead %s", err)
+ }
+ if err == nil && !testCase.success {
+ t.Errorf("Expected failure but passed instead")
+ }
+ if !reflect.DeepEqual(testCase.es, gotEs) {
+ t.Errorf("Expected %v, got %v", testCase.es, gotEs)
+ }
+ })
+ }
+}
diff --git a/cmd/endpoint.go b/cmd/endpoint.go
new file mode 100644
index 00000000..dcfe2e61
--- /dev/null
+++ b/cmd/endpoint.go
@@ -0,0 +1,1262 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "fmt"
+ "net"
+ "net/http"
+ "net/url"
+ "path"
+ "path/filepath"
+ "reflect"
+ "runtime"
+ "slices"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/dustin/go-humanize"
+ "github.com/minio/minio-go/v7/pkg/set"
+ "github.com/minio/minio/internal/config"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/minio/internal/mountinfo"
+ "github.com/minio/pkg/v3/env"
+ xnet "github.com/minio/pkg/v3/net"
+)
+
+// EndpointType - enum for endpoint type.
+type EndpointType int
+
+const (
+ // PathEndpointType - path style endpoint type enum.
+ PathEndpointType EndpointType = iota + 1
+
+ // URLEndpointType - URL style endpoint type enum.
+ URLEndpointType
+)
+
+// ProxyEndpoint - endpoint used for proxy redirects
+// See proxyRequest() for details.
+type ProxyEndpoint struct {
+ Endpoint
+ Transport http.RoundTripper
+}
+
+// Node holds information about a node in this cluster
+type Node struct {
+ *url.URL
+ Pools []int
+ IsLocal bool
+ GridHost string
+}
+
+// Endpoint - any type of endpoint.
+type Endpoint struct {
+ *url.URL
+ IsLocal bool
+
+ PoolIdx, SetIdx, DiskIdx int
+}
+
+// Equal returns true if endpoint == ep
+func (endpoint Endpoint) Equal(ep Endpoint) bool {
+ if endpoint.IsLocal == ep.IsLocal && endpoint.PoolIdx == ep.PoolIdx && endpoint.SetIdx == ep.SetIdx && endpoint.DiskIdx == ep.DiskIdx {
+ if endpoint.Path == ep.Path && endpoint.Host == ep.Host {
+ return true
+ }
+ }
+ return false
+}
+
+func (endpoint Endpoint) String() string {
+ if endpoint.Host == "" {
+ return endpoint.Path
+ }
+
+ return endpoint.URL.String()
+}
+
+// Type - returns type of endpoint.
+func (endpoint Endpoint) Type() EndpointType {
+ if endpoint.Host == "" {
+ return PathEndpointType
+ }
+
+ return URLEndpointType
+}
+
+// HTTPS - returns true if secure for URLEndpointType.
+func (endpoint Endpoint) HTTPS() bool {
+ return endpoint.Scheme == "https"
+}
+
+// GridHost returns the host to be used for grid connections.
+func (endpoint Endpoint) GridHost() string {
+ return fmt.Sprintf("%s://%s", endpoint.Scheme, endpoint.Host)
+}
+
+// UpdateIsLocal - resolves the host and updates if it is local or not.
+func (endpoint *Endpoint) UpdateIsLocal() (err error) {
+ if endpoint.Host != "" {
+ endpoint.IsLocal, err = isLocalHost(endpoint.Hostname(), endpoint.Port(), globalMinioPort)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// SetPoolIndex sets a specific pool number to this node
+func (endpoint *Endpoint) SetPoolIndex(i int) {
+ endpoint.PoolIdx = i
+}
+
+// SetSetIndex sets a specific set number to this node
+func (endpoint *Endpoint) SetSetIndex(i int) {
+ endpoint.SetIdx = i
+}
+
+// SetDiskIndex sets a specific disk number to this node
+func (endpoint *Endpoint) SetDiskIndex(i int) {
+ 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.
+func NewEndpoint(arg string) (ep Endpoint, e error) {
+ // isEmptyPath - check whether given path is not empty.
+ isEmptyPath := func(path string) bool {
+ return path == "" || path == SlashSeparator || path == `\`
+ }
+
+ if isEmptyPath(arg) {
+ return ep, fmt.Errorf("empty or root endpoint is not supported")
+ }
+
+ var isLocal bool
+ var host string
+ u, err := url.Parse(arg)
+ if err == nil && u.Host != "" {
+ // 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.
+ if !isValidURLEndpoint(u) {
+ return ep, fmt.Errorf("invalid URL endpoint format")
+ }
+
+ var port string
+ host, port, err = net.SplitHostPort(u.Host)
+ if err != nil {
+ if !strings.Contains(err.Error(), "missing port in address") {
+ return ep, fmt.Errorf("invalid URL endpoint format: %w", err)
+ }
+
+ host = u.Host
+ } else {
+ var p int
+ p, err = strconv.Atoi(port)
+ if err != nil {
+ return ep, fmt.Errorf("invalid URL endpoint format: invalid port number")
+ } else if p < 1 || p > 65535 {
+ return ep, fmt.Errorf("invalid URL endpoint format: port number must be between 1 to 65535")
+ }
+ }
+ if i := strings.Index(host, "%"); i > -1 {
+ host = host[:i]
+ }
+
+ if host == "" {
+ return ep, fmt.Errorf("invalid URL endpoint format: empty host name")
+ }
+
+ // As this is path in the URL, we should use path package, not filepath package.
+ // On MS Windows, filepath.Clean() converts into Windows path style ie `/foo` becomes `\foo`
+ u.Path = path.Clean(u.Path)
+ if isEmptyPath(u.Path) {
+ return ep, fmt.Errorf("empty or root path is not supported in URL endpoint")
+ }
+
+ // On windows having a preceding SlashSeparator will cause problems, if the
+ // command line already has C:/= len(endpoints) {
+ return ""
+ }
+ return endpoints[i].String()
+}
+
+// GetAllStrings - returns allstring of all endpoints
+func (endpoints Endpoints) GetAllStrings() (all []string) {
+ for _, e := range endpoints {
+ all = append(all, e.String())
+ }
+ return all
+}
+
+func hostResolveToLocalhost(endpoint Endpoint) bool {
+ hostIPs, err := getHostIP(endpoint.Hostname())
+ if err != nil {
+ return false
+ }
+ var loopback int
+ for _, hostIP := range hostIPs.ToSlice() {
+ if net.ParseIP(hostIP).IsLoopback() {
+ loopback++
+ }
+ }
+ return loopback == len(hostIPs)
+}
+
+// UpdateIsLocal - resolves the host and discovers the local host.
+func (endpoints Endpoints) UpdateIsLocal() error {
+ var epsResolved int
+ var foundLocal bool
+ resolvedList := make([]bool, len(endpoints))
+ // Mark the starting time
+ startTime := time.Now()
+ keepAliveTicker := time.NewTicker(500 * time.Millisecond)
+ defer keepAliveTicker.Stop()
+ for !foundLocal && (epsResolved != len(endpoints)) {
+ // Break if the local endpoint is found already Or all the endpoints are resolved.
+
+ // Retry infinitely on Kubernetes and Docker swarm.
+ // This is needed as the remote hosts are sometime
+ // not available immediately.
+ select {
+ case <-globalOSSignalCh:
+ return fmt.Errorf("The endpoint resolution got interrupted")
+ default:
+ for i, resolved := range resolvedList {
+ if resolved {
+ // Continue if host is already resolved.
+ continue
+ }
+
+ if endpoints[i].Host == "" {
+ resolvedList[i] = true
+ endpoints[i].IsLocal = true
+ epsResolved++
+ if !foundLocal {
+ foundLocal = true
+ }
+ continue
+ }
+
+ // Log the message to console about the host resolving
+ reqInfo := (&logger.ReqInfo{}).AppendTags(
+ "host",
+ endpoints[i].Hostname(),
+ )
+
+ if orchestrated && hostResolveToLocalhost(endpoints[i]) {
+ // time elapsed
+ timeElapsed := time.Since(startTime)
+ // log error only if more than a second has elapsed
+ if timeElapsed > time.Second {
+ reqInfo.AppendTags("elapsedTime",
+ humanize.RelTime(startTime,
+ startTime.Add(timeElapsed),
+ "elapsed",
+ "",
+ ))
+ ctx := logger.SetReqInfo(GlobalContext,
+ reqInfo)
+ bootLogOnceIf(ctx, fmt.Errorf("%s resolves to localhost in a containerized deployment, waiting for it to resolve to a valid IP",
+ endpoints[i].Hostname()), endpoints[i].Hostname(), logger.ErrorKind)
+ }
+
+ continue
+ }
+
+ // return err if not Docker or Kubernetes
+ // We use IsDocker() to check for Docker environment
+ // We use IsKubernetes() to check for Kubernetes environment
+ isLocal, err := isLocalHost(endpoints[i].Hostname(),
+ endpoints[i].Port(),
+ globalMinioPort,
+ )
+ if err != nil && !orchestrated {
+ return err
+ }
+ if err != nil {
+ // time elapsed
+ timeElapsed := time.Since(startTime)
+ // log error only if more than a second has elapsed
+ if timeElapsed > time.Second {
+ reqInfo.AppendTags("elapsedTime",
+ humanize.RelTime(startTime,
+ startTime.Add(timeElapsed),
+ "elapsed",
+ "",
+ ))
+ ctx := logger.SetReqInfo(GlobalContext,
+ reqInfo)
+ bootLogOnceIf(ctx, err, endpoints[i].Hostname(), logger.ErrorKind)
+ }
+ } else {
+ resolvedList[i] = true
+ endpoints[i].IsLocal = isLocal
+ epsResolved++
+ if !foundLocal {
+ foundLocal = isLocal
+ }
+ }
+ }
+
+ // Wait for the tick, if the there exist a local endpoint in discovery.
+ // Non docker/kubernetes environment we do not need to wait.
+ if !foundLocal && orchestrated {
+ <-keepAliveTicker.C
+ }
+ }
+ }
+
+ // On Kubernetes/Docker setups DNS resolves inappropriately sometimes
+ // where there are situations same endpoints with multiple disks
+ // come online indicating either one of them is local and some
+ // of them are not local. This situation can never happen and
+ // its only a possibility in orchestrated deployments with dynamic
+ // DNS. Following code ensures that we treat if one of the endpoint
+ // says its local for a given host - it is true for all endpoints
+ // for the same host. Following code ensures that this assumption
+ // is true and it works in all scenarios and it is safe to assume
+ // for a given host.
+ endpointLocalMap := make(map[string]bool)
+ for _, ep := range endpoints {
+ if ep.IsLocal {
+ endpointLocalMap[ep.Host] = ep.IsLocal
+ }
+ }
+ for i := range endpoints {
+ endpoints[i].IsLocal = endpointLocalMap[endpoints[i].Host]
+ }
+ return nil
+}
+
+// NewEndpoints - returns new endpoint list based on input args.
+func NewEndpoints(args ...string) (endpoints Endpoints, err error) {
+ var endpointType EndpointType
+ var scheme string
+
+ uniqueArgs := set.NewStringSet()
+ // Loop through args and adds to endpoint list.
+ for i, arg := range args {
+ endpoint, err := NewEndpoint(arg)
+ if err != nil {
+ return nil, fmt.Errorf("'%s': %s", arg, err.Error())
+ }
+
+ // All endpoints have to be same type and scheme if applicable.
+ //nolint:gocritic
+ if i == 0 {
+ endpointType = endpoint.Type()
+ scheme = endpoint.Scheme
+ } else if endpoint.Type() != endpointType {
+ return nil, fmt.Errorf("mixed style endpoints are not supported")
+ } else if endpoint.Scheme != scheme {
+ return nil, fmt.Errorf("mixed scheme is not supported")
+ }
+
+ arg = endpoint.String()
+ if uniqueArgs.Contains(arg) {
+ return nil, fmt.Errorf("duplicate endpoints found")
+ }
+ uniqueArgs.Add(arg)
+ endpoints = append(endpoints, endpoint)
+ }
+
+ return endpoints, nil
+}
+
+// Checks if there are any cross device mounts.
+func checkCrossDeviceMounts(endpoints Endpoints) (err error) {
+ var absPaths []string
+ for _, endpoint := range endpoints {
+ if endpoint.IsLocal {
+ var absPath string
+ absPath, err = filepath.Abs(endpoint.Path)
+ if err != nil {
+ return err
+ }
+ absPaths = append(absPaths, absPath)
+ }
+ }
+ return mountinfo.CheckCrossDevice(absPaths)
+}
+
+// PoolEndpointList is a temporary type to holds the list of endpoints
+type PoolEndpointList []Endpoints
+
+// UpdateIsLocal - resolves all hosts and discovers which are local
+func (p PoolEndpointList) UpdateIsLocal() error {
+ var epsResolved int
+ var epCount int
+
+ for _, endpoints := range p {
+ epCount += len(endpoints)
+ }
+
+ var foundLocal bool
+ resolvedList := make(map[Endpoint]bool)
+
+ // Mark the starting time
+ startTime := time.Now()
+ keepAliveTicker := time.NewTicker(1 * time.Second)
+ defer keepAliveTicker.Stop()
+ for !foundLocal && (epsResolved != epCount) {
+ // Break if the local endpoint is found already Or all the endpoints are resolved.
+
+ // Retry infinitely on Kubernetes and Docker swarm.
+ // This is needed as the remote hosts are sometime
+ // not available immediately.
+ select {
+ case <-globalOSSignalCh:
+ return fmt.Errorf("The endpoint resolution got interrupted")
+ default:
+ for i, endpoints := range p {
+ for j, endpoint := range endpoints {
+ if resolvedList[endpoint] {
+ // Continue if host is already resolved.
+ continue
+ }
+
+ if endpoint.Host == "" || (orchestrated && env.Get("_MINIO_SERVER_LOCAL", "") == endpoint.Host) {
+ if !foundLocal {
+ foundLocal = true
+ }
+ endpoint.IsLocal = true
+ endpoints[j] = endpoint
+ epsResolved++
+ resolvedList[endpoint] = true
+ continue
+ }
+
+ // Log the message to console about the host resolving
+ reqInfo := (&logger.ReqInfo{}).AppendTags(
+ "host",
+ endpoint.Hostname(),
+ )
+
+ if orchestrated && hostResolveToLocalhost(endpoint) {
+ // time elapsed
+ timeElapsed := time.Since(startTime)
+ // log error only if more than a second has elapsed
+ if timeElapsed > time.Second {
+ reqInfo.AppendTags("elapsedTime",
+ humanize.RelTime(startTime,
+ startTime.Add(timeElapsed),
+ "elapsed",
+ "",
+ ))
+ ctx := logger.SetReqInfo(GlobalContext,
+ reqInfo)
+ bootLogOnceIf(ctx, fmt.Errorf("%s resolves to localhost in a containerized deployment, waiting for it to resolve to a valid IP",
+ endpoint.Hostname()), endpoint.Hostname(), logger.ErrorKind)
+ }
+ continue
+ }
+
+ // return err if not Docker or Kubernetes
+ // We use IsDocker() to check for Docker environment
+ // We use IsKubernetes() to check for Kubernetes environment
+ isLocal, err := isLocalHost(endpoint.Hostname(),
+ endpoint.Port(),
+ globalMinioPort,
+ )
+ if err != nil && !orchestrated {
+ return err
+ }
+ if err != nil {
+ // time elapsed
+ timeElapsed := time.Since(startTime)
+ // log error only if more than a second has elapsed
+ if timeElapsed > time.Second {
+ reqInfo.AppendTags("elapsedTime",
+ humanize.RelTime(startTime,
+ startTime.Add(timeElapsed),
+ "elapsed",
+ "",
+ ))
+ ctx := logger.SetReqInfo(GlobalContext,
+ reqInfo)
+ bootLogOnceIf(ctx, fmt.Errorf("Unable to resolve DNS for %s: %w", endpoint, err), endpoint.Hostname(), logger.ErrorKind)
+ }
+ } else {
+ resolvedList[endpoint] = true
+ endpoint.IsLocal = isLocal
+ epsResolved++
+ if !foundLocal {
+ foundLocal = isLocal
+ }
+ endpoints[j] = endpoint
+ }
+ }
+
+ p[i] = endpoints
+
+ // Wait for the tick, if the there exist a local endpoint in discovery.
+ // Non docker/kubernetes environment we do not need to wait.
+ if !foundLocal && orchestrated {
+ <-keepAliveTicker.C
+ }
+ }
+ }
+ }
+
+ // On Kubernetes/Docker setups DNS resolves inappropriately sometimes
+ // where there are situations same endpoints with multiple disks
+ // come online indicating either one of them is local and some
+ // of them are not local. This situation can never happen and
+ // its only a possibility in orchestrated deployments with dynamic
+ // DNS. Following code ensures that we treat if one of the endpoint
+ // says its local for a given host - it is true for all endpoints
+ // for the same host. Following code ensures that this assumption
+ // is true and it works in all scenarios and it is safe to assume
+ // for a given host.
+ for i, endpoints := range p {
+ endpointLocalMap := make(map[string]bool)
+ for _, ep := range endpoints {
+ if ep.IsLocal {
+ endpointLocalMap[ep.Host] = ep.IsLocal
+ }
+ }
+ for i := range endpoints {
+ endpoints[i].IsLocal = endpointLocalMap[endpoints[i].Host]
+ }
+ p[i] = endpoints
+ }
+
+ return nil
+}
+
+func isEmptyLayout(poolsLayout ...poolDisksLayout) bool {
+ return len(poolsLayout) == 0 || len(poolsLayout[0].layout) == 0 || len(poolsLayout[0].layout[0]) == 0 || len(poolsLayout[0].layout[0][0]) == 0
+}
+
+func isSingleDriveLayout(poolsLayout ...poolDisksLayout) bool {
+ return len(poolsLayout) == 1 && len(poolsLayout[0].layout) == 1 && len(poolsLayout[0].layout[0]) == 1
+}
+
+// CreatePoolEndpoints creates a list of endpoints per pool, resolves their relevant hostnames and
+// discovers those are local or remote.
+func CreatePoolEndpoints(serverAddr string, poolsLayout ...poolDisksLayout) ([]Endpoints, SetupType, error) {
+ var setupType SetupType
+
+ if isEmptyLayout(poolsLayout...) {
+ return nil, setupType, config.ErrInvalidErasureEndpoints(nil).Msg("invalid number of endpoints")
+ }
+
+ // Check whether serverAddr is valid for this host.
+ if err := CheckLocalServerAddr(serverAddr); err != nil {
+ return nil, setupType, err
+ }
+
+ _, serverAddrPort := mustSplitHostPort(serverAddr)
+
+ poolEndpoints := make(PoolEndpointList, len(poolsLayout))
+
+ // For single arg, return single drive EC setup.
+ if isSingleDriveLayout(poolsLayout...) {
+ endpoint, err := NewEndpoint(poolsLayout[0].layout[0][0])
+ if err != nil {
+ return nil, setupType, err
+ }
+ if err := endpoint.UpdateIsLocal(); err != nil {
+ return nil, setupType, err
+ }
+ if endpoint.Type() != PathEndpointType {
+ return nil, setupType, config.ErrInvalidEndpoint(nil).Msg("use path style endpoint for single node setup")
+ }
+
+ endpoint.SetPoolIndex(0)
+ endpoint.SetSetIndex(0)
+ endpoint.SetDiskIndex(0)
+
+ var endpoints Endpoints
+ endpoints = append(endpoints, endpoint)
+ setupType = ErasureSDSetupType
+
+ poolEndpoints[0] = endpoints
+ // Check for cross device mounts if any.
+ if err = checkCrossDeviceMounts(endpoints); err != nil {
+ return nil, setupType, config.ErrInvalidEndpoint(nil).Msg(err.Error())
+ }
+
+ return poolEndpoints, setupType, nil
+ }
+
+ uniqueArgs := set.NewStringSet()
+
+ for poolIdx, pool := range poolsLayout {
+ var endpoints Endpoints
+ for setIdx, setLayout := range pool.layout {
+ // Convert args to endpoints
+ eps, err := NewEndpoints(setLayout...)
+ if err != nil {
+ return nil, setupType, config.ErrInvalidErasureEndpoints(nil).Msg(err.Error())
+ }
+
+ // Check for cross device mounts if any.
+ if err = checkCrossDeviceMounts(eps); err != nil {
+ return nil, setupType, config.ErrInvalidErasureEndpoints(nil).Msg(err.Error())
+ }
+
+ for diskIdx := range eps {
+ eps[diskIdx].SetPoolIndex(poolIdx)
+ eps[diskIdx].SetSetIndex(setIdx)
+ eps[diskIdx].SetDiskIndex(diskIdx)
+ }
+
+ endpoints = append(endpoints, eps...)
+ }
+
+ if len(endpoints) == 0 {
+ return nil, setupType, config.ErrInvalidErasureEndpoints(nil).Msg("invalid number of endpoints")
+ }
+
+ poolEndpoints[poolIdx] = endpoints
+ }
+
+ if err := poolEndpoints.UpdateIsLocal(); err != nil {
+ return nil, setupType, config.ErrInvalidErasureEndpoints(nil).Msg(err.Error())
+ }
+
+ for i, endpoints := range poolEndpoints {
+ // Here all endpoints are URL style.
+ endpointPathSet := set.NewStringSet()
+ localEndpointCount := 0
+ localServerHostSet := set.NewStringSet()
+ localPortSet := set.NewStringSet()
+
+ for _, endpoint := range endpoints {
+ endpointPathSet.Add(endpoint.Path)
+ if endpoint.IsLocal && endpoint.Host != "" {
+ localServerHostSet.Add(endpoint.Hostname())
+
+ _, port, err := net.SplitHostPort(endpoint.Host)
+ if err != nil {
+ port = serverAddrPort
+ }
+ localPortSet.Add(port)
+
+ localEndpointCount++
+ }
+ }
+
+ reverseProxy := (env.Get("_MINIO_REVERSE_PROXY", "") != "") && ((env.Get("MINIO_CI_CD", "") != "") || (env.Get("CI", "") != ""))
+ // If not orchestrated
+ // and not setup in reverse proxy
+ if !orchestrated && !reverseProxy {
+ // Check whether same path is not used in endpoints of a host on different port.
+ // Only verify this on baremetal setups, DNS is not available in orchestrated
+ // environments so we can't do much here.
+ pathIPMap := make(map[string]set.StringSet)
+ hostIPCache := make(map[string]set.StringSet)
+ for _, endpoint := range endpoints {
+ host := endpoint.Hostname()
+ var hostIPSet set.StringSet
+ if host != "" {
+ var ok bool
+ hostIPSet, ok = hostIPCache[host]
+ if !ok {
+ var err error
+ hostIPSet, err = getHostIP(host)
+ if err != nil {
+ return nil, setupType, config.ErrInvalidErasureEndpoints(nil).Msg(fmt.Sprintf("host '%s' cannot resolve: %s", host, err))
+ }
+ hostIPCache[host] = hostIPSet
+ }
+ }
+ if IPSet, ok := pathIPMap[endpoint.Path]; ok {
+ if !IPSet.Intersection(hostIPSet).IsEmpty() {
+ return nil, setupType,
+ config.ErrInvalidErasureEndpoints(nil).Msg(fmt.Sprintf("same path '%s' can not be served by different port on same address", endpoint.Path))
+ }
+ pathIPMap[endpoint.Path] = IPSet.Union(hostIPSet)
+ } else {
+ pathIPMap[endpoint.Path] = hostIPSet
+ }
+ }
+ }
+
+ // Check whether same path is used for more than 1 local endpoints.
+ {
+ localPathSet := set.CreateStringSet()
+ for _, endpoint := range endpoints {
+ if !endpoint.IsLocal {
+ continue
+ }
+ if localPathSet.Contains(endpoint.Path) {
+ return nil, setupType,
+ config.ErrInvalidErasureEndpoints(nil).Msg(fmt.Sprintf("path '%s' cannot be served by different address on same server", endpoint.Path))
+ }
+ localPathSet.Add(endpoint.Path)
+ }
+ }
+
+ // Add missing port in all endpoints.
+ for i := range endpoints {
+ if endpoints[i].Host != "" {
+ _, port, err := net.SplitHostPort(endpoints[i].Host)
+ if err != nil {
+ endpoints[i].Host = net.JoinHostPort(endpoints[i].Host, serverAddrPort)
+ } else if endpoints[i].IsLocal && serverAddrPort != port {
+ // If endpoint is local, but port is different than serverAddrPort, then make it as remote.
+ endpoints[i].IsLocal = false
+ }
+ }
+ }
+
+ // All endpoints are pointing to local host
+ if len(endpoints) == localEndpointCount {
+ // If all endpoints have same port number, Just treat it as local erasure setup
+ // using URL style endpoints.
+ if len(localPortSet) == 1 {
+ if len(localServerHostSet) > 1 {
+ return nil, setupType,
+ config.ErrInvalidErasureEndpoints(nil).Msg("all local endpoints should not have different hostnames/ips")
+ }
+ }
+
+ // Even though all endpoints are local, but those endpoints use different ports.
+ // This means it is DistErasure setup.
+ }
+
+ for _, endpoint := range endpoints {
+ if endpoint.Host != "" {
+ uniqueArgs.Add(endpoint.Host)
+ } else {
+ uniqueArgs.Add(net.JoinHostPort("localhost", serverAddrPort))
+ }
+ }
+
+ poolEndpoints[i] = endpoints
+ }
+
+ publicIPs := env.Get(config.EnvPublicIPs, "")
+ if len(publicIPs) == 0 {
+ updateDomainIPs(uniqueArgs)
+ }
+
+ erasureType := len(uniqueArgs.ToSlice()) == 1
+
+ for _, endpoints := range poolEndpoints {
+ // Return Erasure setup when all endpoints are path style.
+ if endpoints[0].Type() == PathEndpointType {
+ setupType = ErasureSetupType
+ break
+ }
+ if endpoints[0].Type() == URLEndpointType {
+ if erasureType {
+ setupType = ErasureSetupType
+ } else {
+ setupType = DistErasureSetupType
+ }
+ break
+ }
+ }
+
+ return poolEndpoints, setupType, nil
+}
+
+// GetLocalPeer - returns local peer value, returns globalMinioAddr
+// for FS and Erasure mode. In case of distributed server return
+// the first element from the set of peers which indicate that
+// they are local. There is always one entry that is local
+// even with repeated server endpoints.
+func GetLocalPeer(endpointServerPools EndpointServerPools, host, port string) (localPeer string) {
+ peerSet := set.NewStringSet()
+ for _, ep := range endpointServerPools {
+ for _, endpoint := range ep.Endpoints {
+ if endpoint.Type() != URLEndpointType {
+ continue
+ }
+ if endpoint.IsLocal && endpoint.Host != "" {
+ peerSet.Add(endpoint.Host)
+ }
+ }
+ }
+ if peerSet.IsEmpty() {
+ // Local peer can be empty in FS or Erasure coded mode.
+ // If so, return globalMinioHost + globalMinioPort value.
+ if host != "" {
+ return net.JoinHostPort(host, port)
+ }
+
+ return net.JoinHostPort("127.0.0.1", port)
+ }
+ return peerSet.ToSlice()[0]
+}
+
+// GetProxyEndpointLocalIndex returns index of the local proxy endpoint
+func GetProxyEndpointLocalIndex(proxyEps []ProxyEndpoint) int {
+ for i, pep := range proxyEps {
+ if pep.IsLocal {
+ return i
+ }
+ }
+ return -1
+}
+
+// GetProxyEndpoints - get all endpoints that can be used to proxy list request.
+func GetProxyEndpoints(endpointServerPools EndpointServerPools, transport http.RoundTripper) []ProxyEndpoint {
+ var proxyEps []ProxyEndpoint
+
+ proxyEpSet := set.NewStringSet()
+
+ for _, ep := range endpointServerPools {
+ for _, endpoint := range ep.Endpoints {
+ if endpoint.Type() != URLEndpointType {
+ continue
+ }
+
+ host := endpoint.Host
+ if proxyEpSet.Contains(host) {
+ continue
+ }
+ proxyEpSet.Add(host)
+
+ proxyEps = append(proxyEps, ProxyEndpoint{
+ Endpoint: endpoint,
+ Transport: transport,
+ })
+ }
+ }
+ return proxyEps
+}
+
+func updateDomainIPs(endPoints set.StringSet) {
+ ipList := set.NewStringSet()
+ for e := range endPoints {
+ host, port, err := net.SplitHostPort(e)
+ if err != nil {
+ if strings.Contains(err.Error(), "missing port in address") {
+ host = e
+ port = globalMinioPort
+ } else {
+ continue
+ }
+ }
+
+ if net.ParseIP(host) == nil {
+ IPs, err := getHostIP(host)
+ if err != nil {
+ continue
+ }
+
+ IPsWithPort := IPs.ApplyFunc(func(ip string) string {
+ return net.JoinHostPort(ip, port)
+ })
+
+ ipList = ipList.Union(IPsWithPort)
+ }
+
+ ipList.Add(net.JoinHostPort(host, port))
+ }
+
+ globalDomainIPs = ipList.FuncMatch(func(ip string, matchString string) bool {
+ host, _, err := net.SplitHostPort(ip)
+ if err != nil {
+ host = ip
+ }
+ return !net.ParseIP(host).IsLoopback() && host != "localhost"
+ }, "")
+}
diff --git a/cmd/endpoint_contrib_test.go b/cmd/endpoint_contrib_test.go
new file mode 100644
index 00000000..6b81f08b
--- /dev/null
+++ b/cmd/endpoint_contrib_test.go
@@ -0,0 +1,63 @@
+/*
+ * MinIO Object Storage (c) 2021 MinIO, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cmd
+
+import (
+ "testing"
+
+ "github.com/minio/minio-go/v7/pkg/set"
+)
+
+func TestUpdateDomainIPs(t *testing.T) {
+ tempGlobalMinioPort := globalMinioPort
+ defer func() {
+ globalMinioPort = tempGlobalMinioPort
+ }()
+ globalMinioPort = "9000"
+
+ tempGlobalDomainIPs := globalDomainIPs
+ defer func() {
+ globalDomainIPs = tempGlobalDomainIPs
+ }()
+
+ ipv4TestCases := []struct {
+ endPoints set.StringSet
+ expectedResult set.StringSet
+ }{
+ {set.NewStringSet(), set.NewStringSet()},
+ {set.CreateStringSet("localhost"), set.NewStringSet()},
+ {set.CreateStringSet("localhost", "10.0.0.1"), set.CreateStringSet("10.0.0.1:9000")},
+ {set.CreateStringSet("localhost:9001", "10.0.0.1"), set.CreateStringSet("10.0.0.1:9000")},
+ {set.CreateStringSet("localhost", "10.0.0.1:9001"), set.CreateStringSet("10.0.0.1:9001")},
+ {set.CreateStringSet("localhost:9000", "10.0.0.1:9001"), set.CreateStringSet("10.0.0.1:9001")},
+
+ {set.CreateStringSet("10.0.0.1", "10.0.0.2"), set.CreateStringSet("10.0.0.1:9000", "10.0.0.2:9000")},
+ {set.CreateStringSet("10.0.0.1:9001", "10.0.0.2"), set.CreateStringSet("10.0.0.1:9001", "10.0.0.2:9000")},
+ {set.CreateStringSet("10.0.0.1", "10.0.0.2:9002"), set.CreateStringSet("10.0.0.1:9000", "10.0.0.2:9002")},
+ {set.CreateStringSet("10.0.0.1:9001", "10.0.0.2:9002"), set.CreateStringSet("10.0.0.1:9001", "10.0.0.2:9002")},
+ }
+
+ for _, testCase := range ipv4TestCases {
+ globalDomainIPs = nil
+
+ updateDomainIPs(testCase.endPoints)
+
+ if !testCase.expectedResult.Equals(globalDomainIPs) {
+ t.Fatalf("error: expected = %s, got = %s", testCase.expectedResult, globalDomainIPs)
+ }
+ }
+}
diff --git a/cmd/endpoint_test.go b/cmd/endpoint_test.go
new file mode 100644
index 00000000..1eb034ed
--- /dev/null
+++ b/cmd/endpoint_test.go
@@ -0,0 +1,429 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "fmt"
+ "net"
+ "net/url"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+func TestNewEndpoint(t *testing.T) {
+ u2, _ := url.Parse("https://example.org/path")
+ u4, _ := url.Parse("http://192.168.253.200/path")
+ rootSlashFoo, _ := filepath.Abs("/foo")
+ testCases := []struct {
+ arg string
+ expectedEndpoint Endpoint
+ expectedType EndpointType
+ expectedErr error
+ }{
+ {"/foo", Endpoint{&url.URL{Path: rootSlashFoo}, true, -1, -1, -1}, PathEndpointType, nil},
+ {"https://example.org/path", Endpoint{u2, false, -1, -1, -1}, URLEndpointType, nil},
+ {"http://192.168.253.200/path", Endpoint{u4, false, -1, -1, -1}, URLEndpointType, nil},
+ {"", Endpoint{}, -1, fmt.Errorf("empty or root endpoint is not supported")},
+ {SlashSeparator, Endpoint{}, -1, fmt.Errorf("empty or root endpoint is not supported")},
+ {`\`, Endpoint{}, -1, fmt.Errorf("empty or root endpoint is not supported")},
+ {"c://foo", Endpoint{}, -1, fmt.Errorf("invalid URL endpoint format")},
+ {"ftp://foo", Endpoint{}, -1, fmt.Errorf("invalid URL endpoint format")},
+ {"http://server/path?location", Endpoint{}, -1, fmt.Errorf("invalid URL endpoint format")},
+ {"http://:/path", Endpoint{}, -1, fmt.Errorf("invalid URL endpoint format: invalid port number")},
+ {"http://:8080/path", Endpoint{}, -1, fmt.Errorf("invalid URL endpoint format: empty host name")},
+ {"http://server:/path", Endpoint{}, -1, fmt.Errorf("invalid URL endpoint format: invalid port number")},
+ {"https://93.184.216.34:808080/path", Endpoint{}, -1, fmt.Errorf("invalid URL endpoint format: port number must be between 1 to 65535")},
+ {"http://server:8080//", Endpoint{}, -1, fmt.Errorf("empty or root path is not supported in URL endpoint")},
+ {"http://server:8080/", Endpoint{}, -1, fmt.Errorf("empty or root path is not supported in URL endpoint")},
+ {"192.168.1.210:9000", Endpoint{}, -1, fmt.Errorf("invalid URL endpoint format: missing scheme http or https")},
+ }
+
+ for i, test := range testCases {
+ t.Run(fmt.Sprint("case-", i), func(t *testing.T) {
+ endpoint, err := NewEndpoint(test.arg)
+ if err == nil {
+ err = endpoint.UpdateIsLocal()
+ }
+
+ switch {
+ case test.expectedErr == nil:
+ if err != nil {
+ t.Errorf("error: expected = , got = %v", err)
+ }
+ case err == nil:
+ t.Errorf("error: expected = %v, got = ", test.expectedErr)
+ case test.expectedErr.Error() != err.Error():
+ t.Errorf("error: expected = %v, got = %v", test.expectedErr, err)
+ }
+
+ if err == nil {
+ if (test.expectedEndpoint.URL == nil) != (endpoint.URL == nil) {
+ t.Errorf("endpoint url: expected = %#v, got = %#v", test.expectedEndpoint.URL, endpoint.URL)
+ return
+ } else if test.expectedEndpoint.URL.String() != endpoint.URL.String() {
+ t.Errorf("endpoint url: expected = %#v, got = %#v", test.expectedEndpoint.URL.String(), endpoint.URL.String())
+ return
+ }
+ if !reflect.DeepEqual(test.expectedEndpoint, endpoint) {
+ t.Errorf("endpoint: expected = %#v, got = %#v", test.expectedEndpoint, endpoint)
+ }
+ }
+
+ if err == nil && test.expectedType != endpoint.Type() {
+ t.Errorf("type: expected = %+v, got = %+v", test.expectedType, endpoint.Type())
+ }
+ })
+ }
+}
+
+func TestNewEndpoints(t *testing.T) {
+ testCases := []struct {
+ args []string
+ expectedErr error
+ }{
+ {[]string{"/d1", "/d2", "/d3", "/d4"}, nil},
+ {[]string{"http://localhost/d1", "http://localhost/d2", "http://localhost/d3", "http://localhost/d4"}, nil},
+ {[]string{"http://example.org/d1", "http://example.com/d1", "http://example.net/d1", "http://example.edu/d1"}, nil},
+ {[]string{"http://localhost/d1", "http://localhost/d2", "http://example.org/d1", "http://example.org/d2"}, nil},
+ {[]string{"https://localhost:9000/d1", "https://localhost:9001/d2", "https://localhost:9002/d3", "https://localhost:9003/d4"}, nil},
+ // // It is valid WRT endpoint list that same path is expected with different port on same server.
+ {[]string{"https://127.0.0.1:9000/d1", "https://127.0.0.1:9001/d1", "https://127.0.0.1:9002/d1", "https://127.0.0.1:9003/d1"}, nil},
+ {[]string{"d1", "d2", "d3", "d1"}, fmt.Errorf("duplicate endpoints found")},
+ {[]string{"d1", "d2", "d3", "./d1"}, fmt.Errorf("duplicate endpoints found")},
+ {[]string{"http://localhost/d1", "http://localhost/d2", "http://localhost/d1", "http://localhost/d4"}, fmt.Errorf("duplicate endpoints found")},
+ {[]string{"ftp://server/d1", "http://server/d2", "http://server/d3", "http://server/d4"}, fmt.Errorf("'ftp://server/d1': invalid URL endpoint format")},
+ {[]string{"d1", "http://localhost/d2", "d3", "d4"}, fmt.Errorf("mixed style endpoints are not supported")},
+ {[]string{"http://example.org/d1", "https://example.com/d1", "http://example.net/d1", "https://example.edut/d1"}, fmt.Errorf("mixed scheme is not supported")},
+ {[]string{"192.168.1.210:9000/tmp/dir0", "192.168.1.210:9000/tmp/dir1", "192.168.1.210:9000/tmp/dir2", "192.168.110:9000/tmp/dir3"}, fmt.Errorf("'192.168.1.210:9000/tmp/dir0': invalid URL endpoint format: missing scheme http or https")},
+ }
+
+ for _, testCase := range testCases {
+ _, err := NewEndpoints(testCase.args...)
+ switch {
+ case testCase.expectedErr == nil:
+ if err != nil {
+ t.Fatalf("error: expected = , got = %v", err)
+ }
+ case err == nil:
+ t.Fatalf("error: expected = %v, got = ", testCase.expectedErr)
+ case testCase.expectedErr.Error() != err.Error():
+ t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err)
+ }
+ }
+}
+
+func TestCreateEndpoints(t *testing.T) {
+ tempGlobalMinioPort := globalMinioPort
+ defer func() {
+ globalMinioPort = tempGlobalMinioPort
+ }()
+ globalMinioPort = "9000"
+
+ // Filter ipList by IPs those do not start with '127.'.
+ nonLoopBackIPs := localIP4.FuncMatch(func(ip string, matchString string) bool {
+ return !net.ParseIP(ip).IsLoopback()
+ }, "")
+ if len(nonLoopBackIPs) == 0 {
+ t.Fatalf("No non-loop back IP address found for this host")
+ }
+ nonLoopBackIP := nonLoopBackIPs.ToSlice()[0]
+
+ mustAbs := func(s string) string {
+ s, err := filepath.Abs(s)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return s
+ }
+ getExpectedEndpoints := func(args []string, prefix string) ([]*url.URL, []bool) {
+ var URLs []*url.URL
+ var localFlags []bool
+ for _, arg := range args {
+ u, _ := url.Parse(arg)
+ URLs = append(URLs, u)
+ localFlags = append(localFlags, strings.HasPrefix(arg, prefix))
+ }
+
+ return URLs, localFlags
+ }
+
+ case1Endpoint1 := "http://" + nonLoopBackIP + "/d1"
+ case1Endpoint2 := "http://" + nonLoopBackIP + "/d2"
+ args := []string{
+ "http://" + nonLoopBackIP + ":10000/d1",
+ "http://" + nonLoopBackIP + ":10000/d2",
+ "http://example.org:10000/d3",
+ "http://example.com:10000/d4",
+ }
+ case1URLs, case1LocalFlags := getExpectedEndpoints(args, "http://"+nonLoopBackIP+":10000/")
+
+ case2Endpoint1 := "http://" + nonLoopBackIP + "/d1"
+ case2Endpoint2 := "http://" + nonLoopBackIP + ":9000/d2"
+ args = []string{
+ "http://" + nonLoopBackIP + ":10000/d1",
+ "http://" + nonLoopBackIP + ":9000/d2",
+ "http://example.org:10000/d3",
+ "http://example.com:10000/d4",
+ }
+ case2URLs, case2LocalFlags := getExpectedEndpoints(args, "http://"+nonLoopBackIP+":10000/")
+
+ case3Endpoint1 := "http://" + nonLoopBackIP + "/d1"
+ args = []string{
+ "http://" + nonLoopBackIP + ":80/d1",
+ "http://example.org:9000/d2",
+ "http://example.com:80/d3",
+ "http://example.net:80/d4",
+ }
+ case3URLs, case3LocalFlags := getExpectedEndpoints(args, "http://"+nonLoopBackIP+":80/")
+
+ case4Endpoint1 := "http://" + nonLoopBackIP + "/d1"
+ args = []string{
+ "http://" + nonLoopBackIP + ":9000/d1",
+ "http://example.org:9000/d2",
+ "http://example.com:9000/d3",
+ "http://example.net:9000/d4",
+ }
+ case4URLs, case4LocalFlags := getExpectedEndpoints(args, "http://"+nonLoopBackIP+":9000/")
+
+ case5Endpoint1 := "http://" + nonLoopBackIP + ":9000/d1"
+ case5Endpoint2 := "http://" + nonLoopBackIP + ":9001/d2"
+ case5Endpoint3 := "http://" + nonLoopBackIP + ":9002/d3"
+ case5Endpoint4 := "http://" + nonLoopBackIP + ":9003/d4"
+ args = []string{
+ case5Endpoint1,
+ case5Endpoint2,
+ case5Endpoint3,
+ case5Endpoint4,
+ }
+ case5URLs, case5LocalFlags := getExpectedEndpoints(args, "http://"+nonLoopBackIP+":9000/")
+
+ case6Endpoint := "http://" + nonLoopBackIP + ":9003/d4"
+ args = []string{
+ "http://localhost:9000/d1",
+ "http://localhost:9001/d2",
+ "http://127.0.0.1:9002/d3",
+ case6Endpoint,
+ }
+ case6URLs, case6LocalFlags := getExpectedEndpoints(args, "http://"+nonLoopBackIP+":9003/")
+
+ testCases := []struct {
+ serverAddr string
+ args []string
+ expectedServerAddr string
+ expectedEndpoints Endpoints
+ expectedSetupType SetupType
+ expectedErr error
+ }{
+ {"localhost", []string{}, "", Endpoints{}, -1, fmt.Errorf("address localhost: missing port in address")},
+
+ // Erasure Single Drive
+ {"localhost:9000", []string{"http://localhost/d1"}, "", Endpoints{}, -1, fmt.Errorf("use path style endpoint for SD setup")},
+ {":443", []string{"/d1"}, ":443", Endpoints{Endpoint{URL: &url.URL{Path: mustAbs("/d1")}, IsLocal: true}}, ErasureSDSetupType, nil},
+ {"localhost:10000", []string{"/d1"}, "localhost:10000", Endpoints{Endpoint{URL: &url.URL{Path: mustAbs("/d1")}, IsLocal: true}}, ErasureSDSetupType, nil},
+ {"localhost:9000", []string{"https://127.0.0.1:9000/d1", "https://localhost:9001/d1", "https://example.com/d1", "https://example.com/d2"}, "", Endpoints{}, -1, fmt.Errorf("path '/d1' can not be served by different port on same address")},
+
+ // Erasure Setup with PathEndpointType
+ {
+ ":1234",
+ []string{"/d1", "/d2", "/d3", "/d4"},
+ ":1234",
+ Endpoints{
+ Endpoint{URL: &url.URL{Path: mustAbs("/d1")}, IsLocal: true},
+ Endpoint{URL: &url.URL{Path: mustAbs("/d2")}, IsLocal: true},
+ Endpoint{URL: &url.URL{Path: mustAbs("/d3")}, IsLocal: true},
+ Endpoint{URL: &url.URL{Path: mustAbs("/d4")}, IsLocal: true},
+ },
+ ErasureSetupType, nil,
+ },
+ // DistErasure Setup with URLEndpointType
+ {":9000", []string{"http://localhost/d1", "http://localhost/d2", "http://localhost/d3", "http://localhost/d4"}, ":9000", Endpoints{
+ Endpoint{URL: &url.URL{Scheme: "http", Host: "localhost:9000", Path: "/d1"}, IsLocal: true},
+ Endpoint{URL: &url.URL{Scheme: "http", Host: "localhost:9000", Path: "/d2"}, IsLocal: true},
+ Endpoint{URL: &url.URL{Scheme: "http", Host: "localhost:9000", Path: "/d3"}, IsLocal: true},
+ Endpoint{URL: &url.URL{Scheme: "http", Host: "localhost:9000", Path: "/d4"}, IsLocal: true},
+ }, ErasureSetupType, nil},
+ // DistErasure Setup with URLEndpointType having mixed naming to local host.
+ {"127.0.0.1:10000", []string{"http://localhost/d1", "http://localhost/d2", "http://127.0.0.1/d3", "http://127.0.0.1/d4"}, "", Endpoints{}, -1, fmt.Errorf("all local endpoints should not have different hostnames/ips")},
+
+ {":9001", []string{"http://10.0.0.1:9000/export", "http://10.0.0.2:9000/export", "http://" + nonLoopBackIP + ":9001/export", "http://10.0.0.2:9001/export"}, "", Endpoints{}, -1, fmt.Errorf("path '/export' can not be served by different port on same address")},
+
+ {":9000", []string{"http://127.0.0.1:9000/export", "http://" + nonLoopBackIP + ":9000/export", "http://10.0.0.1:9000/export", "http://10.0.0.2:9000/export"}, "", Endpoints{}, -1, fmt.Errorf("path '/export' cannot be served by different address on same server")},
+
+ // DistErasure type
+ {"127.0.0.1:10000", []string{case1Endpoint1, case1Endpoint2, "http://example.org/d3", "http://example.com/d4"}, "127.0.0.1:10000", Endpoints{
+ Endpoint{URL: case1URLs[0], IsLocal: case1LocalFlags[0]},
+ Endpoint{URL: case1URLs[1], IsLocal: case1LocalFlags[1]},
+ Endpoint{URL: case1URLs[2], IsLocal: case1LocalFlags[2]},
+ Endpoint{URL: case1URLs[3], IsLocal: case1LocalFlags[3]},
+ }, DistErasureSetupType, nil},
+
+ {"127.0.0.1:10000", []string{case2Endpoint1, case2Endpoint2, "http://example.org/d3", "http://example.com/d4"}, "127.0.0.1:10000", Endpoints{
+ Endpoint{URL: case2URLs[0], IsLocal: case2LocalFlags[0]},
+ Endpoint{URL: case2URLs[1], IsLocal: case2LocalFlags[1]},
+ Endpoint{URL: case2URLs[2], IsLocal: case2LocalFlags[2]},
+ Endpoint{URL: case2URLs[3], IsLocal: case2LocalFlags[3]},
+ }, DistErasureSetupType, nil},
+
+ {":80", []string{case3Endpoint1, "http://example.org:9000/d2", "http://example.com/d3", "http://example.net/d4"}, ":80", Endpoints{
+ Endpoint{URL: case3URLs[0], IsLocal: case3LocalFlags[0]},
+ Endpoint{URL: case3URLs[1], IsLocal: case3LocalFlags[1]},
+ Endpoint{URL: case3URLs[2], IsLocal: case3LocalFlags[2]},
+ Endpoint{URL: case3URLs[3], IsLocal: case3LocalFlags[3]},
+ }, DistErasureSetupType, nil},
+
+ {":9000", []string{case4Endpoint1, "http://example.org/d2", "http://example.com/d3", "http://example.net/d4"}, ":9000", Endpoints{
+ Endpoint{URL: case4URLs[0], IsLocal: case4LocalFlags[0]},
+ Endpoint{URL: case4URLs[1], IsLocal: case4LocalFlags[1]},
+ Endpoint{URL: case4URLs[2], IsLocal: case4LocalFlags[2]},
+ Endpoint{URL: case4URLs[3], IsLocal: case4LocalFlags[3]},
+ }, DistErasureSetupType, nil},
+
+ {":9000", []string{case5Endpoint1, case5Endpoint2, case5Endpoint3, case5Endpoint4}, ":9000", Endpoints{
+ Endpoint{URL: case5URLs[0], IsLocal: case5LocalFlags[0]},
+ Endpoint{URL: case5URLs[1], IsLocal: case5LocalFlags[1]},
+ Endpoint{URL: case5URLs[2], IsLocal: case5LocalFlags[2]},
+ Endpoint{URL: case5URLs[3], IsLocal: case5LocalFlags[3]},
+ }, DistErasureSetupType, nil},
+
+ // DistErasure Setup using only local host.
+ {":9003", []string{"http://localhost:9000/d1", "http://localhost:9001/d2", "http://127.0.0.1:9002/d3", case6Endpoint}, ":9003", Endpoints{
+ Endpoint{URL: case6URLs[0], IsLocal: case6LocalFlags[0]},
+ Endpoint{URL: case6URLs[1], IsLocal: case6LocalFlags[1]},
+ Endpoint{URL: case6URLs[2], IsLocal: case6LocalFlags[2]},
+ Endpoint{URL: case6URLs[3], IsLocal: case6LocalFlags[3]},
+ }, DistErasureSetupType, nil},
+ }
+
+ for i, testCase := range testCases {
+ testCase := testCase
+ t.Run("", func(t *testing.T) {
+ var srvCtxt serverCtxt
+ err := mergeDisksLayoutFromArgs(testCase.args, &srvCtxt)
+ if err != nil && testCase.expectedErr == nil {
+ t.Errorf("Test %d: unexpected error: %v", i+1, err)
+ }
+ pools, setupType, err := CreatePoolEndpoints(testCase.serverAddr, srvCtxt.Layout.pools...)
+ if err == nil && testCase.expectedErr != nil {
+ t.Errorf("Test %d: expected = %v, got = ", i+1, testCase.expectedErr)
+ }
+ if err == nil {
+ if setupType != testCase.expectedSetupType {
+ t.Errorf("Test %d: setupType: expected = %v, got = %v", i+1, testCase.expectedSetupType, setupType)
+ }
+ endpoints := pools[0]
+ if len(endpoints) != len(testCase.expectedEndpoints) {
+ t.Errorf("Test %d: endpoints: expected = %d, got = %d", i+1, len(testCase.expectedEndpoints),
+ len(endpoints))
+ } else {
+ for i, endpoint := range endpoints {
+ if testCase.expectedEndpoints[i].String() != endpoint.String() {
+ t.Errorf("Test %d: endpoints: expected = %s, got = %s",
+ i+1,
+ testCase.expectedEndpoints[i],
+ endpoint)
+ }
+ }
+ }
+ }
+ if err != nil && testCase.expectedErr == nil {
+ t.Errorf("Test %d: error: expected = , got = %v, testCase: %v", i+1, err, testCase)
+ }
+ })
+ }
+}
+
+// Tests get local peer functionality, local peer is supposed to only return one entry per minio service.
+// So it means that if you have say localhost:9000 and localhost:9001 as endpointArgs then localhost:9001
+// is considered a remote service from localhost:9000 perspective.
+func TestGetLocalPeer(t *testing.T) {
+ tempGlobalMinioPort := globalMinioPort
+ defer func() {
+ globalMinioPort = tempGlobalMinioPort
+ }()
+ globalMinioPort = "9000"
+
+ testCases := []struct {
+ endpointArgs []string
+ expectedResult string
+ }{
+ {[]string{"/d1", "/d2", "d3", "d4"}, "127.0.0.1:9000"},
+ {
+ []string{"http://localhost:9000/d1", "http://localhost:9000/d2", "http://example.org:9000/d3", "http://example.com:9000/d4"},
+ "localhost:9000",
+ },
+ {
+ []string{"http://localhost:9000/d1", "http://example.org:9000/d2", "http://example.com:9000/d3", "http://example.net:9000/d4"},
+ "localhost:9000",
+ },
+ {
+ []string{"http://localhost:9000/d1", "http://localhost:9001/d2", "http://localhost:9002/d3", "http://localhost:9003/d4"},
+ "localhost:9000",
+ },
+ }
+
+ for i, testCase := range testCases {
+ zendpoints := mustGetPoolEndpoints(0, testCase.endpointArgs...)
+ if !zendpoints[0].Endpoints[0].IsLocal {
+ if err := zendpoints[0].Endpoints.UpdateIsLocal(); err != nil {
+ t.Fatalf("error: expected = , got = %v", err)
+ }
+ }
+ localPeer := GetLocalPeer(zendpoints, "", "9000")
+ if localPeer != testCase.expectedResult {
+ t.Fatalf("Test %d: expected: %v, got: %v", i+1, testCase.expectedResult, localPeer)
+ }
+ }
+}
+
+func TestGetRemotePeers(t *testing.T) {
+ tempGlobalMinioPort := globalMinioPort
+ defer func() {
+ globalMinioPort = tempGlobalMinioPort
+ }()
+ globalMinioPort = "9000"
+
+ testCases := []struct {
+ endpointArgs []string
+ expectedResult []string
+ expectedLocal string
+ }{
+ {[]string{"/d1", "/d2", "d3", "d4"}, []string{}, ""},
+ {[]string{"http://localhost:9000/d1", "http://localhost:9000/d2", "http://example.org:9000/d3", "http://example.com:9000/d4"}, []string{"example.com:9000", "example.org:9000", "localhost:9000"}, "localhost:9000"},
+ {[]string{"http://localhost:9000/d1", "http://localhost:10000/d2", "http://example.org:9000/d3", "http://example.com:9000/d4"}, []string{"example.com:9000", "example.org:9000", "localhost:10000", "localhost:9000"}, "localhost:9000"},
+ {[]string{"http://localhost:9000/d1", "http://example.org:9000/d2", "http://example.com:9000/d3", "http://example.net:9000/d4"}, []string{"example.com:9000", "example.net:9000", "example.org:9000", "localhost:9000"}, "localhost:9000"},
+ {[]string{"http://localhost:9000/d1", "http://localhost:9001/d2", "http://localhost:9002/d3", "http://localhost:9003/d4"}, []string{"localhost:9000", "localhost:9001", "localhost:9002", "localhost:9003"}, "localhost:9000"},
+ }
+
+ for _, testCase := range testCases {
+ zendpoints := mustGetPoolEndpoints(0, testCase.endpointArgs...)
+ if !zendpoints[0].Endpoints[0].IsLocal {
+ if err := zendpoints[0].Endpoints.UpdateIsLocal(); err != nil {
+ t.Errorf("error: expected = , got = %v", err)
+ }
+ }
+ remotePeers, local := zendpoints.peers()
+ if !reflect.DeepEqual(remotePeers, testCase.expectedResult) {
+ t.Errorf("expected: %v, got: %v", testCase.expectedResult, remotePeers)
+ }
+ if local != testCase.expectedLocal {
+ t.Errorf("expected: %v, got: %v", testCase.expectedLocal, local)
+ }
+ }
+}
diff --git a/cmd/erasure-coding.go b/cmd/erasure-coding.go
new file mode 100644
index 00000000..3294458e
--- /dev/null
+++ b/cmd/erasure-coding.go
@@ -0,0 +1,206 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "encoding/hex"
+ "fmt"
+ "os"
+ "reflect"
+ "sync"
+
+ "github.com/cespare/xxhash/v2"
+ "github.com/klauspost/reedsolomon"
+ "github.com/minio/minio/internal/logger"
+)
+
+// Erasure - erasure encoding details.
+type Erasure struct {
+ encoder func() reedsolomon.Encoder
+ dataBlocks, parityBlocks int
+ blockSize int64
+}
+
+// NewErasure creates a new ErasureStorage.
+func NewErasure(ctx context.Context, dataBlocks, parityBlocks int, blockSize int64) (e Erasure, err error) {
+ // Check the parameters for sanity now.
+ if dataBlocks <= 0 || parityBlocks < 0 {
+ return e, reedsolomon.ErrInvShardNum
+ }
+
+ if dataBlocks+parityBlocks > 256 {
+ return e, reedsolomon.ErrMaxShardNum
+ }
+
+ e = Erasure{
+ dataBlocks: dataBlocks,
+ parityBlocks: parityBlocks,
+ blockSize: blockSize,
+ }
+
+ // Encoder when needed.
+ var enc reedsolomon.Encoder
+ var once sync.Once
+ e.encoder = func() reedsolomon.Encoder {
+ once.Do(func() {
+ e, err := reedsolomon.New(dataBlocks, parityBlocks, reedsolomon.WithAutoGoroutines(int(e.ShardSize())))
+ if err != nil {
+ // Error conditions should be checked above.
+ panic(err)
+ }
+ enc = e
+ })
+ return enc
+ }
+ return e, err
+}
+
+// EncodeData encodes the given data and returns the erasure-coded data.
+// It returns an error if the erasure coding failed.
+func (e *Erasure) EncodeData(ctx context.Context, data []byte) ([][]byte, error) {
+ if len(data) == 0 {
+ return make([][]byte, e.dataBlocks+e.parityBlocks), nil
+ }
+ encoded, err := e.encoder().Split(data)
+ if err != nil {
+ return nil, err
+ }
+ if err = e.encoder().Encode(encoded); err != nil {
+ return nil, err
+ }
+ return encoded, nil
+}
+
+// DecodeDataBlocks decodes the given erasure-coded data.
+// It only decodes the data blocks but does not verify them.
+// It returns an error if the decoding failed.
+func (e *Erasure) DecodeDataBlocks(data [][]byte) error {
+ isZero := 0
+ for _, b := range data {
+ if len(b) == 0 {
+ isZero++
+ break
+ }
+ }
+ if isZero == 0 || isZero == len(data) {
+ // If all are zero, payload is 0 bytes.
+ return nil
+ }
+ return e.encoder().ReconstructData(data)
+}
+
+// DecodeDataAndParityBlocks decodes the given erasure-coded data and verifies it.
+// It returns an error if the decoding failed.
+func (e *Erasure) DecodeDataAndParityBlocks(ctx context.Context, data [][]byte) error {
+ return e.encoder().Reconstruct(data)
+}
+
+// ShardSize - returns actual shared size from erasure blockSize.
+func (e *Erasure) ShardSize() int64 {
+ return ceilFrac(e.blockSize, int64(e.dataBlocks))
+}
+
+// ShardFileSize - returns final erasure size from original size.
+func (e *Erasure) ShardFileSize(totalLength int64) int64 {
+ if totalLength == 0 {
+ return 0
+ }
+ if totalLength == -1 {
+ return -1
+ }
+ numShards := totalLength / e.blockSize
+ lastBlockSize := totalLength % e.blockSize
+ lastShardSize := ceilFrac(lastBlockSize, int64(e.dataBlocks))
+ return numShards*e.ShardSize() + lastShardSize
+}
+
+// ShardFileOffset - returns the effective offset where erasure reading begins.
+func (e *Erasure) ShardFileOffset(startOffset, length, totalLength int64) int64 {
+ shardSize := e.ShardSize()
+ shardFileSize := e.ShardFileSize(totalLength)
+ endShard := (startOffset + length) / e.blockSize
+ tillOffset := min(endShard*shardSize+shardSize, shardFileSize)
+ return tillOffset
+}
+
+// erasureSelfTest performs a self-test to ensure that erasure
+// algorithms compute expected erasure codes. If any algorithm
+// produces an incorrect value it fails with a hard error.
+//
+// erasureSelfTest tries to catch any issue in the erasure implementation
+// early instead of silently corrupting data.
+func erasureSelfTest() {
+ // Approx runtime ~1ms
+ var testConfigs [][2]uint8
+ for total := uint8(4); total < 16; total++ {
+ for data := total / 2; data < total; data++ {
+ parity := total - data
+ testConfigs = append(testConfigs, [2]uint8{data, parity})
+ }
+ }
+ got := make(map[[2]uint8]map[ErasureAlgo]uint64, len(testConfigs))
+ // Copied from output of fmt.Printf("%#v", got) at the end.
+ want := map[[2]uint8]map[ErasureAlgo]uint64{{0x2, 0x2}: {0x1: 0x23fb21be2496f5d3}, {0x2, 0x3}: {0x1: 0xa5cd5600ba0d8e7c}, {0x3, 0x1}: {0x1: 0x60ab052148b010b4}, {0x3, 0x2}: {0x1: 0xe64927daef76435a}, {0x3, 0x3}: {0x1: 0x672f6f242b227b21}, {0x3, 0x4}: {0x1: 0x571e41ba23a6dc6}, {0x4, 0x1}: {0x1: 0x524eaa814d5d86e2}, {0x4, 0x2}: {0x1: 0x62b9552945504fef}, {0x4, 0x3}: {0x1: 0xcbf9065ee053e518}, {0x4, 0x4}: {0x1: 0x9a07581dcd03da8}, {0x4, 0x5}: {0x1: 0xbf2d27b55370113f}, {0x5, 0x1}: {0x1: 0xf71031a01d70daf}, {0x5, 0x2}: {0x1: 0x8e5845859939d0f4}, {0x5, 0x3}: {0x1: 0x7ad9161acbb4c325}, {0x5, 0x4}: {0x1: 0xc446b88830b4f800}, {0x5, 0x5}: {0x1: 0xabf1573cc6f76165}, {0x5, 0x6}: {0x1: 0x7b5598a85045bfb8}, {0x6, 0x1}: {0x1: 0xe2fc1e677cc7d872}, {0x6, 0x2}: {0x1: 0x7ed133de5ca6a58e}, {0x6, 0x3}: {0x1: 0x39ef92d0a74cc3c0}, {0x6, 0x4}: {0x1: 0xcfc90052bc25d20}, {0x6, 0x5}: {0x1: 0x71c96f6baeef9c58}, {0x6, 0x6}: {0x1: 0x4b79056484883e4c}, {0x6, 0x7}: {0x1: 0xb1a0e2427ac2dc1a}, {0x7, 0x1}: {0x1: 0x937ba2b7af467a22}, {0x7, 0x2}: {0x1: 0x5fd13a734d27d37a}, {0x7, 0x3}: {0x1: 0x3be2722d9b66912f}, {0x7, 0x4}: {0x1: 0x14c628e59011be3d}, {0x7, 0x5}: {0x1: 0xcc3b39ad4c083b9f}, {0x7, 0x6}: {0x1: 0x45af361b7de7a4ff}, {0x7, 0x7}: {0x1: 0x456cc320cec8a6e6}, {0x7, 0x8}: {0x1: 0x1867a9f4db315b5c}, {0x8, 0x1}: {0x1: 0xbc5756b9a9ade030}, {0x8, 0x2}: {0x1: 0xdfd7d9d0b3e36503}, {0x8, 0x3}: {0x1: 0x72bb72c2cdbcf99d}, {0x8, 0x4}: {0x1: 0x3ba5e9b41bf07f0}, {0x8, 0x5}: {0x1: 0xd7dabc15800f9d41}, {0x8, 0x6}: {0x1: 0xb482a6169fd270f}, {0x8, 0x7}: {0x1: 0x50748e0099d657e8}, {0x9, 0x1}: {0x1: 0xc77ae0144fcaeb6e}, {0x9, 0x2}: {0x1: 0x8a86c7dbebf27b68}, {0x9, 0x3}: {0x1: 0xa64e3be6d6fe7e92}, {0x9, 0x4}: {0x1: 0x239b71c41745d207}, {0x9, 0x5}: {0x1: 0x2d0803094c5a86ce}, {0x9, 0x6}: {0x1: 0xa3c2539b3af84874}, {0xa, 0x1}: {0x1: 0x7d30d91b89fcec21}, {0xa, 0x2}: {0x1: 0xfa5af9aa9f1857a3}, {0xa, 0x3}: {0x1: 0x84bc4bda8af81f90}, {0xa, 0x4}: {0x1: 0x6c1cba8631de994a}, {0xa, 0x5}: {0x1: 0x4383e58a086cc1ac}, {0xb, 0x1}: {0x1: 0x4ed2929a2df690b}, {0xb, 0x2}: {0x1: 0xecd6f1b1399775c0}, {0xb, 0x3}: {0x1: 0xc78cfbfc0dc64d01}, {0xb, 0x4}: {0x1: 0xb2643390973702d6}, {0xc, 0x1}: {0x1: 0x3b2a88686122d082}, {0xc, 0x2}: {0x1: 0xfd2f30a48a8e2e9}, {0xc, 0x3}: {0x1: 0xd5ce58368ae90b13}, {0xd, 0x1}: {0x1: 0x9c88e2a9d1b8fff8}, {0xd, 0x2}: {0x1: 0xcb8460aa4cf6613}, {0xe, 0x1}: {0x1: 0x78a28bbaec57996e}}
+ var testData [256]byte
+ for i := range testData {
+ testData[i] = byte(i)
+ }
+ ok := true
+ for algo := invalidErasureAlgo + 1; algo < lastErasureAlgo; algo++ {
+ for _, conf := range testConfigs {
+ failOnErr := func(err error) {
+ if err != nil {
+ logger.Fatal(errSelfTestFailure, "%v: error on self-test [d:%d,p:%d]: %v. Unsafe to start server.\n", algo, conf[0], conf[1], err)
+ }
+ }
+ e, err := NewErasure(context.Background(), int(conf[0]), int(conf[1]), blockSizeV2)
+ failOnErr(err)
+ encoded, err := e.EncodeData(GlobalContext, testData[:])
+ failOnErr(err)
+ hash := xxhash.New()
+ for i, data := range encoded {
+ // Write index to keep track of sizes of each.
+ _, err = hash.Write([]byte{byte(i)})
+ failOnErr(err)
+ _, err = hash.Write(data)
+ failOnErr(err)
+ got[conf] = map[ErasureAlgo]uint64{algo: hash.Sum64()}
+ }
+
+ if a, b := want[conf], got[conf]; !reflect.DeepEqual(a, b) {
+ fmt.Fprintf(os.Stderr, "%v: error on self-test [d:%d,p:%d]: want %#v, got %#v\n", algo, conf[0], conf[1], a, b)
+ ok = false
+ continue
+ }
+ // Delete first shard and reconstruct...
+ first := encoded[0]
+ encoded[0] = nil
+ failOnErr(e.DecodeDataBlocks(encoded))
+ if a, b := first, encoded[0]; !bytes.Equal(a, b) {
+ fmt.Fprintf(os.Stderr, "%v: error on self-test [d:%d,p:%d]: want %#v, got %#v\n", algo, conf[0], conf[1], hex.EncodeToString(a), hex.EncodeToString(b))
+ ok = false
+ continue
+ }
+ }
+ }
+ if !ok {
+ logger.Fatal(errSelfTestFailure, "Erasure Coding self test failed")
+ }
+}
diff --git a/cmd/erasure-common.go b/cmd/erasure-common.go
new file mode 100644
index 00000000..7146766a
--- /dev/null
+++ b/cmd/erasure-common.go
@@ -0,0 +1,84 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "math/rand"
+ "sync"
+ "time"
+)
+
+func (er erasureObjects) getOnlineDisks() (newDisks []StorageAPI) {
+ disks := er.getDisks()
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+ for _, i := range r.Perm(len(disks)) {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ if disks[i] == nil {
+ return
+ }
+ di, err := disks[i].DiskInfo(context.Background(), DiskInfoOptions{})
+ if err != nil || di.Healing {
+ // - Do not consume disks which are not reachable
+ // unformatted or simply not accessible for some reason.
+ //
+ // - Do not consume disks which are being healed
+ //
+ // - Future: skip busy disks
+ return
+ }
+
+ mu.Lock()
+ newDisks = append(newDisks, disks[i])
+ mu.Unlock()
+ }()
+ }
+ wg.Wait()
+ return newDisks
+}
+
+func (er erasureObjects) getOnlineLocalDisks() (newDisks []StorageAPI) {
+ disks := er.getOnlineDisks()
+
+ // Based on the random shuffling return back randomized disks.
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+
+ for _, i := range r.Perm(len(disks)) {
+ if disks[i] != nil && disks[i].IsLocal() {
+ newDisks = append(newDisks, disks[i])
+ }
+ }
+
+ return newDisks
+}
+
+func (er erasureObjects) getLocalDisks() (newDisks []StorageAPI) {
+ disks := er.getDisks()
+ // Based on the random shuffling return back randomized disks.
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+ for _, i := range r.Perm(len(disks)) {
+ if disks[i] != nil && disks[i].IsLocal() {
+ newDisks = append(newDisks, disks[i])
+ }
+ }
+ return newDisks
+}
diff --git a/cmd/erasure-decode.go b/cmd/erasure-decode.go
new file mode 100644
index 00000000..f0cc90ab
--- /dev/null
+++ b/cmd/erasure-decode.go
@@ -0,0 +1,364 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "sync"
+ "sync/atomic"
+
+ xioutil "github.com/minio/minio/internal/ioutil"
+)
+
+// Reads in parallel from readers.
+type parallelReader struct {
+ readers []io.ReaderAt
+ orgReaders []io.ReaderAt
+ dataBlocks int
+ offset int64
+ shardSize int64
+ shardFileSize int64
+ buf [][]byte
+ readerToBuf []int
+ stashBuffer []byte
+}
+
+// newParallelReader returns parallelReader.
+func newParallelReader(readers []io.ReaderAt, e Erasure, offset, totalLength int64) *parallelReader {
+ r2b := make([]int, len(readers))
+ for i := range r2b {
+ r2b[i] = i
+ }
+ bufs := make([][]byte, len(readers))
+ shardSize := int(e.ShardSize())
+ var b []byte
+
+ // We should always have enough capacity, but older objects may be bigger
+ // we do not need stashbuffer for them.
+ if globalBytePoolCap.Load().WidthCap() >= len(readers)*shardSize {
+ // Fill buffers
+ b = globalBytePoolCap.Load().Get()
+ // Seed the buffers.
+ for i := range bufs {
+ bufs[i] = b[i*shardSize : (i+1)*shardSize]
+ }
+ }
+
+ return ¶llelReader{
+ readers: readers,
+ orgReaders: readers,
+ dataBlocks: e.dataBlocks,
+ offset: (offset / e.blockSize) * e.ShardSize(),
+ shardSize: e.ShardSize(),
+ shardFileSize: e.ShardFileSize(totalLength),
+ buf: make([][]byte, len(readers)),
+ readerToBuf: r2b,
+ stashBuffer: b,
+ }
+}
+
+// Done will release any resources used by the parallelReader.
+func (p *parallelReader) Done() {
+ if p.stashBuffer != nil {
+ globalBytePoolCap.Load().Put(p.stashBuffer)
+ p.stashBuffer = nil
+ }
+}
+
+// preferReaders can mark readers as preferred.
+// These will be chosen before others.
+func (p *parallelReader) preferReaders(prefer []bool) {
+ if len(prefer) != len(p.orgReaders) {
+ return
+ }
+ // Copy so we don't change our input.
+ tmp := make([]io.ReaderAt, len(p.orgReaders))
+ copy(tmp, p.orgReaders)
+ p.readers = tmp
+ // next is the next non-preferred index.
+ next := 0
+ for i, ok := range prefer {
+ if !ok || p.readers[i] == nil {
+ continue
+ }
+ if i == next {
+ next++
+ continue
+ }
+ // Move reader with index i to index next.
+ // Do this by swapping next and i
+ p.readers[next], p.readers[i] = p.readers[i], p.readers[next]
+ p.readerToBuf[next] = i
+ p.readerToBuf[i] = next
+ next++
+ }
+}
+
+// Returns if buf can be erasure decoded.
+func (p *parallelReader) canDecode(buf [][]byte) bool {
+ bufCount := 0
+ for _, b := range buf {
+ if len(b) > 0 {
+ bufCount++
+ }
+ }
+ return bufCount >= p.dataBlocks
+}
+
+// Read reads from readers in parallel. Returns p.dataBlocks number of bufs.
+func (p *parallelReader) Read(dst [][]byte) ([][]byte, error) {
+ newBuf := dst
+ if len(dst) != len(p.readers) {
+ newBuf = make([][]byte, len(p.readers))
+ } else {
+ for i := range newBuf {
+ newBuf[i] = newBuf[i][:0]
+ }
+ }
+ var newBufLK sync.RWMutex
+
+ if p.offset+p.shardSize > p.shardFileSize {
+ p.shardSize = p.shardFileSize - p.offset
+ }
+ if p.shardSize == 0 {
+ return newBuf, nil
+ }
+
+ readTriggerCh := make(chan bool, len(p.readers))
+ defer xioutil.SafeClose(readTriggerCh) // close the channel upon return
+
+ for i := 0; i < p.dataBlocks; i++ {
+ // Setup read triggers for p.dataBlocks number of reads so that it reads in parallel.
+ readTriggerCh <- true
+ }
+
+ disksNotFound := int32(0)
+ bitrotHeal := int32(0) // Atomic bool flag.
+ missingPartsHeal := int32(0) // Atomic bool flag.
+ readerIndex := 0
+ var wg sync.WaitGroup
+ // if readTrigger is true, it implies next disk.ReadAt() should be tried
+ // if readTrigger is false, it implies previous disk.ReadAt() was successful and there is no need
+ // to try reading the next disk.
+ for readTrigger := range readTriggerCh {
+ newBufLK.RLock()
+ canDecode := p.canDecode(newBuf)
+ newBufLK.RUnlock()
+ if canDecode {
+ break
+ }
+ if readerIndex == len(p.readers) {
+ break
+ }
+ if !readTrigger {
+ continue
+ }
+ wg.Add(1)
+ go func(i int) {
+ defer wg.Done()
+ rr := p.readers[i]
+ if rr == nil {
+ // Since reader is nil, trigger another read.
+ readTriggerCh <- true
+ return
+ }
+ bufIdx := p.readerToBuf[i]
+ if p.buf[bufIdx] == nil {
+ // Reading first time on this disk, hence the buffer needs to be allocated.
+ // Subsequent reads will reuse this buffer.
+ p.buf[bufIdx] = make([]byte, p.shardSize)
+ }
+ // For the last shard, the shardsize might be less than previous shard sizes.
+ // Hence the following statement ensures that the buffer size is reset to the right size.
+ p.buf[bufIdx] = p.buf[bufIdx][:p.shardSize]
+ n, err := rr.ReadAt(p.buf[bufIdx], p.offset)
+ if err != nil {
+ switch {
+ case errors.Is(err, errFileNotFound):
+ atomic.StoreInt32(&missingPartsHeal, 1)
+ case errors.Is(err, errFileCorrupt):
+ atomic.StoreInt32(&bitrotHeal, 1)
+ case errors.Is(err, errDiskNotFound):
+ atomic.AddInt32(&disksNotFound, 1)
+ }
+
+ // This will be communicated upstream.
+ p.orgReaders[bufIdx] = nil
+ if br, ok := p.readers[i].(io.Closer); ok {
+ br.Close()
+ }
+ p.readers[i] = nil
+
+ // Since ReadAt returned error, trigger another read.
+ readTriggerCh <- true
+ return
+ }
+ newBufLK.Lock()
+ newBuf[bufIdx] = p.buf[bufIdx][:n]
+ newBufLK.Unlock()
+ // Since ReadAt returned success, there is no need to trigger another read.
+ readTriggerCh <- false
+ }(readerIndex)
+ readerIndex++
+ }
+ wg.Wait()
+ if p.canDecode(newBuf) {
+ p.offset += p.shardSize
+ if missingPartsHeal == 1 {
+ return newBuf, errFileNotFound
+ } else if bitrotHeal == 1 {
+ return newBuf, errFileCorrupt
+ }
+ return newBuf, nil
+ }
+
+ // If we cannot decode, just return read quorum error.
+ return nil, fmt.Errorf("%w (offline-disks=%d/%d)", errErasureReadQuorum, disksNotFound, len(p.readers))
+}
+
+// Decode reads from readers, reconstructs data if needed and writes the data to the writer.
+// A set of preferred drives can be supplied. In that case they will be used and the data reconstructed.
+func (e Erasure) Decode(ctx context.Context, writer io.Writer, readers []io.ReaderAt, offset, length, totalLength int64, prefer []bool) (written int64, derr error) {
+ if offset < 0 || length < 0 {
+ return -1, errInvalidArgument
+ }
+ if offset+length > totalLength {
+ return -1, errInvalidArgument
+ }
+
+ if length == 0 {
+ return 0, nil
+ }
+
+ reader := newParallelReader(readers, e, offset, totalLength)
+ if len(prefer) == len(readers) {
+ reader.preferReaders(prefer)
+ }
+ defer reader.Done()
+
+ startBlock := offset / e.blockSize
+ endBlock := (offset + length) / e.blockSize
+
+ var bytesWritten int64
+ var bufs [][]byte
+ for block := startBlock; block <= endBlock; block++ {
+ var blockOffset, blockLength int64
+ switch {
+ case startBlock == endBlock:
+ blockOffset = offset % e.blockSize
+ blockLength = length
+ case block == startBlock:
+ blockOffset = offset % e.blockSize
+ blockLength = e.blockSize - blockOffset
+ case block == endBlock:
+ blockOffset = 0
+ blockLength = (offset + length) % e.blockSize
+ default:
+ blockOffset = 0
+ blockLength = e.blockSize
+ }
+ if blockLength == 0 {
+ break
+ }
+
+ var err error
+ bufs, err = reader.Read(bufs)
+ if len(bufs) > 0 {
+ // Set only if there are be enough data for reconstruction.
+ // and only for expected errors, also set once.
+ if errors.Is(err, errFileNotFound) || errors.Is(err, errFileCorrupt) {
+ if derr == nil {
+ derr = err
+ }
+ }
+ } else if err != nil {
+ // For all errors that cannot be reconstructed fail the read operation.
+ return -1, err
+ }
+
+ if err = e.DecodeDataBlocks(bufs); err != nil {
+ return -1, err
+ }
+
+ n, err := writeDataBlocks(ctx, writer, bufs, e.dataBlocks, blockOffset, blockLength)
+ if err != nil {
+ return -1, err
+ }
+
+ bytesWritten += n
+ }
+
+ if bytesWritten != length {
+ return bytesWritten, errLessData
+ }
+
+ return bytesWritten, derr
+}
+
+// Heal reads from readers, reconstruct shards and writes the data to the writers.
+func (e Erasure) Heal(ctx context.Context, writers []io.Writer, readers []io.ReaderAt, totalLength int64, prefer []bool) (derr error) {
+ if len(writers) != e.parityBlocks+e.dataBlocks {
+ return errInvalidArgument
+ }
+
+ reader := newParallelReader(readers, e, 0, totalLength)
+ if len(readers) == len(prefer) {
+ reader.preferReaders(prefer)
+ }
+ defer reader.Done()
+
+ startBlock := int64(0)
+ endBlock := totalLength / e.blockSize
+ if totalLength%e.blockSize != 0 {
+ endBlock++
+ }
+
+ var bufs [][]byte
+ for block := startBlock; block < endBlock; block++ {
+ var err error
+ bufs, err = reader.Read(bufs)
+ if len(bufs) > 0 {
+ if errors.Is(err, errFileNotFound) || errors.Is(err, errFileCorrupt) {
+ if derr == nil {
+ derr = err
+ }
+ }
+ } else if err != nil {
+ return err
+ }
+
+ if err = e.DecodeDataAndParityBlocks(ctx, bufs); err != nil {
+ return err
+ }
+
+ w := multiWriter{
+ writers: writers,
+ writeQuorum: 1,
+ errs: make([]error, len(writers)),
+ }
+
+ if err = w.Write(ctx, bufs); err != nil {
+ return err
+ }
+ }
+
+ return derr
+}
diff --git a/cmd/erasure-decode_test.go b/cmd/erasure-decode_test.go
new file mode 100644
index 00000000..229047e1
--- /dev/null
+++ b/cmd/erasure-decode_test.go
@@ -0,0 +1,385 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ crand "crypto/rand"
+ "io"
+ "math/rand"
+ "testing"
+
+ "github.com/dustin/go-humanize"
+)
+
+func (a badDisk) ReadFile(ctx context.Context, volume string, path string, offset int64, buf []byte, verifier *BitrotVerifier) (n int64, err error) {
+ return 0, errFaultyDisk
+}
+
+var erasureDecodeTests = []struct {
+ dataBlocks int
+ onDisks, offDisks int
+ blocksize, data int64
+ offset int64
+ length int64
+ algorithm BitrotAlgorithm
+ shouldFail, shouldFailQuorum bool
+}{
+ {dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 0
+ {dataBlocks: 3, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false}, // 1
+ {dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 2
+ {dataBlocks: 5, onDisks: 10, offDisks: 0, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 1, length: oneMiByte - 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 3
+ {dataBlocks: 6, onDisks: 12, offDisks: 0, blocksize: int64(oneMiByte), data: oneMiByte, offset: oneMiByte, length: 0, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false},
+ // 4
+ {dataBlocks: 7, onDisks: 14, offDisks: 0, blocksize: int64(oneMiByte), data: oneMiByte, offset: 3, length: 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 5
+ {dataBlocks: 8, onDisks: 16, offDisks: 0, blocksize: int64(oneMiByte), data: oneMiByte, offset: 4, length: 8 * 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 6
+ {dataBlocks: 7, onDisks: 14, offDisks: 7, blocksize: int64(blockSizeV2), data: oneMiByte, offset: oneMiByte, length: 1, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 7
+ {dataBlocks: 6, onDisks: 12, offDisks: 6, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 8
+ {dataBlocks: 5, onDisks: 10, offDisks: 5, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 9
+ {dataBlocks: 4, onDisks: 8, offDisks: 4, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false}, // 10
+ {dataBlocks: 3, onDisks: 6, offDisks: 3, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 11
+ {dataBlocks: 2, onDisks: 4, offDisks: 2, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 12
+ {dataBlocks: 2, onDisks: 4, offDisks: 1, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 13
+ {dataBlocks: 3, onDisks: 6, offDisks: 2, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 14
+ {dataBlocks: 4, onDisks: 8, offDisks: 3, blocksize: int64(2 * oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 15
+ {dataBlocks: 5, onDisks: 10, offDisks: 6, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 16
+ {dataBlocks: 5, onDisks: 10, offDisks: 2, blocksize: int64(blockSizeV2), data: 2 * oneMiByte, offset: oneMiByte, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 17
+ {dataBlocks: 5, onDisks: 10, offDisks: 1, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 18
+ {dataBlocks: 6, onDisks: 12, offDisks: 3, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false},
+ // 19
+ {dataBlocks: 6, onDisks: 12, offDisks: 7, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 20
+ {dataBlocks: 8, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 21
+ {dataBlocks: 8, onDisks: 16, offDisks: 9, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 22
+ {dataBlocks: 8, onDisks: 16, offDisks: 7, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 23
+ {dataBlocks: 2, onDisks: 4, offDisks: 1, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 24
+ {dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 25
+ {dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV2), data: int64(blockSizeV2) + 1, offset: 0, length: int64(blockSizeV2) + 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 26
+ {dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV2), data: int64(2 * blockSizeV2), offset: 12, length: int64(blockSizeV2) + 17, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 27
+ {dataBlocks: 3, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV2), data: int64(2 * blockSizeV2), offset: 1023, length: int64(blockSizeV2) + 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 28
+ {dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(blockSizeV2), data: int64(2 * blockSizeV2), offset: 11, length: int64(blockSizeV2) + 2*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 29
+ {dataBlocks: 6, onDisks: 12, offDisks: 0, blocksize: int64(blockSizeV2), data: int64(2 * blockSizeV2), offset: 512, length: int64(blockSizeV2) + 8*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 30
+ {dataBlocks: 8, onDisks: 16, offDisks: 0, blocksize: int64(blockSizeV2), data: int64(2 * blockSizeV2), offset: int64(blockSizeV2), length: int64(blockSizeV2) - 1, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 31
+ {dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV2), data: int64(oneMiByte), offset: -1, length: 3, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 32
+ {dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV2), data: int64(oneMiByte), offset: 1024, length: -1, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 33
+ {dataBlocks: 4, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV2), data: int64(blockSizeV2), offset: 0, length: int64(blockSizeV2), algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 34
+ {dataBlocks: 4, onDisks: 6, offDisks: 1, blocksize: int64(blockSizeV2), data: int64(2 * blockSizeV2), offset: 12, length: int64(blockSizeV2) + 17, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 35
+ {dataBlocks: 4, onDisks: 6, offDisks: 3, blocksize: int64(blockSizeV2), data: int64(2 * blockSizeV2), offset: 1023, length: int64(blockSizeV2) + 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 36
+ {dataBlocks: 8, onDisks: 12, offDisks: 4, blocksize: int64(blockSizeV2), data: int64(2 * blockSizeV2), offset: 11, length: int64(blockSizeV2) + 2*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 37
+}
+
+func TestErasureDecode(t *testing.T) {
+ for i, test := range erasureDecodeTests {
+ setup, err := newErasureTestSetup(t, test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
+ if err != nil {
+ t.Fatalf("Test %d: failed to create test setup: %v", i, err)
+ }
+ erasure, err := NewErasure(t.Context(), test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
+ if err != nil {
+ t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
+ }
+ disks := setup.disks
+ data := make([]byte, test.data)
+ if _, err = io.ReadFull(crand.Reader, data); err != nil {
+ t.Fatalf("Test %d: failed to generate random test data: %v", i, err)
+ }
+
+ writeAlgorithm := test.algorithm
+ if !test.algorithm.Available() {
+ writeAlgorithm = DefaultBitrotAlgorithm
+ }
+ buffer := make([]byte, test.blocksize, 2*test.blocksize)
+ writers := make([]io.Writer, len(disks))
+ for i, disk := range disks {
+ writers[i] = newBitrotWriter(disk, "", "testbucket", "object", erasure.ShardFileSize(test.data), writeAlgorithm, erasure.ShardSize())
+ }
+ n, err := erasure.Encode(t.Context(), bytes.NewReader(data), writers, buffer, erasure.dataBlocks+1)
+ closeBitrotWriters(writers)
+ if err != nil {
+ t.Fatalf("Test %d: failed to create erasure test file: %v", i, err)
+ }
+ if n != test.data {
+ t.Fatalf("Test %d: failed to create erasure test file", i)
+ }
+ for i, w := range writers {
+ if w == nil {
+ disks[i] = nil
+ }
+ }
+
+ // Get the checksums of the current part.
+ bitrotReaders := make([]io.ReaderAt, len(disks))
+ for index, disk := range disks {
+ if disk == OfflineDisk {
+ continue
+ }
+ tillOffset := erasure.ShardFileOffset(test.offset, test.length, test.data)
+
+ bitrotReaders[index] = newBitrotReader(disk, nil, "testbucket", "object", tillOffset, writeAlgorithm, bitrotWriterSum(writers[index]), erasure.ShardSize())
+ }
+
+ writer := bytes.NewBuffer(nil)
+ _, err = erasure.Decode(t.Context(), writer, bitrotReaders, test.offset, test.length, test.data, nil)
+ closeBitrotReaders(bitrotReaders)
+ if err != nil && !test.shouldFail {
+ t.Errorf("Test %d: should pass but failed with: %v", i, err)
+ }
+ if err == nil && test.shouldFail {
+ t.Errorf("Test %d: should fail but it passed", i)
+ }
+ if err == nil {
+ if content := writer.Bytes(); !bytes.Equal(content, data[test.offset:test.offset+test.length]) {
+ t.Errorf("Test %d: read returns wrong file content.", i)
+ }
+ }
+
+ for i, r := range bitrotReaders {
+ if r == nil {
+ disks[i] = OfflineDisk
+ }
+ }
+ if err == nil && !test.shouldFail {
+ bitrotReaders = make([]io.ReaderAt, len(disks))
+ for index, disk := range disks {
+ if disk == OfflineDisk {
+ continue
+ }
+ tillOffset := erasure.ShardFileOffset(test.offset, test.length, test.data)
+ bitrotReaders[index] = newBitrotReader(disk, nil, "testbucket", "object", tillOffset, writeAlgorithm, bitrotWriterSum(writers[index]), erasure.ShardSize())
+ }
+ for j := range disks[:test.offDisks] {
+ if bitrotReaders[j] == nil {
+ continue
+ }
+ switch r := bitrotReaders[j].(type) {
+ case *wholeBitrotReader:
+ r.disk = badDisk{nil}
+ case *streamingBitrotReader:
+ r.disk = badDisk{nil}
+ }
+ }
+ if test.offDisks > 0 {
+ bitrotReaders[0] = nil
+ }
+ writer.Reset()
+ _, err = erasure.Decode(t.Context(), writer, bitrotReaders, test.offset, test.length, test.data, nil)
+ closeBitrotReaders(bitrotReaders)
+ if err != nil && !test.shouldFailQuorum {
+ t.Errorf("Test %d: should pass but failed with: %v", i, err)
+ }
+ if err == nil && test.shouldFailQuorum {
+ t.Errorf("Test %d: should fail but it passed", i)
+ }
+ if !test.shouldFailQuorum {
+ if content := writer.Bytes(); !bytes.Equal(content, data[test.offset:test.offset+test.length]) {
+ t.Errorf("Test %d: read returns wrong file content", i)
+ }
+ }
+ }
+ }
+}
+
+// Test erasureDecode with random offset and lengths.
+// This test is t.Skip()ed as it a long time to run, hence should be run
+// explicitly after commenting out t.Skip()
+func TestErasureDecodeRandomOffsetLength(t *testing.T) {
+ if testing.Short() {
+ t.Skip()
+ }
+ // Initialize environment needed for the test.
+ dataBlocks := 7
+ parityBlocks := 7
+ blockSize := int64(1 * humanize.MiByte)
+ setup, err := newErasureTestSetup(t, dataBlocks, parityBlocks, blockSize)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ disks := setup.disks
+ erasure, err := NewErasure(t.Context(), dataBlocks, parityBlocks, blockSize)
+ if err != nil {
+ t.Fatalf("failed to create ErasureStorage: %v", err)
+ }
+ // Prepare a slice of 5MiB with random data.
+ data := make([]byte, 5*humanize.MiByte)
+ length := int64(len(data))
+ _, err = rand.Read(data)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ writers := make([]io.Writer, len(disks))
+ for i, disk := range disks {
+ if disk == nil {
+ continue
+ }
+ writers[i] = newBitrotWriter(disk, "", "testbucket", "object", erasure.ShardFileSize(length), DefaultBitrotAlgorithm, erasure.ShardSize())
+ }
+
+ // 10000 iterations with random offsets and lengths.
+ iterations := 10000
+
+ // Create a test file to read from.
+ buffer := make([]byte, blockSize, 2*blockSize)
+ n, err := erasure.Encode(t.Context(), bytes.NewReader(data), writers, buffer, erasure.dataBlocks+1)
+ closeBitrotWriters(writers)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if n != length {
+ t.Errorf("erasureCreateFile returned %d, expected %d", n, length)
+ }
+
+ // To generate random offset/length.
+ r := rand.New(rand.NewSource(UTCNow().UnixNano()))
+
+ buf := &bytes.Buffer{}
+
+ // Verify erasure.Decode() for random offsets and lengths.
+ for range iterations {
+ offset := r.Int63n(length)
+ readLen := r.Int63n(length - offset)
+
+ expected := data[offset : offset+readLen]
+
+ // Get the checksums of the current part.
+ bitrotReaders := make([]io.ReaderAt, len(disks))
+ for index, disk := range disks {
+ if disk == OfflineDisk {
+ continue
+ }
+ tillOffset := erasure.ShardFileOffset(offset, readLen, length)
+ bitrotReaders[index] = newStreamingBitrotReader(disk, nil, "testbucket", "object", tillOffset, DefaultBitrotAlgorithm, erasure.ShardSize())
+ }
+ _, err = erasure.Decode(t.Context(), buf, bitrotReaders, offset, readLen, length, nil)
+ closeBitrotReaders(bitrotReaders)
+ if err != nil {
+ t.Fatal(err, offset, readLen)
+ }
+ got := buf.Bytes()
+ if !bytes.Equal(expected, got) {
+ t.Fatalf("read data is different from what was expected, offset=%d length=%d", offset, readLen)
+ }
+ buf.Reset()
+ }
+}
+
+// Benchmarks
+
+func benchmarkErasureDecode(data, parity, dataDown, parityDown int, size int64, b *testing.B) {
+ setup, err := newErasureTestSetup(b, data, parity, blockSizeV2)
+ if err != nil {
+ b.Fatalf("failed to create test setup: %v", err)
+ }
+ disks := setup.disks
+ erasure, err := NewErasure(context.Background(), data, parity, blockSizeV2)
+ if err != nil {
+ b.Fatalf("failed to create ErasureStorage: %v", err)
+ }
+
+ writers := make([]io.Writer, len(disks))
+ for i, disk := range disks {
+ if disk == nil {
+ continue
+ }
+ writers[i] = newBitrotWriter(disk, "", "testbucket", "object", erasure.ShardFileSize(size), DefaultBitrotAlgorithm, erasure.ShardSize())
+ }
+
+ content := make([]byte, size)
+ buffer := make([]byte, blockSizeV2, 2*blockSizeV2)
+ _, err = erasure.Encode(context.Background(), bytes.NewReader(content), writers, buffer, erasure.dataBlocks+1)
+ closeBitrotWriters(writers)
+ if err != nil {
+ b.Fatalf("failed to create erasure test file: %v", err)
+ }
+
+ for i := range dataDown {
+ writers[i] = nil
+ }
+ for i := data; i < data+parityDown; i++ {
+ writers[i] = nil
+ }
+
+ b.SetBytes(size)
+ b.ReportAllocs()
+ for b.Loop() {
+ bitrotReaders := make([]io.ReaderAt, len(disks))
+ for index, disk := range disks {
+ if writers[index] == nil {
+ continue
+ }
+ tillOffset := erasure.ShardFileOffset(0, size, size)
+ bitrotReaders[index] = newStreamingBitrotReader(disk, nil, "testbucket", "object", tillOffset, DefaultBitrotAlgorithm, erasure.ShardSize())
+ }
+ if _, err = erasure.Decode(context.Background(), bytes.NewBuffer(content[:0]), bitrotReaders, 0, size, size, nil); err != nil {
+ panic(err)
+ }
+ closeBitrotReaders(bitrotReaders)
+ }
+}
+
+func BenchmarkErasureDecodeQuick(b *testing.B) {
+ const size = 12 * 1024 * 1024
+ b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureDecode(2, 2, 0, 0, size, b) })
+ b.Run(" 00|X0 ", func(b *testing.B) { benchmarkErasureDecode(2, 2, 0, 1, size, b) })
+ b.Run(" X0|00 ", func(b *testing.B) { benchmarkErasureDecode(2, 2, 1, 0, size, b) })
+ b.Run(" X0|X0 ", func(b *testing.B) { benchmarkErasureDecode(2, 2, 1, 1, size, b) })
+}
+
+func BenchmarkErasureDecode_4_64KB(b *testing.B) {
+ const size = 64 * 1024
+ b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureDecode(2, 2, 0, 0, size, b) })
+ b.Run(" 00|X0 ", func(b *testing.B) { benchmarkErasureDecode(2, 2, 0, 1, size, b) })
+ b.Run(" X0|00 ", func(b *testing.B) { benchmarkErasureDecode(2, 2, 1, 0, size, b) })
+ b.Run(" X0|X0 ", func(b *testing.B) { benchmarkErasureDecode(2, 2, 1, 1, size, b) })
+ b.Run(" 00|XX ", func(b *testing.B) { benchmarkErasureDecode(2, 2, 0, 2, size, b) })
+ b.Run(" XX|00 ", func(b *testing.B) { benchmarkErasureDecode(2, 2, 2, 0, size, b) })
+}
+
+func BenchmarkErasureDecode_8_20MB(b *testing.B) {
+ const size = 20 * 1024 * 1024
+ b.Run(" 0000|0000 ", func(b *testing.B) { benchmarkErasureDecode(4, 4, 0, 0, size, b) })
+ b.Run(" 0000|X000 ", func(b *testing.B) { benchmarkErasureDecode(4, 4, 0, 1, size, b) })
+ b.Run(" X000|0000 ", func(b *testing.B) { benchmarkErasureDecode(4, 4, 1, 0, size, b) })
+ b.Run(" X000|X000 ", func(b *testing.B) { benchmarkErasureDecode(4, 4, 1, 1, size, b) })
+ b.Run(" 0000|XXXX ", func(b *testing.B) { benchmarkErasureDecode(4, 4, 0, 4, size, b) })
+ b.Run(" XX00|XX00 ", func(b *testing.B) { benchmarkErasureDecode(4, 4, 2, 2, size, b) })
+ b.Run(" XXXX|0000 ", func(b *testing.B) { benchmarkErasureDecode(4, 4, 4, 0, size, b) })
+}
+
+func BenchmarkErasureDecode_12_30MB(b *testing.B) {
+ const size = 30 * 1024 * 1024
+ b.Run(" 000000|000000 ", func(b *testing.B) { benchmarkErasureDecode(6, 6, 0, 0, size, b) })
+ b.Run(" 000000|X00000 ", func(b *testing.B) { benchmarkErasureDecode(6, 6, 0, 1, size, b) })
+ b.Run(" X00000|000000 ", func(b *testing.B) { benchmarkErasureDecode(6, 6, 1, 0, size, b) })
+ b.Run(" X00000|X00000 ", func(b *testing.B) { benchmarkErasureDecode(6, 6, 1, 1, size, b) })
+ b.Run(" 000000|XXXXXX ", func(b *testing.B) { benchmarkErasureDecode(6, 6, 0, 6, size, b) })
+ b.Run(" XXX000|XXX000 ", func(b *testing.B) { benchmarkErasureDecode(6, 6, 3, 3, size, b) })
+ b.Run(" XXXXXX|000000 ", func(b *testing.B) { benchmarkErasureDecode(6, 6, 6, 0, size, b) })
+}
+
+func BenchmarkErasureDecode_16_40MB(b *testing.B) {
+ const size = 40 * 1024 * 1024
+ b.Run(" 00000000|00000000 ", func(b *testing.B) { benchmarkErasureDecode(8, 8, 0, 0, size, b) })
+ b.Run(" 00000000|X0000000 ", func(b *testing.B) { benchmarkErasureDecode(8, 8, 0, 1, size, b) })
+ b.Run(" X0000000|00000000 ", func(b *testing.B) { benchmarkErasureDecode(8, 8, 1, 0, size, b) })
+ b.Run(" X0000000|X0000000 ", func(b *testing.B) { benchmarkErasureDecode(8, 8, 1, 1, size, b) })
+ b.Run(" 00000000|XXXXXXXX ", func(b *testing.B) { benchmarkErasureDecode(8, 8, 0, 8, size, b) })
+ b.Run(" XXXX0000|XXXX0000 ", func(b *testing.B) { benchmarkErasureDecode(8, 8, 4, 4, size, b) })
+ b.Run(" XXXXXXXX|00000000 ", func(b *testing.B) { benchmarkErasureDecode(8, 8, 8, 0, size, b) })
+}
diff --git a/cmd/erasure-encode.go b/cmd/erasure-encode.go
new file mode 100644
index 00000000..215ac172
--- /dev/null
+++ b/cmd/erasure-encode.go
@@ -0,0 +1,110 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "io"
+)
+
+// Writes to multiple writers
+type multiWriter struct {
+ writers []io.Writer
+ writeQuorum int
+ errs []error
+}
+
+// Write writes data to writers.
+func (p *multiWriter) Write(ctx context.Context, blocks [][]byte) error {
+ for i := range p.writers {
+ if p.errs[i] != nil {
+ continue
+ }
+ if p.writers[i] == nil {
+ p.errs[i] = errDiskNotFound
+ continue
+ }
+ var n int
+ n, p.errs[i] = p.writers[i].Write(blocks[i])
+ if p.errs[i] == nil {
+ if n != len(blocks[i]) {
+ p.errs[i] = io.ErrShortWrite
+ p.writers[i] = nil
+ }
+ } else {
+ p.writers[i] = nil
+ }
+ }
+
+ // If nilCount >= p.writeQuorum, we return nil. This is because HealFile() uses
+ // CreateFile with p.writeQuorum=1 to accommodate healing of single disk.
+ // i.e if we do no return here in such a case, reduceWriteQuorumErrs() would
+ // return a quorum error to HealFile().
+ nilCount := countErrs(p.errs, nil)
+ if nilCount >= p.writeQuorum {
+ return nil
+ }
+
+ writeErr := reduceWriteQuorumErrs(ctx, p.errs, objectOpIgnoredErrs, p.writeQuorum)
+ return fmt.Errorf("%w (offline-disks=%d/%d)", writeErr, countErrs(p.errs, errDiskNotFound), len(p.writers))
+}
+
+// Encode reads from the reader, erasure-encodes the data and writes to the writers.
+func (e *Erasure) Encode(ctx context.Context, src io.Reader, writers []io.Writer, buf []byte, quorum int) (total int64, err error) {
+ writer := &multiWriter{
+ writers: writers,
+ writeQuorum: quorum,
+ errs: make([]error, len(writers)),
+ }
+
+ for {
+ var blocks [][]byte
+ n, err := io.ReadFull(src, buf)
+ if err != nil {
+ if !IsErrIgnored(err, []error{
+ io.EOF,
+ io.ErrUnexpectedEOF,
+ }...) {
+ return 0, err
+ }
+ }
+
+ eof := err == io.EOF || err == io.ErrUnexpectedEOF
+ if n == 0 && total != 0 {
+ // Reached EOF, nothing more to be done.
+ break
+ }
+
+ // We take care of the situation where if n == 0 and total == 0 by creating empty data and parity files.
+ blocks, err = e.EncodeData(ctx, buf[:n])
+ if err != nil {
+ return 0, err
+ }
+
+ if err = writer.Write(ctx, blocks); err != nil {
+ return 0, err
+ }
+
+ total += int64(n)
+ if eof {
+ break
+ }
+ }
+ return total, nil
+}
diff --git a/cmd/erasure-encode_test.go b/cmd/erasure-encode_test.go
new file mode 100644
index 00000000..54a37b71
--- /dev/null
+++ b/cmd/erasure-encode_test.go
@@ -0,0 +1,243 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "crypto/rand"
+ "io"
+ "testing"
+
+ "github.com/dustin/go-humanize"
+)
+
+type badDisk struct{ StorageAPI }
+
+func (a badDisk) String() string {
+ return "bad-disk"
+}
+
+func (a badDisk) AppendFile(ctx context.Context, volume string, path string, buf []byte) error {
+ return errFaultyDisk
+}
+
+func (a badDisk) ReadFileStream(ctx context.Context, volume, path string, offset, length int64) (io.ReadCloser, error) {
+ return nil, errFaultyDisk
+}
+
+func (a badDisk) CreateFile(ctx context.Context, origvolume, volume, path string, size int64, reader io.Reader) error {
+ return errFaultyDisk
+}
+
+func (badDisk) Hostname() string {
+ return ""
+}
+
+const oneMiByte = 1 * humanize.MiByte
+
+var erasureEncodeTests = []struct {
+ dataBlocks int
+ onDisks, offDisks int
+ blocksize, data int64
+ offset int
+ algorithm BitrotAlgorithm
+ shouldFail, shouldFailQuorum bool
+}{
+ {dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 0
+ {dataBlocks: 3, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 1, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false}, // 1
+ {dataBlocks: 4, onDisks: 8, offDisks: 2, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 2
+ {dataBlocks: 5, onDisks: 10, offDisks: 3, blocksize: int64(blockSizeV2), data: oneMiByte, offset: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 3
+ {dataBlocks: 6, onDisks: 12, offDisks: 4, blocksize: int64(blockSizeV2), data: oneMiByte, offset: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 4
+ {dataBlocks: 7, onDisks: 14, offDisks: 5, blocksize: int64(blockSizeV2), data: 0, offset: 0, shouldFail: false, algorithm: SHA256, shouldFailQuorum: false}, // 5
+ {dataBlocks: 8, onDisks: 16, offDisks: 7, blocksize: int64(blockSizeV2), data: 0, offset: 0, shouldFail: false, algorithm: DefaultBitrotAlgorithm, shouldFailQuorum: false}, // 6
+ {dataBlocks: 2, onDisks: 4, offDisks: 2, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: true}, // 7
+ {dataBlocks: 4, onDisks: 8, offDisks: 4, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, algorithm: SHA256, shouldFail: false, shouldFailQuorum: true}, // 8
+ {dataBlocks: 7, onDisks: 14, offDisks: 7, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 9
+ {dataBlocks: 8, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 10
+ {dataBlocks: 5, onDisks: 10, offDisks: 3, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 11
+ {dataBlocks: 3, onDisks: 6, offDisks: 1, blocksize: int64(blockSizeV2), data: oneMiByte, offset: oneMiByte / 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 12
+ {dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(oneMiByte / 2), data: oneMiByte, offset: oneMiByte/2 + 1, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 13
+ {dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(oneMiByte - 1), data: oneMiByte, offset: oneMiByte - 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 14
+ {dataBlocks: 8, onDisks: 12, offDisks: 2, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 15
+ {dataBlocks: 8, onDisks: 10, offDisks: 1, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 16
+ {dataBlocks: 10, onDisks: 14, offDisks: 0, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 17, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 17
+ {dataBlocks: 2, onDisks: 6, offDisks: 2, blocksize: int64(oneMiByte), data: oneMiByte, offset: oneMiByte / 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 18
+ {dataBlocks: 10, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV2), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 19
+}
+
+func TestErasureEncode(t *testing.T) {
+ for i, test := range erasureEncodeTests {
+ setup, err := newErasureTestSetup(t, test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
+ if err != nil {
+ t.Fatalf("Test %d: failed to create test setup: %v", i, err)
+ }
+ disks := setup.disks
+ erasure, err := NewErasure(t.Context(), test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
+ if err != nil {
+ t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
+ }
+ buffer := make([]byte, test.blocksize, 2*test.blocksize)
+
+ data := make([]byte, test.data)
+ if _, err = io.ReadFull(rand.Reader, data); err != nil {
+ t.Fatalf("Test %d: failed to generate random test data: %v", i, err)
+ }
+ writers := make([]io.Writer, len(disks))
+ for i, disk := range disks {
+ if disk == OfflineDisk {
+ continue
+ }
+ writers[i] = newBitrotWriter(disk, "", "testbucket", "object", erasure.ShardFileSize(int64(len(data[test.offset:]))), test.algorithm, erasure.ShardSize())
+ }
+ n, err := erasure.Encode(t.Context(), bytes.NewReader(data[test.offset:]), writers, buffer, erasure.dataBlocks+1)
+ closeBitrotWriters(writers)
+ if err != nil && !test.shouldFail {
+ t.Errorf("Test %d: should pass but failed with: %v", i, err)
+ }
+ if err == nil && test.shouldFail {
+ t.Errorf("Test %d: should fail but it passed", i)
+ }
+ for i, w := range writers {
+ if w == nil {
+ disks[i] = OfflineDisk
+ }
+ }
+ if err == nil {
+ if length := int64(len(data[test.offset:])); n != length {
+ t.Errorf("Test %d: invalid number of bytes written: got: #%d want #%d", i, n, length)
+ }
+ writers := make([]io.Writer, len(disks))
+ for i, disk := range disks {
+ if disk == nil {
+ continue
+ }
+ writers[i] = newBitrotWriter(disk, "", "testbucket", "object2", erasure.ShardFileSize(int64(len(data[test.offset:]))), test.algorithm, erasure.ShardSize())
+ }
+ for j := range disks[:test.offDisks] {
+ switch w := writers[j].(type) {
+ case *wholeBitrotWriter:
+ w.disk = badDisk{nil}
+ case *streamingBitrotWriter:
+ w.closeWithErr(errFaultyDisk)
+ }
+ }
+ if test.offDisks > 0 {
+ writers[0] = nil
+ }
+ n, err = erasure.Encode(t.Context(), bytes.NewReader(data[test.offset:]), writers, buffer, erasure.dataBlocks+1)
+ closeBitrotWriters(writers)
+ if err != nil && !test.shouldFailQuorum {
+ t.Errorf("Test %d: should pass but failed with: %v", i, err)
+ }
+ if err == nil && test.shouldFailQuorum {
+ t.Errorf("Test %d: should fail but it passed", i)
+ }
+ if err == nil {
+ if length := int64(len(data[test.offset:])); n != length {
+ t.Errorf("Test %d: invalid number of bytes written: got: #%d want #%d", i, n, length)
+ }
+ }
+ }
+ }
+}
+
+// Benchmarks
+
+func benchmarkErasureEncode(data, parity, dataDown, parityDown int, size int64, b *testing.B) {
+ setup, err := newErasureTestSetup(b, data, parity, blockSizeV2)
+ if err != nil {
+ b.Fatalf("failed to create test setup: %v", err)
+ }
+ erasure, err := NewErasure(context.Background(), data, parity, blockSizeV2)
+ if err != nil {
+ b.Fatalf("failed to create ErasureStorage: %v", err)
+ }
+ disks := setup.disks
+ buffer := make([]byte, blockSizeV2, 2*blockSizeV2)
+ content := make([]byte, size)
+
+ for i := range dataDown {
+ disks[i] = OfflineDisk
+ }
+ for i := data; i < data+parityDown; i++ {
+ disks[i] = OfflineDisk
+ }
+
+ b.SetBytes(size)
+ b.ReportAllocs()
+ for b.Loop() {
+ writers := make([]io.Writer, len(disks))
+ for i, disk := range disks {
+ if disk == OfflineDisk {
+ continue
+ }
+ disk.Delete(context.Background(), "testbucket", "object", DeleteOptions{
+ Recursive: false,
+ Immediate: false,
+ })
+ writers[i] = newBitrotWriter(disk, "", "testbucket", "object", erasure.ShardFileSize(size), DefaultBitrotAlgorithm, erasure.ShardSize())
+ }
+ _, err := erasure.Encode(context.Background(), bytes.NewReader(content), writers, buffer, erasure.dataBlocks+1)
+ closeBitrotWriters(writers)
+ if err != nil {
+ panic(err)
+ }
+ }
+}
+
+func BenchmarkErasureEncodeQuick(b *testing.B) {
+ const size = 12 * 1024 * 1024
+ b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureEncode(2, 2, 0, 0, size, b) })
+ b.Run(" 00|X0 ", func(b *testing.B) { benchmarkErasureEncode(2, 2, 0, 1, size, b) })
+ b.Run(" X0|00 ", func(b *testing.B) { benchmarkErasureEncode(2, 2, 1, 0, size, b) })
+}
+
+func BenchmarkErasureEncode_4_64KB(b *testing.B) {
+ const size = 64 * 1024
+ b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureEncode(2, 2, 0, 0, size, b) })
+ b.Run(" 00|X0 ", func(b *testing.B) { benchmarkErasureEncode(2, 2, 0, 1, size, b) })
+ b.Run(" X0|00 ", func(b *testing.B) { benchmarkErasureEncode(2, 2, 1, 0, size, b) })
+}
+
+func BenchmarkErasureEncode_8_20MB(b *testing.B) {
+ const size = 20 * 1024 * 1024
+ b.Run(" 0000|0000 ", func(b *testing.B) { benchmarkErasureEncode(4, 4, 0, 0, size, b) })
+ b.Run(" 0000|X000 ", func(b *testing.B) { benchmarkErasureEncode(4, 4, 0, 1, size, b) })
+ b.Run(" X000|0000 ", func(b *testing.B) { benchmarkErasureEncode(4, 4, 1, 0, size, b) })
+ b.Run(" 0000|XXX0 ", func(b *testing.B) { benchmarkErasureEncode(4, 4, 0, 3, size, b) })
+ b.Run(" XXX0|0000 ", func(b *testing.B) { benchmarkErasureEncode(4, 4, 3, 0, size, b) })
+}
+
+func BenchmarkErasureEncode_12_30MB(b *testing.B) {
+ const size = 30 * 1024 * 1024
+ b.Run(" 000000|000000 ", func(b *testing.B) { benchmarkErasureEncode(6, 6, 0, 0, size, b) })
+ b.Run(" 000000|X00000 ", func(b *testing.B) { benchmarkErasureEncode(6, 6, 0, 1, size, b) })
+ b.Run(" X00000|000000 ", func(b *testing.B) { benchmarkErasureEncode(6, 6, 1, 0, size, b) })
+ b.Run(" 000000|XXXXX0 ", func(b *testing.B) { benchmarkErasureEncode(6, 6, 0, 5, size, b) })
+ b.Run(" XXXXX0|000000 ", func(b *testing.B) { benchmarkErasureEncode(6, 6, 5, 0, size, b) })
+}
+
+func BenchmarkErasureEncode_16_40MB(b *testing.B) {
+ const size = 40 * 1024 * 1024
+ b.Run(" 00000000|00000000 ", func(b *testing.B) { benchmarkErasureEncode(8, 8, 0, 0, size, b) })
+ b.Run(" 00000000|X0000000 ", func(b *testing.B) { benchmarkErasureEncode(8, 8, 0, 1, size, b) })
+ b.Run(" X0000000|00000000 ", func(b *testing.B) { benchmarkErasureEncode(8, 8, 1, 0, size, b) })
+ b.Run(" 00000000|XXXXXXX0 ", func(b *testing.B) { benchmarkErasureEncode(8, 8, 0, 7, size, b) })
+ b.Run(" XXXXXXX0|00000000 ", func(b *testing.B) { benchmarkErasureEncode(8, 8, 7, 0, size, b) })
+}
diff --git a/cmd/erasure-errors.go b/cmd/erasure-errors.go
new file mode 100644
index 00000000..a54f72ba
--- /dev/null
+++ b/cmd/erasure-errors.go
@@ -0,0 +1,29 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import "errors"
+
+// errErasureReadQuorum - did not meet read quorum.
+var errErasureReadQuorum = errors.New("Read failed. Insufficient number of drives online")
+
+// errErasureWriteQuorum - did not meet write quorum.
+var errErasureWriteQuorum = errors.New("Write failed. Insufficient number of drives online")
+
+// errNoHealRequired - returned when healing is attempted on a previously healed disks.
+var errNoHealRequired = errors.New("No healing is required")
diff --git a/cmd/erasure-heal_test.go b/cmd/erasure-heal_test.go
new file mode 100644
index 00000000..cb6b25fd
--- /dev/null
+++ b/cmd/erasure-heal_test.go
@@ -0,0 +1,156 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "crypto/rand"
+ "io"
+ "os"
+ "testing"
+)
+
+var erasureHealTests = []struct {
+ dataBlocks, disks int
+
+ // number of offline disks is also number of staleDisks for
+ // erasure reconstruction in this test
+ offDisks int
+
+ // bad disks are online disks which return errors
+ badDisks, badStaleDisks int
+
+ blocksize, size int64
+ algorithm BitrotAlgorithm
+ shouldFail bool
+}{
+ {dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: SHA256, shouldFail: false}, // 0
+ {dataBlocks: 3, disks: 6, offDisks: 2, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 1
+ {dataBlocks: 4, disks: 8, offDisks: 2, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 2
+ {dataBlocks: 5, disks: 10, offDisks: 3, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 3
+ {dataBlocks: 6, disks: 12, offDisks: 2, badDisks: 3, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: SHA256, shouldFail: false}, // 4
+ {dataBlocks: 7, disks: 14, offDisks: 4, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 5
+ {dataBlocks: 8, disks: 16, offDisks: 6, badDisks: 1, badStaleDisks: 1, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 6
+ {dataBlocks: 7, disks: 14, offDisks: 2, badDisks: 3, badStaleDisks: 0, blocksize: int64(oneMiByte / 2), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 7
+ {dataBlocks: 6, disks: 12, offDisks: 1, badDisks: 0, badStaleDisks: 1, blocksize: int64(oneMiByte - 1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: true}, // 8
+ {dataBlocks: 5, disks: 10, offDisks: 3, badDisks: 0, badStaleDisks: 3, blocksize: int64(oneMiByte / 2), size: oneMiByte, algorithm: SHA256, shouldFail: true}, // 9
+ {dataBlocks: 4, disks: 8, offDisks: 1, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 10
+ {dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 1, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: true}, // 11
+ {dataBlocks: 6, disks: 12, offDisks: 8, badDisks: 3, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: true}, // 12
+ {dataBlocks: 7, disks: 14, offDisks: 3, badDisks: 4, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 13
+ {dataBlocks: 7, disks: 14, offDisks: 6, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 14
+ {dataBlocks: 8, disks: 16, offDisks: 4, badDisks: 5, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: true}, // 15
+ {dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 16
+ {dataBlocks: 12, disks: 16, offDisks: 2, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 17
+ {dataBlocks: 6, disks: 8, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 18
+ {dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV2), size: oneMiByte * 64, algorithm: SHA256, shouldFail: false}, // 19
+}
+
+func TestErasureHeal(t *testing.T) {
+ for i, test := range erasureHealTests {
+ if test.offDisks < test.badStaleDisks {
+ // test case sanity check
+ t.Fatalf("Test %d: Bad test case - number of stale drives cannot be less than number of badstale drives", i)
+ }
+
+ // create some test data
+ setup, err := newErasureTestSetup(t, test.dataBlocks, test.disks-test.dataBlocks, test.blocksize)
+ if err != nil {
+ t.Fatalf("Test %d: failed to setup Erasure environment: %v", i, err)
+ }
+ disks := setup.disks
+ erasure, err := NewErasure(t.Context(), test.dataBlocks, test.disks-test.dataBlocks, test.blocksize)
+ if err != nil {
+ t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
+ }
+ data := make([]byte, test.size)
+ if _, err = io.ReadFull(rand.Reader, data); err != nil {
+ t.Fatalf("Test %d: failed to create random test data: %v", i, err)
+ }
+ buffer := make([]byte, test.blocksize, 2*test.blocksize)
+ writers := make([]io.Writer, len(disks))
+ for i, disk := range disks {
+ writers[i] = newBitrotWriter(disk, "", "testbucket", "testobject", erasure.ShardFileSize(test.size), test.algorithm, erasure.ShardSize())
+ }
+ _, err = erasure.Encode(t.Context(), bytes.NewReader(data), writers, buffer, erasure.dataBlocks+1)
+ closeBitrotWriters(writers)
+ if err != nil {
+ t.Fatalf("Test %d: failed to create random test data: %v", i, err)
+ }
+
+ readers := make([]io.ReaderAt, len(disks))
+ for i, disk := range disks {
+ shardFilesize := erasure.ShardFileSize(test.size)
+ readers[i] = newBitrotReader(disk, nil, "testbucket", "testobject", shardFilesize, test.algorithm, bitrotWriterSum(writers[i]), erasure.ShardSize())
+ }
+
+ // setup stale disks for the test case
+ staleDisks := make([]StorageAPI, len(disks))
+ copy(staleDisks, disks)
+ for j := range staleDisks {
+ if j < test.offDisks {
+ readers[j] = nil
+ } else {
+ staleDisks[j] = nil
+ }
+ }
+ for j := 0; j < test.badDisks; j++ {
+ switch r := readers[test.offDisks+j].(type) {
+ case *streamingBitrotReader:
+ r.disk = badDisk{nil}
+ case *wholeBitrotReader:
+ r.disk = badDisk{nil}
+ }
+ }
+ for j := 0; j < test.badStaleDisks; j++ {
+ staleDisks[j] = badDisk{nil}
+ }
+
+ staleWriters := make([]io.Writer, len(staleDisks))
+ for i, disk := range staleDisks {
+ if disk == nil {
+ continue
+ }
+ os.Remove(pathJoin(disk.String(), "testbucket", "testobject"))
+ staleWriters[i] = newBitrotWriter(disk, "", "testbucket", "testobject", erasure.ShardFileSize(test.size), test.algorithm, erasure.ShardSize())
+ }
+
+ // test case setup is complete - now call Heal()
+ err = erasure.Heal(t.Context(), staleWriters, readers, test.size, nil)
+ closeBitrotReaders(readers)
+ closeBitrotWriters(staleWriters)
+ if err != nil && !test.shouldFail {
+ t.Errorf("Test %d: should pass but it failed with: %v", i, err)
+ }
+ if err == nil && test.shouldFail {
+ t.Errorf("Test %d: should fail but it passed", i)
+ }
+ if err == nil {
+ // Verify that checksums of staleDisks
+ // match expected values
+ for i := range staleWriters {
+ if staleWriters[i] == nil {
+ continue
+ }
+ if !bytes.Equal(bitrotWriterSum(staleWriters[i]), bitrotWriterSum(writers[i])) {
+ t.Errorf("Test %d: heal returned different bitrot checksums", i)
+ }
+ }
+ }
+ }
+}
diff --git a/cmd/erasure-healing-common.go b/cmd/erasure-healing-common.go
new file mode 100644
index 00000000..0bde3107
--- /dev/null
+++ b/cmd/erasure-healing-common.go
@@ -0,0 +1,440 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "slices"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+)
+
+func commonETags(etags []string) (etag string, maxima int) {
+ etagOccurrenceMap := make(map[string]int, len(etags))
+
+ // Ignore the uuid sentinel and count the rest.
+ for _, etag := range etags {
+ if etag == "" {
+ continue
+ }
+ etagOccurrenceMap[etag]++
+ }
+
+ maxima = 0 // Counter for remembering max occurrence of elements.
+ latest := ""
+
+ // Find the common cardinality from previously collected
+ // occurrences of elements.
+ for etag, count := range etagOccurrenceMap {
+ if count < maxima {
+ continue
+ }
+
+ // We are at or above maxima
+ if count > maxima {
+ maxima = count
+ latest = etag
+ }
+ }
+
+ // Return the collected common max time, with maxima
+ return latest, maxima
+}
+
+// commonTime returns a maximally occurring time from a list of time.
+func commonTimeAndOccurrence(times []time.Time, group time.Duration) (maxTime time.Time, maxima int) {
+ timeOccurrenceMap := make(map[int64]int, len(times))
+ groupNano := group.Nanoseconds()
+ // Ignore the uuid sentinel and count the rest.
+ for _, t := range times {
+ if t.Equal(timeSentinel) || t.IsZero() {
+ continue
+ }
+ nano := t.UnixNano()
+ if group > 0 {
+ for k := range timeOccurrenceMap {
+ if k == nano {
+ // We add to ourself later
+ continue
+ }
+ diff := k - nano
+ if diff < 0 {
+ diff = -diff
+ }
+ // We are within the limit
+ if diff < groupNano {
+ timeOccurrenceMap[k]++
+ }
+ }
+ }
+ // Add ourself...
+ timeOccurrenceMap[nano]++
+ }
+
+ maxima = 0 // Counter for remembering max occurrence of elements.
+ latest := int64(0)
+
+ // Find the common cardinality from previously collected
+ // occurrences of elements.
+ for nano, count := range timeOccurrenceMap {
+ if count < maxima {
+ continue
+ }
+
+ // We are at or above maxima
+ if count > maxima || nano > latest {
+ maxima = count
+ latest = nano
+ }
+ }
+
+ // Return the collected common max time, with maxima
+ return time.Unix(0, latest).UTC(), maxima
+}
+
+// commonTime returns a maximally occurring time from a list of time if it
+// occurs >= quorum, else return timeSentinel
+func commonTime(modTimes []time.Time, quorum int) time.Time {
+ if modTime, count := commonTimeAndOccurrence(modTimes, 0); count >= quorum {
+ return modTime
+ }
+
+ return timeSentinel
+}
+
+func commonETag(etags []string, quorum int) string {
+ if etag, count := commonETags(etags); count >= quorum {
+ return etag
+ }
+ return ""
+}
+
+// Beginning of unix time is treated as sentinel value here.
+var (
+ timeSentinel = time.Unix(0, 0).UTC()
+ timeSentinel1970 = time.Unix(0, 1).UTC() // 1970 used for special cases when xlmeta.version == 0
+)
+
+// Boot modTimes up to disk count, setting the value to time sentinel.
+func bootModtimes(diskCount int) []time.Time {
+ modTimes := make([]time.Time, diskCount)
+ // Boots up all the modtimes.
+ for i := range modTimes {
+ modTimes[i] = timeSentinel
+ }
+ return modTimes
+}
+
+func listObjectETags(partsMetadata []FileInfo, errs []error, quorum int) (etags []string) {
+ etags = make([]string, len(partsMetadata))
+ vidMap := map[string]int{}
+ for index, metadata := range partsMetadata {
+ if errs[index] != nil {
+ continue
+ }
+ vid := metadata.VersionID
+ if metadata.VersionID == "" {
+ vid = nullVersionID
+ }
+ vidMap[vid]++
+ etags[index] = metadata.Metadata["etag"]
+ }
+
+ for _, count := range vidMap {
+ // do we have enough common versions
+ // that have enough quorum to satisfy
+ // the etag.
+ if count >= quorum {
+ return etags
+ }
+ }
+
+ return make([]string, len(partsMetadata))
+}
+
+// Extracts list of times from FileInfo slice and returns, skips
+// slice elements which have errors.
+func listObjectModtimes(partsMetadata []FileInfo, errs []error) (modTimes []time.Time) {
+ modTimes = bootModtimes(len(partsMetadata))
+ for index, metadata := range partsMetadata {
+ if errs[index] != nil {
+ continue
+ }
+ // Once the file is found, save the uuid saved on disk.
+ modTimes[index] = metadata.ModTime
+ }
+ return modTimes
+}
+
+func filterOnlineDisksInplace(fi FileInfo, partsMetadata []FileInfo, onlineDisks []StorageAPI) {
+ for i, meta := range partsMetadata {
+ if fi.XLV1 == meta.XLV1 {
+ continue
+ }
+ onlineDisks[i] = nil
+ }
+}
+
+// Notes:
+// There are 5 possible states a disk could be in,
+// 1. __online__ - has the latest copy of xl.meta - returned by listOnlineDisks
+//
+// 2. __offline__ - err == errDiskNotFound
+//
+// 3. __availableWithParts__ - has the latest copy of xl.meta and has all
+// parts with checksums matching; returned by disksWithAllParts
+//
+// 4. __outdated__ - returned by outDatedDisk, provided []StorageAPI
+// returned by diskWithAllParts is passed for latestDisks.
+// - has an old copy of xl.meta
+// - doesn't have xl.meta (errFileNotFound)
+// - has the latest xl.meta but one or more parts are corrupt
+//
+// 5. __missingParts__ - has the latest copy of xl.meta but has some parts
+// missing. This is identified separately since this may need manual
+// inspection to understand the root cause. E.g, this could be due to
+// backend filesystem corruption.
+
+// listOnlineDisks - returns
+// - a slice of disks where disk having 'older' xl.meta (or nothing)
+// are set to nil.
+// - latest (in time) of the maximally occurring modTime(s), which has at least quorum occurrences.
+func listOnlineDisks(disks []StorageAPI, partsMetadata []FileInfo, errs []error, quorum int) (onlineDisks []StorageAPI, modTime time.Time, etag string) {
+ onlineDisks = make([]StorageAPI, len(disks))
+
+ // List all the file commit ids from parts metadata.
+ modTimes := listObjectModtimes(partsMetadata, errs)
+
+ // Reduce list of UUIDs to a single common value.
+ modTime = commonTime(modTimes, quorum)
+
+ if modTime.IsZero() || modTime.Equal(timeSentinel) {
+ etags := listObjectETags(partsMetadata, errs, quorum)
+
+ etag = commonETag(etags, quorum)
+
+ if etag != "" { // allow this fallback only if a non-empty etag is found.
+ for index, e := range etags {
+ if partsMetadata[index].IsValid() && e == etag {
+ onlineDisks[index] = disks[index]
+ } else {
+ onlineDisks[index] = nil
+ }
+ }
+ return onlineDisks, modTime, etag
+ }
+ }
+
+ // Create a new online disks slice, which have common uuid.
+ for index, t := range modTimes {
+ if partsMetadata[index].IsValid() && t.Equal(modTime) {
+ onlineDisks[index] = disks[index]
+ } else {
+ onlineDisks[index] = nil
+ }
+ }
+
+ return onlineDisks, modTime, ""
+}
+
+// Convert verify or check parts returned error to integer representation
+func convPartErrToInt(err error) int {
+ err = unwrapAll(err)
+ switch err {
+ case nil:
+ return checkPartSuccess
+ case errFileNotFound, errFileVersionNotFound:
+ return checkPartFileNotFound
+ case errFileCorrupt:
+ return checkPartFileCorrupt
+ case errVolumeNotFound:
+ return checkPartVolumeNotFound
+ case errDiskNotFound:
+ return checkPartDiskNotFound
+ default:
+ return checkPartUnknown
+ }
+}
+
+func partNeedsHealing(partErrs []int) bool {
+ return slices.IndexFunc(partErrs, func(i int) bool { return i != checkPartSuccess && i != checkPartUnknown }) > -1
+}
+
+func countPartNotSuccess(partErrs []int) (c int) {
+ for _, pe := range partErrs {
+ if pe != checkPartSuccess {
+ c++
+ }
+ }
+ return c
+}
+
+// checkObjectWithAllParts sets partsMetadata and onlineDisks when xl.meta is inexistant/corrupted or outdated
+// it also checks if the status of each part (corrupted, missing, ok) in each drive
+func checkObjectWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetadata []FileInfo,
+ errs []error, latestMeta FileInfo, filterByETag bool, bucket, object string,
+ scanMode madmin.HealScanMode,
+) (dataErrsByDisk map[int][]int, dataErrsByPart map[int][]int) {
+ dataErrsByDisk = make(map[int][]int, len(onlineDisks))
+ for i := range onlineDisks {
+ dataErrsByDisk[i] = make([]int, len(latestMeta.Parts))
+ }
+
+ dataErrsByPart = make(map[int][]int, len(latestMeta.Parts))
+ for i := range latestMeta.Parts {
+ dataErrsByPart[i] = make([]int, len(onlineDisks))
+ }
+
+ inconsistent := 0
+ for i, meta := range partsMetadata {
+ if !meta.IsValid() {
+ // Since for majority of the cases erasure.Index matches with erasure.Distribution we can
+ // consider the offline disks as consistent.
+ continue
+ }
+ if !meta.Deleted {
+ if len(meta.Erasure.Distribution) != len(onlineDisks) {
+ // Erasure distribution seems to have lesser
+ // number of items than number of online disks.
+ inconsistent++
+ continue
+ }
+ if meta.Erasure.Distribution[i] != meta.Erasure.Index {
+ // Mismatch indexes with distribution order
+ inconsistent++
+ }
+ }
+ }
+
+ erasureDistributionReliable := inconsistent <= len(partsMetadata)/2
+
+ metaErrs := make([]error, len(errs))
+
+ for i := range onlineDisks {
+ if errs[i] != nil {
+ metaErrs[i] = errs[i]
+ continue
+ }
+ if onlineDisks[i] == OfflineDisk {
+ metaErrs[i] = errDiskNotFound
+ continue
+ }
+
+ meta := partsMetadata[i]
+ 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
+ partsMetadata[i] = FileInfo{}
+ onlineDisks[i] = nil
+ continue
+ }
+
+ if erasureDistributionReliable {
+ if !meta.IsValid() {
+ partsMetadata[i] = FileInfo{}
+ metaErrs[i] = errFileCorrupt
+ onlineDisks[i] = nil
+ continue
+ }
+
+ if !meta.Deleted {
+ if len(meta.Erasure.Distribution) != len(onlineDisks) {
+ // Erasure distribution is not the same as onlineDisks
+ // attempt a fix if possible, assuming other entries
+ // might have the right erasure distribution.
+ partsMetadata[i] = FileInfo{}
+ metaErrs[i] = errFileCorrupt
+ onlineDisks[i] = nil
+ continue
+ }
+ }
+ }
+ }
+
+ // Copy meta errors to part errors
+ for i, err := range metaErrs {
+ if err != nil {
+ partErr := convPartErrToInt(err)
+ for p := range latestMeta.Parts {
+ dataErrsByPart[p][i] = partErr
+ }
+ }
+ }
+
+ for i, onlineDisk := range onlineDisks {
+ if metaErrs[i] != nil {
+ continue
+ }
+
+ meta := partsMetadata[i]
+ if meta.Deleted || meta.IsRemote() {
+ continue
+ }
+
+ // Always check data, if we got it.
+ if (len(meta.Data) > 0 || meta.Size == 0) && len(meta.Parts) > 0 {
+ checksumInfo := meta.Erasure.GetChecksumInfo(meta.Parts[0].Number)
+ verifyErr := bitrotVerify(bytes.NewReader(meta.Data),
+ int64(len(meta.Data)),
+ meta.Erasure.ShardFileSize(meta.Size),
+ checksumInfo.Algorithm,
+ checksumInfo.Hash, meta.Erasure.ShardSize())
+ dataErrsByPart[0][i] = convPartErrToInt(verifyErr)
+ continue
+ }
+
+ var (
+ verifyErr error
+ verifyResp *CheckPartsResp
+ )
+
+ switch scanMode {
+ case madmin.HealDeepScan:
+ // disk has a valid xl.meta but may not have all the
+ // parts. This is considered an outdated disk, since
+ // it needs healing too.
+ verifyResp, verifyErr = onlineDisk.VerifyFile(ctx, bucket, object, meta)
+ default:
+ verifyResp, verifyErr = onlineDisk.CheckParts(ctx, bucket, object, meta)
+ }
+
+ for p := range latestMeta.Parts {
+ if verifyErr != nil {
+ dataErrsByPart[p][i] = convPartErrToInt(verifyErr)
+ } else {
+ dataErrsByPart[p][i] = verifyResp.Results[p]
+ }
+ }
+ }
+
+ // Build dataErrs by disk from dataErrs by part
+ for part, disks := range dataErrsByPart {
+ for disk := range disks {
+ dataErrsByDisk[disk][part] = dataErrsByPart[part][disk]
+ }
+ }
+ return dataErrsByDisk, dataErrsByPart
+}
diff --git a/cmd/erasure-healing-common_test.go b/cmd/erasure-healing-common_test.go
new file mode 100644
index 00000000..04909b2d
--- /dev/null
+++ b/cmd/erasure-healing-common_test.go
@@ -0,0 +1,774 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "runtime"
+ "testing"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+)
+
+// Returns the latest updated FileInfo files and error in case of failure.
+func getLatestFileInfo(ctx context.Context, partsMetadata []FileInfo, defaultParityCount int, errs []error) (FileInfo, error) {
+ // There should be at least half correct entries, if not return failure
+ expectedRQuorum := len(partsMetadata) / 2
+ if defaultParityCount == 0 {
+ // if parity count is '0', we expected all entries to be present.
+ expectedRQuorum = len(partsMetadata)
+ }
+
+ reducedErr := reduceReadQuorumErrs(ctx, errs, objectOpIgnoredErrs, expectedRQuorum)
+ if reducedErr != nil {
+ return FileInfo{}, reducedErr
+ }
+
+ // List all the file commit ids from parts metadata.
+ modTimes := listObjectModtimes(partsMetadata, errs)
+
+ // Count all latest updated FileInfo values
+ var count int
+ var latestFileInfo FileInfo
+
+ // Reduce list of UUIDs to a single common value - i.e. the last updated Time
+ modTime := commonTime(modTimes, expectedRQuorum)
+
+ if modTime.IsZero() || modTime.Equal(timeSentinel) {
+ return FileInfo{}, errErasureReadQuorum
+ }
+
+ // Iterate through all the modTimes and count the FileInfo(s) with latest time.
+ for index, t := range modTimes {
+ if partsMetadata[index].IsValid() && t.Equal(modTime) {
+ latestFileInfo = partsMetadata[index]
+ count++
+ }
+ }
+
+ if !latestFileInfo.IsValid() {
+ return FileInfo{}, errErasureReadQuorum
+ }
+
+ if count < latestFileInfo.Erasure.DataBlocks {
+ return FileInfo{}, errErasureReadQuorum
+ }
+
+ return latestFileInfo, nil
+}
+
+// validates functionality provided to find most common
+// time occurrence from a list of time.
+func TestCommonTime(t *testing.T) {
+ // List of test cases for common modTime.
+ testCases := []struct {
+ times []time.Time
+ time time.Time
+ quorum int
+ }{
+ {
+ // 1. Tests common times when slice has varying time elements.
+ []time.Time{
+ time.Unix(0, 1).UTC(),
+ time.Unix(0, 2).UTC(),
+ time.Unix(0, 3).UTC(),
+ time.Unix(0, 3).UTC(),
+ time.Unix(0, 2).UTC(),
+ time.Unix(0, 3).UTC(),
+ time.Unix(0, 1).UTC(),
+ },
+ time.Unix(0, 3).UTC(),
+ 3,
+ },
+ {
+ // 2. Tests common time obtained when all elements are equal.
+ []time.Time{
+ time.Unix(0, 3).UTC(),
+ time.Unix(0, 3).UTC(),
+ time.Unix(0, 3).UTC(),
+ time.Unix(0, 3).UTC(),
+ time.Unix(0, 3).UTC(),
+ time.Unix(0, 3).UTC(),
+ time.Unix(0, 3).UTC(),
+ },
+ time.Unix(0, 3).UTC(),
+ 4,
+ },
+ {
+ // 3. Tests common time obtained when elements have a mixture of
+ // sentinel values and don't have read quorum on any of the values.
+ []time.Time{
+ time.Unix(0, 3).UTC(),
+ time.Unix(0, 3).UTC(),
+ time.Unix(0, 2).UTC(),
+ time.Unix(0, 1).UTC(),
+ time.Unix(0, 3).UTC(),
+ time.Unix(0, 4).UTC(),
+ time.Unix(0, 3).UTC(),
+ timeSentinel,
+ timeSentinel,
+ timeSentinel,
+ },
+ timeSentinel,
+ 5,
+ },
+ }
+
+ // Tests all the testcases, and validates them against expected
+ // common modtime. Tests fail if modtime does not match.
+ for i, testCase := range testCases {
+ // Obtain a common mod time from modTimes slice.
+ ctime := commonTime(testCase.times, testCase.quorum)
+ if !testCase.time.Equal(ctime) {
+ t.Errorf("Test case %d, expect to pass but failed. Wanted modTime: %s, got modTime: %s\n", i+1, testCase.time, ctime)
+ }
+ }
+}
+
+// TestListOnlineDisks - checks if listOnlineDisks and outDatedDisks
+// are consistent with each other.
+func TestListOnlineDisks(t *testing.T) {
+ if runtime.GOOS == globalWindowsOSName {
+ t.Skip()
+ }
+
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ obj, disks, err := prepareErasure16(ctx)
+ if err != nil {
+ t.Fatalf("Prepare Erasure backend failed - %v", err)
+ }
+ setObjectLayer(obj)
+ defer obj.Shutdown(t.Context())
+ defer removeRoots(disks)
+
+ type tamperKind int
+ const (
+ noTamper tamperKind = iota
+ deletePart
+ corruptPart
+ )
+
+ timeSentinel := time.Unix(1, 0).UTC()
+ threeNanoSecs := time.Unix(3, 0).UTC()
+ fourNanoSecs := time.Unix(4, 0).UTC()
+ modTimesThreeNone := make([]time.Time, 16)
+ modTimesThreeFour := make([]time.Time, 16)
+ for i := range 16 {
+ // Have 13 good xl.meta, 12 for default parity count = 4 (EC:4) and one
+ // to be tampered with.
+ if i > 12 {
+ modTimesThreeFour[i] = fourNanoSecs
+ modTimesThreeNone[i] = timeSentinel
+ continue
+ }
+ modTimesThreeFour[i] = threeNanoSecs
+ modTimesThreeNone[i] = threeNanoSecs
+ }
+
+ testCases := []struct {
+ modTimes []time.Time
+ expectedTime time.Time
+ errs []error
+ _tamperBackend tamperKind
+ }{
+ {
+ modTimes: modTimesThreeFour,
+ expectedTime: threeNanoSecs,
+ errs: []error{
+ nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, nil,
+ },
+ _tamperBackend: noTamper,
+ },
+ {
+ modTimes: modTimesThreeNone,
+ expectedTime: threeNanoSecs,
+ errs: []error{
+ // Disks that have a valid xl.meta.
+ nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil,
+ // Some disks can't access xl.meta.
+ errFileNotFound, errDiskAccessDenied, errDiskNotFound,
+ },
+ _tamperBackend: deletePart,
+ },
+ {
+ modTimes: modTimesThreeNone,
+ expectedTime: threeNanoSecs,
+ errs: []error{
+ // Disks that have a valid xl.meta.
+ nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil,
+ // Some disks don't have xl.meta.
+ errDiskNotFound, errFileNotFound, errFileNotFound,
+ },
+ _tamperBackend: corruptPart,
+ },
+ }
+
+ bucket := "bucket"
+ err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{})
+ if err != nil {
+ t.Fatalf("Failed to make a bucket %v", err)
+ }
+
+ object := "object"
+ data := bytes.Repeat([]byte("a"), smallFileThreshold*32)
+ z := obj.(*erasureServerPools)
+
+ erasureDisks, err := z.GetDisks(0, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for i, test := range testCases {
+ t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) {
+ _, err = obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), ObjectOptions{})
+ if err != nil {
+ t.Fatalf("Failed to putObject %v", err)
+ }
+
+ partsMetadata, errs := readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true)
+ fi, err := getLatestFileInfo(ctx, partsMetadata, z.serverPools[0].sets[0].defaultParityCount, errs)
+ if err != nil {
+ t.Fatalf("Failed to getLatestFileInfo %v", err)
+ }
+
+ for j := range partsMetadata {
+ if errs[j] != nil {
+ t.Fatalf("expected error to be nil: %s", errs[j])
+ }
+ partsMetadata[j].ModTime = test.modTimes[j]
+ }
+
+ tamperedIndex := -1
+ switch test._tamperBackend {
+ case deletePart:
+ for index, err := range test.errs {
+ if err != nil {
+ continue
+ }
+ // Remove a part from a disk
+ // which has a valid xl.meta,
+ // and check if that disk
+ // appears in outDatedDisks.
+ tamperedIndex = index
+ dErr := erasureDisks[index].Delete(t.Context(), bucket, pathJoin(object, fi.DataDir, "part.1"), DeleteOptions{
+ Recursive: false,
+ Immediate: false,
+ })
+ if dErr != nil {
+ t.Fatalf("Failed to delete %s - %v", filepath.Join(object, "part.1"), dErr)
+ }
+ break
+ }
+ case corruptPart:
+ for index, err := range test.errs {
+ if err != nil {
+ continue
+ }
+ // Corrupt a part from a disk
+ // which has a valid xl.meta,
+ // and check if that disk
+ // appears in outDatedDisks.
+ tamperedIndex = index
+ filePath := pathJoin(erasureDisks[index].String(), bucket, object, fi.DataDir, "part.1")
+ f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_SYNC, 0)
+ if err != nil {
+ t.Fatalf("Failed to open %s: %s\n", filePath, err)
+ }
+ f.WriteString("oops") // Will cause bitrot error
+ f.Close()
+ break
+ }
+ }
+
+ rQuorum := len(errs) - z.serverPools[0].sets[0].defaultParityCount
+ onlineDisks, modTime, _ := listOnlineDisks(erasureDisks, partsMetadata, test.errs, rQuorum)
+ if !modTime.Equal(test.expectedTime) {
+ t.Fatalf("Expected modTime to be equal to %v but was found to be %v",
+ test.expectedTime, modTime)
+ }
+ _, _ = checkObjectWithAllParts(ctx, onlineDisks, partsMetadata,
+ test.errs, fi, false, bucket, object, madmin.HealDeepScan)
+
+ if test._tamperBackend != noTamper {
+ if tamperedIndex != -1 && onlineDisks[tamperedIndex] != nil {
+ t.Fatalf("Drive (%v) with part.1 missing is not a drive with available data",
+ erasureDisks[tamperedIndex])
+ }
+ }
+ })
+ }
+}
+
+// TestListOnlineDisksSmallObjects - checks if listOnlineDisks and outDatedDisks
+// are consistent with each other.
+func TestListOnlineDisksSmallObjects(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ obj, disks, err := prepareErasure16(ctx)
+ if err != nil {
+ t.Fatalf("Prepare Erasure backend failed - %v", err)
+ }
+ setObjectLayer(obj)
+ defer obj.Shutdown(t.Context())
+ defer removeRoots(disks)
+
+ type tamperKind int
+ const (
+ noTamper tamperKind = iota
+ deletePart tamperKind = iota
+ corruptPart tamperKind = iota
+ )
+ timeSentinel := time.Unix(1, 0).UTC()
+ threeNanoSecs := time.Unix(3, 0).UTC()
+ fourNanoSecs := time.Unix(4, 0).UTC()
+ modTimesThreeNone := make([]time.Time, 16)
+ modTimesThreeFour := make([]time.Time, 16)
+ for i := range 16 {
+ // Have 13 good xl.meta, 12 for default parity count = 4 (EC:4) and one
+ // to be tampered with.
+ if i > 12 {
+ modTimesThreeFour[i] = fourNanoSecs
+ modTimesThreeNone[i] = timeSentinel
+ continue
+ }
+ modTimesThreeFour[i] = threeNanoSecs
+ modTimesThreeNone[i] = threeNanoSecs
+ }
+
+ testCases := []struct {
+ modTimes []time.Time
+ expectedTime time.Time
+ errs []error
+ _tamperBackend tamperKind
+ }{
+ {
+ modTimes: modTimesThreeFour,
+ expectedTime: threeNanoSecs,
+ errs: []error{
+ nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, nil,
+ },
+ _tamperBackend: noTamper,
+ },
+ {
+ modTimes: modTimesThreeNone,
+ expectedTime: threeNanoSecs,
+ errs: []error{
+ // Disks that have a valid xl.meta.
+ nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil,
+ // Some disks can't access xl.meta.
+ errFileNotFound, errDiskAccessDenied, errDiskNotFound,
+ },
+ _tamperBackend: deletePart,
+ },
+ {
+ modTimes: modTimesThreeNone,
+ expectedTime: threeNanoSecs,
+ errs: []error{
+ // Disks that have a valid xl.meta.
+ nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil,
+ // Some disks don't have xl.meta.
+ errDiskNotFound, errFileNotFound, errFileNotFound,
+ },
+ _tamperBackend: corruptPart,
+ },
+ }
+
+ bucket := "bucket"
+ err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{})
+ if err != nil {
+ t.Fatalf("Failed to make a bucket %v", err)
+ }
+
+ object := "object"
+ data := bytes.Repeat([]byte("a"), smallFileThreshold/2)
+ z := obj.(*erasureServerPools)
+
+ erasureDisks, err := z.GetDisks(0, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for i, test := range testCases {
+ t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) {
+ _, err := obj.PutObject(ctx, bucket, object,
+ mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), ObjectOptions{})
+ if err != nil {
+ t.Fatalf("Failed to putObject %v", err)
+ }
+
+ partsMetadata, errs := readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", true, true)
+ fi, err := getLatestFileInfo(ctx, partsMetadata, z.serverPools[0].sets[0].defaultParityCount, errs)
+ if err != nil {
+ t.Fatalf("Failed to getLatestFileInfo %v", err)
+ }
+
+ for j := range partsMetadata {
+ if errs[j] != nil {
+ t.Fatalf("expected error to be nil: %s", errs[j])
+ }
+ partsMetadata[j].ModTime = test.modTimes[j]
+ }
+
+ if erasureDisks, err = writeUniqueFileInfo(ctx, erasureDisks, "", bucket, object, partsMetadata, diskCount(erasureDisks)); err != nil {
+ t.Fatal(ctx, err)
+ }
+
+ tamperedIndex := -1
+ switch test._tamperBackend {
+ case deletePart:
+ for index, err := range test.errs {
+ if err != nil {
+ continue
+ }
+ // Remove a part from a disk
+ // which has a valid xl.meta,
+ // and check if that disk
+ // appears in outDatedDisks.
+ tamperedIndex = index
+ dErr := erasureDisks[index].Delete(t.Context(), bucket, pathJoin(object, xlStorageFormatFile), DeleteOptions{
+ Recursive: false,
+ Immediate: false,
+ })
+ if dErr != nil {
+ t.Fatalf("Failed to delete %s - %v", pathJoin(object, xlStorageFormatFile), dErr)
+ }
+ break
+ }
+ case corruptPart:
+ for index, err := range test.errs {
+ if err != nil {
+ continue
+ }
+ // Corrupt a part from a disk
+ // which has a valid xl.meta,
+ // and check if that disk
+ // appears in outDatedDisks.
+ tamperedIndex = index
+ filePath := pathJoin(erasureDisks[index].String(), bucket, object, xlStorageFormatFile)
+ f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_SYNC, 0)
+ if err != nil {
+ t.Fatalf("Failed to open %s: %s\n", filePath, err)
+ }
+ f.WriteString("oops") // Will cause bitrot error
+ f.Close()
+ break
+ }
+ }
+
+ rQuorum := len(errs) - z.serverPools[0].sets[0].defaultParityCount
+ onlineDisks, modTime, _ := listOnlineDisks(erasureDisks, partsMetadata, test.errs, rQuorum)
+ if !modTime.Equal(test.expectedTime) {
+ t.Fatalf("Expected modTime to be equal to %v but was found to be %v",
+ test.expectedTime, modTime)
+ }
+
+ _, _ = checkObjectWithAllParts(ctx, onlineDisks, partsMetadata,
+ test.errs, fi, false, bucket, object, madmin.HealDeepScan)
+
+ if test._tamperBackend != noTamper {
+ if tamperedIndex != -1 && onlineDisks[tamperedIndex] != nil {
+ t.Fatalf("Drive (%v) with part.1 missing is not a drive with available data",
+ erasureDisks[tamperedIndex])
+ }
+ }
+ })
+ }
+}
+
+func TestDisksWithAllParts(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+ obj, disks, err := prepareErasure16(ctx)
+ if err != nil {
+ t.Fatalf("Prepare Erasure backend failed - %v", err)
+ }
+ setObjectLayer(obj)
+ defer obj.Shutdown(t.Context())
+ defer removeRoots(disks)
+
+ bucket := "bucket"
+ object := "object"
+ // make data with more than one part
+ partCount := 3
+ data := bytes.Repeat([]byte("a"), 6*1024*1024*partCount)
+ z := obj.(*erasureServerPools)
+ s := z.serverPools[0].sets[0]
+ erasureDisks := s.getDisks()
+ err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{})
+ if err != nil {
+ t.Fatalf("Failed to make a bucket %v", err)
+ }
+
+ _, err = obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), ObjectOptions{})
+ if err != nil {
+ t.Fatalf("Failed to putObject %v", err)
+ }
+
+ _, errs := readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true)
+ readQuorum := len(erasureDisks) / 2
+ if reducedErr := reduceReadQuorumErrs(ctx, errs, objectOpIgnoredErrs, readQuorum); reducedErr != nil {
+ t.Fatalf("Failed to read xl meta data %v", reducedErr)
+ }
+
+ // Test 1: Test that all disks are returned without any failures with
+ // unmodified meta data
+ erasureDisks = s.getDisks()
+ partsMetadata, errs := readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true)
+ if err != nil {
+ t.Fatalf("Failed to read xl meta data %v", err)
+ }
+
+ fi, err := getLatestFileInfo(ctx, partsMetadata, s.defaultParityCount, errs)
+ if err != nil {
+ t.Fatalf("Failed to get quorum consistent fileInfo %v", err)
+ }
+
+ erasureDisks, _, _ = listOnlineDisks(erasureDisks, partsMetadata, errs, readQuorum)
+
+ dataErrsPerDisk, _ := checkObjectWithAllParts(ctx, erasureDisks, partsMetadata,
+ errs, fi, false, bucket, object, madmin.HealDeepScan)
+
+ for diskIndex, disk := range erasureDisks {
+ if partNeedsHealing(dataErrsPerDisk[diskIndex]) {
+ t.Errorf("Unexpected error: %v", dataErrsPerDisk[diskIndex])
+ }
+
+ if disk == nil {
+ t.Errorf("Drive erroneously filtered, driveIndex: %d", diskIndex)
+ }
+ }
+
+ // Test 2: Not synchronized modtime
+ erasureDisks = s.getDisks()
+ partsMetadataBackup := partsMetadata[0]
+ partsMetadata[0].ModTime = partsMetadata[0].ModTime.Add(-1 * time.Hour)
+
+ errs = make([]error, len(erasureDisks))
+ _, _ = checkObjectWithAllParts(ctx, erasureDisks, partsMetadata,
+ errs, fi, false, bucket, object, madmin.HealDeepScan)
+
+ for diskIndex, disk := range erasureDisks {
+ if diskIndex == 0 && disk != nil {
+ t.Errorf("Drive not filtered as expected, drive: %d", diskIndex)
+ }
+ if diskIndex != 0 && disk == nil {
+ t.Errorf("Drive erroneously filtered, driveIndex: %d", diskIndex)
+ }
+ }
+ partsMetadata[0] = partsMetadataBackup // Revert before going to the next test
+
+ // Test 3: Not synchronized DataDir
+ erasureDisks = s.getDisks()
+ partsMetadataBackup = partsMetadata[1]
+ partsMetadata[1].DataDir = "foo-random"
+
+ errs = make([]error, len(erasureDisks))
+ _, _ = checkObjectWithAllParts(ctx, erasureDisks, partsMetadata,
+ errs, fi, false, bucket, object, madmin.HealDeepScan)
+
+ for diskIndex, disk := range erasureDisks {
+ if diskIndex == 1 && disk != nil {
+ t.Errorf("Drive not filtered as expected, drive: %d", diskIndex)
+ }
+ if diskIndex != 1 && disk == nil {
+ t.Errorf("Drive erroneously filtered, driveIndex: %d", diskIndex)
+ }
+ }
+ partsMetadata[1] = partsMetadataBackup // Revert before going to the next test
+
+ // Test 4: key = disk index, value = part name with hash mismatch
+ erasureDisks = s.getDisks()
+ diskFailures := make(map[int]string)
+ diskFailures[0] = "part.1"
+ diskFailures[3] = "part.1"
+ diskFailures[15] = "part.1"
+
+ for diskIndex, partName := range diskFailures {
+ for i := range partsMetadata[diskIndex].Parts {
+ if fmt.Sprintf("part.%d", i+1) == partName {
+ filePath := pathJoin(erasureDisks[diskIndex].String(), bucket, object, partsMetadata[diskIndex].DataDir, partName)
+ f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_SYNC, 0)
+ if err != nil {
+ t.Fatalf("Failed to open %s: %s\n", filePath, err)
+ }
+ f.WriteString("oops") // Will cause bitrot error
+ f.Close()
+ }
+ }
+ }
+
+ errs = make([]error, len(erasureDisks))
+ dataErrsPerDisk, _ = checkObjectWithAllParts(ctx, erasureDisks, partsMetadata,
+ errs, fi, false, bucket, object, madmin.HealDeepScan)
+
+ for diskIndex := range erasureDisks {
+ if _, ok := diskFailures[diskIndex]; ok {
+ if !partNeedsHealing(dataErrsPerDisk[diskIndex]) {
+ t.Errorf("Disk expected to be healed, driveIndex: %d", diskIndex)
+ }
+ } else {
+ if partNeedsHealing(dataErrsPerDisk[diskIndex]) {
+ t.Errorf("Disk not expected to be healed, driveIndex: %d", diskIndex)
+ }
+ }
+ }
+}
+
+func TestCommonParities(t *testing.T) {
+ // This test uses two FileInfo values that represent the same object but
+ // have different parities. They occur in equal number of drives, but only
+ // one has read quorum. commonParity should pick the parity corresponding to
+ // the FileInfo which has read quorum.
+ fi1 := FileInfo{
+ Volume: "mybucket",
+ Name: "myobject",
+ VersionID: "",
+ IsLatest: true,
+ Deleted: false,
+ ExpireRestored: false,
+ DataDir: "4a01d9dd-0c5e-4103-88f8-b307c57d212e",
+ XLV1: false,
+ ModTime: time.Date(2023, time.March, 15, 11, 18, 4, 989906961, time.UTC),
+ Size: 329289, Mode: 0x0, WrittenByVersion: 0x63c77756,
+ Metadata: map[string]string{
+ "content-type": "application/octet-stream", "etag": "f205307ef9f50594c4b86d9c246bee86", "x-minio-internal-erasure-upgraded": "5->6", "x-minio-internal-inline-data": "true",
+ },
+ Parts: []ObjectPartInfo{
+ {
+ ETag: "",
+ Number: 1,
+ Size: 329289,
+ ActualSize: 329289,
+ ModTime: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
+ Index: []uint8(nil),
+ Checksums: map[string]string(nil),
+ },
+ },
+ Erasure: ErasureInfo{
+ Algorithm: "ReedSolomon",
+ DataBlocks: 6,
+ ParityBlocks: 6,
+ BlockSize: 1048576,
+ Index: 1,
+ Distribution: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
+ Checksums: []ChecksumInfo{{PartNumber: 1, Algorithm: 0x3, Hash: []uint8{}}},
+ },
+ NumVersions: 1,
+ Idx: 0,
+ }
+
+ fi2 := FileInfo{
+ Volume: "mybucket",
+ Name: "myobject",
+ VersionID: "",
+ IsLatest: true,
+ Deleted: false,
+ DataDir: "6f5c106d-9d28-4c85-a7f4-eac56225876b",
+ ModTime: time.Date(2023, time.March, 15, 19, 57, 30, 492530160, time.UTC),
+ Size: 329289,
+ Mode: 0x0,
+ WrittenByVersion: 0x63c77756,
+ Metadata: map[string]string{"content-type": "application/octet-stream", "etag": "f205307ef9f50594c4b86d9c246bee86", "x-minio-internal-inline-data": "true"},
+ Parts: []ObjectPartInfo{
+ {
+ ETag: "",
+ Number: 1,
+ Size: 329289,
+ ActualSize: 329289,
+ ModTime: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
+ Index: []uint8(nil),
+ Checksums: map[string]string(nil),
+ },
+ },
+ Erasure: ErasureInfo{
+ Algorithm: "ReedSolomon",
+ DataBlocks: 7,
+ ParityBlocks: 5,
+ BlockSize: 1048576,
+ Index: 2,
+ Distribution: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
+ Checksums: []ChecksumInfo{
+ {PartNumber: 1, Algorithm: 0x3, Hash: []uint8{}},
+ },
+ },
+ NumVersions: 1,
+ Idx: 0,
+ }
+
+ fiDel := FileInfo{
+ Volume: "mybucket",
+ Name: "myobject",
+ VersionID: "",
+ IsLatest: true,
+ Deleted: true,
+ ModTime: time.Date(2023, time.March, 15, 19, 57, 30, 492530160, time.UTC),
+ Mode: 0x0,
+ WrittenByVersion: 0x63c77756,
+ NumVersions: 1,
+ Idx: 0,
+ }
+
+ tests := []struct {
+ fi1, fi2 FileInfo
+ }{
+ {
+ fi1: fi1,
+ fi2: fi2,
+ },
+ {
+ fi1: fi1,
+ fi2: fiDel,
+ },
+ }
+ for idx, test := range tests {
+ var metaArr []FileInfo
+ for i := range 12 {
+ fi := test.fi1
+ if i%2 == 0 {
+ fi = test.fi2
+ }
+ metaArr = append(metaArr, fi)
+ }
+
+ parities := listObjectParities(metaArr, make([]error, len(metaArr)))
+ parity := commonParity(parities, 5)
+ var match int
+ for _, fi := range metaArr {
+ if fi.Erasure.ParityBlocks == parity {
+ match++
+ }
+ }
+ if match < len(metaArr)-parity {
+ t.Fatalf("Test %d: Expected %d drives with parity=%d, but got %d", idx, len(metaArr)-parity, parity, match)
+ }
+ }
+}
diff --git a/cmd/erasure-healing.go b/cmd/erasure-healing.go
new file mode 100644
index 00000000..9ea507f7
--- /dev/null
+++ b/cmd/erasure-healing.go
@@ -0,0 +1,1137 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/grid"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/sync/errgroup"
+ "github.com/puzpuzpuz/xsync/v3"
+)
+
+//go:generate stringer -type=healingMetric -trimprefix=healingMetric $GOFILE
+
+type healingMetric uint8
+
+const (
+ healingMetricBucket healingMetric = iota
+ healingMetricObject
+ healingMetricCheckAbandonedParts
+)
+
+// 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)
+ defer cancel()
+
+ disks, _ := er.getOnlineDisksWithHealing(false)
+ if len(disks) == 0 {
+ return errors.New("listAndHeal: No non-healing drives found")
+ }
+
+ // How to resolve partial results.
+ resolver := metadataResolutionParams{
+ dirQuorum: 1,
+ objQuorum: 1,
+ bucket: bucket,
+ strict: false, // Allow less strict matching.
+ }
+
+ path := baseDirFromPrefix(prefix)
+ filterPrefix := strings.Trim(strings.TrimPrefix(prefix, path), slashSeparator)
+ if path == prefix {
+ filterPrefix = ""
+ }
+
+ lopts := listPathRawOptions{
+ disks: disks,
+ bucket: bucket,
+ path: path,
+ filterPrefix: filterPrefix,
+ recursive: recursive,
+ forwardTo: "",
+ minDisks: 1,
+ reportNotFound: false,
+ agreed: func(entry metaCacheEntry) {
+ if !recursive && prefix != entry.name {
+ return
+ }
+ if err := healEntry(bucket, entry, scanMode); err != nil {
+ cancel()
+ }
+ },
+ partial: func(entries metaCacheEntries, _ []error) {
+ entry, ok := entries.resolve(&resolver)
+ if !ok {
+ // check if we can get one entry at least
+ // proceed to heal nonetheless.
+ entry, _ = entries.firstFound()
+ }
+ if !recursive && prefix != entry.name {
+ return
+ }
+ if err := healEntry(bucket, *entry, scanMode); err != nil {
+ cancel()
+ return
+ }
+ },
+ finished: nil,
+ }
+
+ if err := listPathRaw(ctx, lopts); err != nil {
+ return fmt.Errorf("listPathRaw returned %w: opts(%#v)", err, lopts)
+ }
+
+ return nil
+}
+
+// listAllBuckets lists all buckets from all disks. It also
+// returns the occurrence of each buckets in all disks
+func listAllBuckets(ctx context.Context, storageDisks []StorageAPI, healBuckets *xsync.MapOf[string, VolInfo], readQuorum int) error {
+ g := errgroup.WithNErrs(len(storageDisks))
+ for index := range storageDisks {
+ g.Go(func() error {
+ if storageDisks[index] == nil {
+ // we ignore disk not found errors
+ return nil
+ }
+ volsInfo, err := storageDisks[index].ListVols(ctx)
+ if err != nil {
+ return err
+ }
+
+ for _, volInfo := range volsInfo {
+ // StorageAPI can send volume names which are
+ // incompatible with buckets - these are
+ // skipped, like the meta-bucket.
+ if isReservedOrInvalidBucket(volInfo.Name, false) {
+ continue
+ }
+
+ healBuckets.Compute(volInfo.Name, func(oldValue VolInfo, loaded bool) (newValue VolInfo, del bool) {
+ if loaded {
+ newValue = oldValue
+ newValue.count = oldValue.count + 1
+ return newValue, false
+ }
+ return VolInfo{
+ Name: volInfo.Name,
+ Created: volInfo.Created,
+ count: 1,
+ }, false
+ })
+ }
+
+ return nil
+ }, index)
+ }
+
+ 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")
+ errOutdatedXLMeta = errors.New("outdated XL meta")
+ errPartCorrupt = errors.New("part corrupt")
+ errPartMissing = errors.New("part missing")
+)
+
+// 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.
+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) {
+ return true, true, erErr
+ }
+ if erErr == nil {
+ if meta.XLV1 {
+ // Legacy means heal always
+ // always check first.
+ return true, true, errLegacyXLMeta
+ }
+ if !latestMeta.Equals(meta) {
+ return true, true, errOutdatedXLMeta
+ }
+ if !meta.Deleted && !meta.IsRemote() {
+ // If xl.meta was read fine but there may be problem with the part.N files.
+ for _, partErr := range partsErrs {
+ if partErr == checkPartFileNotFound {
+ return true, false, errPartMissing
+ }
+ if partErr == checkPartFileCorrupt {
+ return true, false, errPartCorrupt
+ }
+ }
+ }
+ return false, false, nil
+ }
+ return false, false, erErr
+}
+
+const (
+ xMinIOHealing = ReservedMetadataPrefix + "healing"
+ xMinIODataMov = ReservedMetadataPrefix + "data-mov"
+)
+
+// SetHealing marks object (version) as being healed.
+// Note: this is to be used only from healObject
+func (fi *FileInfo) SetHealing() {
+ if fi.Metadata == nil {
+ fi.Metadata = make(map[string]string)
+ }
+ fi.Metadata[xMinIOHealing] = "true"
+}
+
+// Healing returns true if object is being healed (i.e fi is being passed down
+// from healObject)
+func (fi FileInfo) Healing() bool {
+ _, ok := fi.Metadata[xMinIOHealing]
+ return ok
+}
+
+// SetDataMov marks object (version) as being currently
+// in movement, such as decommissioning or rebalance.
+func (fi *FileInfo) SetDataMov() {
+ if fi.Metadata == nil {
+ fi.Metadata = make(map[string]string)
+ }
+ fi.Metadata[xMinIODataMov] = "true"
+}
+
+// DataMov returns true if object is being in movement
+func (fi FileInfo) DataMov() bool {
+ _, ok := fi.Metadata[xMinIODataMov]
+ return ok
+}
+
+func (er *erasureObjects) auditHealObject(ctx context.Context, bucket, object, versionID string, result madmin.HealResultItem, err error) {
+ if len(logger.AuditTargets()) == 0 {
+ return
+ }
+
+ opts := AuditLogOptions{
+ Event: "HealObject",
+ Bucket: bucket,
+ Object: decodeDirObject(object),
+ VersionID: versionID,
+ }
+ if err != nil {
+ opts.Error = err.Error()
+ }
+
+ b, a := result.GetCorruptedCounts()
+ if b > 0 && b == a {
+ opts.Error = fmt.Sprintf("unable to heal %d corrupted blocks on drives", b)
+ }
+
+ b, a = result.GetMissingCounts()
+ if b > 0 && b == a {
+ opts.Error = fmt.Sprintf("unable to heal %d missing blocks on drives", b)
+ }
+
+ opts.Tags = map[string]string{
+ "healObject": auditObjectOp{
+ Name: opts.Object,
+ Pool: er.poolIndex + 1,
+ Set: er.setIndex + 1,
+ }.String(),
+ }
+
+ 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.
+func (er *erasureObjects) healObject(ctx context.Context, bucket string, object string, versionID string, opts madmin.HealOpts) (result madmin.HealResultItem, err error) {
+ dryRun := opts.DryRun
+ scanMode := opts.ScanMode
+
+ storageDisks := er.getDisks()
+ storageEndpoints := er.getEndpoints()
+
+ defer func() {
+ er.auditHealObject(ctx, bucket, object, versionID, result, err)
+ }()
+
+ if globalTrace.NumSubscribers(madmin.TraceHealing) > 0 {
+ startTime := time.Now()
+ defer func() {
+ healTrace(healingMetricObject, startTime, bucket, object, &opts, err, &result)
+ }()
+ }
+
+ // Initialize heal result object
+ result = madmin.HealResultItem{
+ Type: madmin.HealItemObject,
+ Bucket: bucket,
+ Object: object,
+ VersionID: versionID,
+ DiskCount: len(storageDisks),
+ }
+
+ if !opts.NoLock {
+ lk := er.NewNSLock(bucket, object)
+ lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ return result, err
+ }
+ ctx = lkctx.Context()
+ defer lk.Unlock(lkctx)
+ }
+
+ // Re-read when we have lock...
+ partsMetadata, errs := readAllFileInfo(ctx, storageDisks, "", bucket, object, versionID, true, true)
+ if isAllNotFound(errs) {
+ err := errFileNotFound
+ if versionID != "" {
+ err = errFileVersionNotFound
+ }
+ // Nothing to do, file is already gone.
+ return er.defaultHealResult(FileInfo{}, storageDisks, storageEndpoints,
+ errs, bucket, object, versionID), err
+ }
+
+ readQuorum, _, err := objectQuorumFromMeta(ctx, partsMetadata, errs, er.defaultParityCount)
+ if err != nil {
+ m, derr := er.deleteIfDangling(ctx, bucket, object, partsMetadata, errs, nil, ObjectOptions{
+ VersionID: versionID,
+ })
+ errs = make([]error, len(errs))
+ if derr == nil {
+ derr = errFileNotFound
+ if versionID != "" {
+ derr = errFileVersionNotFound
+ }
+ // We did find a new danging object
+ return er.defaultHealResult(m, storageDisks, storageEndpoints,
+ errs, bucket, object, versionID), derr
+ }
+ return er.defaultHealResult(m, storageDisks, storageEndpoints,
+ errs, bucket, object, versionID), err
+ }
+
+ result.ParityBlocks = result.DiskCount - readQuorum
+ result.DataBlocks = readQuorum
+
+ // List of disks having latest version of the object xl.meta
+ // (by modtime).
+ onlineDisks, quorumModTime, quorumETag := listOnlineDisks(storageDisks, partsMetadata, errs, readQuorum)
+
+ // Latest FileInfo for reference. If a valid metadata is not
+ // present, it is as good as object not found.
+ latestMeta, err := pickValidFileInfo(ctx, partsMetadata, quorumModTime, quorumETag, readQuorum)
+ if err != nil {
+ return result, err
+ }
+
+ // No modtime quorum
+ filterDisksByETag := quorumETag != ""
+
+ dataErrsByDisk, dataErrsByPart := checkObjectWithAllParts(ctx, onlineDisks, partsMetadata,
+ errs, latestMeta, filterDisksByETag, bucket, object, scanMode)
+
+ var erasure Erasure
+ if !latestMeta.Deleted && !latestMeta.IsRemote() {
+ // Initialize erasure coding
+ erasure, err = NewErasure(ctx, latestMeta.Erasure.DataBlocks,
+ latestMeta.Erasure.ParityBlocks, latestMeta.Erasure.BlockSize)
+ if err != nil {
+ return result, err
+ }
+ }
+
+ result.ObjectSize, err = latestMeta.ToObjectInfo(bucket, object, true).GetActualSize()
+ if err != nil {
+ return result, err
+ }
+
+ // Loop to find number of disks with valid data, per-drive
+ // data state and a list of outdated disks on which data needs
+ // to be healed.
+ outDatedDisks := make([]StorageAPI, len(storageDisks))
+ disksToHealCount, xlMetaToHealCount := 0, 0
+ for i := range onlineDisks {
+ yes, isMeta, reason := shouldHealObjectOnDisk(errs[i], dataErrsByDisk[i], partsMetadata[i], latestMeta)
+ if yes {
+ outDatedDisks[i] = storageDisks[i]
+ disksToHealCount++
+ if isMeta {
+ xlMetaToHealCount++
+ }
+ }
+
+ driveState := objectErrToDriveState(reason)
+
+ result.Before.Drives = append(result.Before.Drives, madmin.HealDriveInfo{
+ UUID: "",
+ Endpoint: storageEndpoints[i].String(),
+ State: driveState,
+ })
+ result.After.Drives = append(result.After.Drives, madmin.HealDriveInfo{
+ UUID: "",
+ Endpoint: storageEndpoints[i].String(),
+ State: driveState,
+ })
+ }
+
+ if disksToHealCount == 0 {
+ // Nothing to heal!
+ return result, nil
+ }
+
+ // After this point, only have to repair data on disk - so
+ // return if it is a dry-run
+ if dryRun {
+ return result, nil
+ }
+
+ 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.
+ // this would end up restoring the correct readable versions.
+ m, err := er.deleteIfDangling(ctx, bucket, object, partsMetadata, errs, dataErrsByPart, ObjectOptions{
+ VersionID: versionID,
+ })
+ errs = make([]error, len(errs))
+ if err == nil {
+ err = errFileNotFound
+ if versionID != "" {
+ err = errFileVersionNotFound
+ }
+ // We did find a new danging object
+ return er.defaultHealResult(m, storageDisks, storageEndpoints,
+ errs, bucket, object, versionID), err
+ }
+ for i := range errs {
+ errs[i] = err
+ }
+ return er.defaultHealResult(m, storageDisks, storageEndpoints,
+ errs, bucket, object, versionID), err
+ }
+
+ cleanFileInfo := func(fi FileInfo) FileInfo {
+ // Returns a copy of the 'fi' with erasure index, checksums and inline data niled.
+ nfi := fi
+ if !nfi.IsRemote() {
+ nfi.Data = nil
+ nfi.Erasure.Index = 0
+ nfi.Erasure.Checksums = nil
+ }
+ return nfi
+ }
+
+ // We write at temporary location and then rename to final location.
+ tmpID := mustGetUUID()
+ migrateDataDir := mustGetUUID()
+
+ if !latestMeta.Deleted && len(latestMeta.Erasure.Distribution) != len(onlineDisks) {
+ err := fmt.Errorf("unexpected file distribution (%v) from online disks (%v), looks like backend disks have been manually modified refusing to heal %s/%s(%s)",
+ latestMeta.Erasure.Distribution, onlineDisks, bucket, object, versionID)
+ healingLogOnceIf(ctx, err, "heal-object-online-disks")
+ return er.defaultHealResult(latestMeta, storageDisks, storageEndpoints, errs,
+ bucket, object, versionID), err
+ }
+
+ latestDisks := shuffleDisks(onlineDisks, latestMeta.Erasure.Distribution)
+
+ if !latestMeta.Deleted && len(latestMeta.Erasure.Distribution) != len(outDatedDisks) {
+ err := fmt.Errorf("unexpected file distribution (%v) from outdated disks (%v), looks like backend disks have been manually modified refusing to heal %s/%s(%s)",
+ latestMeta.Erasure.Distribution, outDatedDisks, bucket, object, versionID)
+ healingLogOnceIf(ctx, err, "heal-object-outdated-disks")
+ return er.defaultHealResult(latestMeta, storageDisks, storageEndpoints, errs,
+ bucket, object, versionID), err
+ }
+
+ outDatedDisks = shuffleDisks(outDatedDisks, latestMeta.Erasure.Distribution)
+
+ if !latestMeta.Deleted && len(latestMeta.Erasure.Distribution) != len(partsMetadata) {
+ err := fmt.Errorf("unexpected file distribution (%v) from metadata entries (%v), looks like backend disks have been manually modified refusing to heal %s/%s(%s)",
+ latestMeta.Erasure.Distribution, len(partsMetadata), bucket, object, versionID)
+ healingLogOnceIf(ctx, err, "heal-object-metadata-entries")
+ return er.defaultHealResult(latestMeta, storageDisks, storageEndpoints, errs,
+ bucket, object, versionID), err
+ }
+
+ partsMetadata = shufflePartsMetadata(partsMetadata, latestMeta.Erasure.Distribution)
+
+ copyPartsMetadata := make([]FileInfo, len(partsMetadata))
+ for i := range latestDisks {
+ if latestDisks[i] == nil {
+ continue
+ }
+ copyPartsMetadata[i] = partsMetadata[i]
+ }
+
+ for i := range outDatedDisks {
+ if outDatedDisks[i] == nil {
+ continue
+ }
+ // Make sure to write the FileInfo information
+ // that is expected to be in quorum.
+ partsMetadata[i] = cleanFileInfo(latestMeta)
+ }
+
+ // source data dir shall be empty in case of XLV1
+ // differentiate it with dstDataDir for readability
+ // srcDataDir is the one used with newBitrotReader()
+ // to read existing content.
+ srcDataDir := latestMeta.DataDir
+ dstDataDir := latestMeta.DataDir
+ if latestMeta.XLV1 {
+ dstDataDir = migrateDataDir
+ }
+
+ var inlineBuffers []*bytes.Buffer
+ if !latestMeta.Deleted && !latestMeta.IsRemote() {
+ if latestMeta.InlineData() {
+ inlineBuffers = make([]*bytes.Buffer, len(outDatedDisks))
+ }
+
+ erasureInfo := latestMeta.Erasure
+ for partIndex := 0; partIndex < len(latestMeta.Parts); partIndex++ {
+ partSize := latestMeta.Parts[partIndex].Size
+ partActualSize := latestMeta.Parts[partIndex].ActualSize
+ partModTime := latestMeta.Parts[partIndex].ModTime
+ partNumber := latestMeta.Parts[partIndex].Number
+ partIdx := latestMeta.Parts[partIndex].Index
+ partChecksums := latestMeta.Parts[partIndex].Checksums
+ tillOffset := erasure.ShardFileOffset(0, partSize, partSize)
+ readers := make([]io.ReaderAt, len(latestDisks))
+ prefer := make([]bool, len(latestDisks))
+ checksumAlgo := erasureInfo.GetChecksumInfo(partNumber).Algorithm
+ for i, disk := range latestDisks {
+ if disk == OfflineDisk {
+ continue
+ }
+ thisPartErrs := shuffleCheckParts(dataErrsByPart[partIndex], latestMeta.Erasure.Distribution)
+ if thisPartErrs[i] != checkPartSuccess {
+ continue
+ }
+ checksumInfo := copyPartsMetadata[i].Erasure.GetChecksumInfo(partNumber)
+ partPath := pathJoin(object, srcDataDir, fmt.Sprintf("part.%d", partNumber))
+ readers[i] = newBitrotReader(disk, copyPartsMetadata[i].Data, bucket, partPath, tillOffset, checksumAlgo,
+ checksumInfo.Hash, erasure.ShardSize())
+ prefer[i] = disk.Hostname() == ""
+ }
+ writers := make([]io.Writer, len(outDatedDisks))
+ for i, disk := range outDatedDisks {
+ if disk == OfflineDisk {
+ continue
+ }
+ partPath := pathJoin(tmpID, dstDataDir, fmt.Sprintf("part.%d", partNumber))
+ if len(inlineBuffers) > 0 {
+ buf := grid.GetByteBufferCap(int(erasure.ShardFileSize(latestMeta.Size)) + 64)
+ inlineBuffers[i] = bytes.NewBuffer(buf[:0])
+ defer grid.PutByteBuffer(buf)
+
+ writers[i] = newStreamingBitrotWriterBuffer(inlineBuffers[i], DefaultBitrotAlgorithm, erasure.ShardSize())
+ } else {
+ writers[i] = newBitrotWriter(disk, bucket, minioMetaTmpBucket, partPath,
+ tillOffset, DefaultBitrotAlgorithm, erasure.ShardSize())
+ }
+ }
+
+ // Heal each part. erasure.Heal() will write the healed
+ // part to .minio/tmp/uuid/ which needs to be renamed
+ // later to the final location.
+ err = erasure.Heal(ctx, writers, readers, partSize, prefer)
+ closeBitrotReaders(readers)
+ closeErrs := closeBitrotWriters(writers)
+ if err != nil {
+ return result, err
+ }
+
+ // outDatedDisks that had write errors should not be
+ // written to for remaining parts, so we nil it out.
+ for i, disk := range outDatedDisks {
+ if disk == OfflineDisk {
+ continue
+ }
+
+ // A non-nil stale disk which did not receive
+ // a healed part checksum had a write error.
+ if writers[i] == nil {
+ outDatedDisks[i] = nil
+ disksToHealCount--
+ 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].AddObjectPart(partNumber, "", partSize, partActualSize, partModTime, partIdx, partChecksums)
+ if len(inlineBuffers) > 0 && inlineBuffers[i] != nil {
+ partsMetadata[i].Data = inlineBuffers[i].Bytes()
+ partsMetadata[i].SetInlineData()
+ } else {
+ partsMetadata[i].Data = nil
+ }
+ }
+
+ // If all disks are having errors, we give up.
+ if disksToHealCount == 0 {
+ return result, fmt.Errorf("all drives had write errors, unable to heal %s/%s", bucket, object)
+ }
+ }
+ }
+
+ defer er.deleteAll(context.Background(), minioMetaTmpBucket, tmpID)
+
+ // Rename from tmp location to the actual location.
+ for i, disk := range outDatedDisks {
+ if disk == OfflineDisk {
+ continue
+ }
+
+ // record the index of the updated disks
+ partsMetadata[i].Erasure.Index = i + 1
+
+ // Attempt a rename now from healed data to final location.
+ partsMetadata[i].SetHealing()
+
+ if _, err = disk.RenameData(ctx, minioMetaTmpBucket, tmpID, partsMetadata[i], bucket, object, RenameOptions{}); err != nil {
+ return result, err
+ }
+
+ // - Remove any remaining parts from outdated disks from before transition.
+ if partsMetadata[i].IsRemote() {
+ rmDataDir := partsMetadata[i].DataDir
+ disk.Delete(ctx, bucket, pathJoin(encodeDirObject(object), rmDataDir), DeleteOptions{
+ Immediate: true,
+ Recursive: true,
+ })
+ }
+
+ for i, v := range result.Before.Drives {
+ if v.Endpoint == disk.Endpoint().String() {
+ result.After.Drives[i].State = madmin.DriveStateOk
+ }
+ }
+ }
+
+ return result, nil
+}
+
+// checkAbandonedParts will check if an object has abandoned parts,
+// meaning data-dirs or inlined data that are no longer referenced by the xl.meta
+// Errors are generally ignored by this function.
+func (er *erasureObjects) checkAbandonedParts(ctx context.Context, bucket string, object string, opts madmin.HealOpts) (err error) {
+ if !opts.Remove || opts.DryRun {
+ return nil
+ }
+
+ if globalTrace.NumSubscribers(madmin.TraceHealing) > 0 {
+ startTime := time.Now()
+ defer func() {
+ healTrace(healingMetricCheckAbandonedParts, startTime, bucket, object, nil, err, nil)
+ }()
+ }
+ if !opts.NoLock {
+ lk := er.NewNSLock(bucket, object)
+ lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ return err
+ }
+ ctx = lkctx.Context()
+ defer lk.Unlock(lkctx)
+ }
+ var wg sync.WaitGroup
+ for _, disk := range er.getDisks() {
+ if disk != nil {
+ wg.Add(1)
+ go func(disk StorageAPI) {
+ defer wg.Done()
+ _ = disk.CleanAbandonedData(ctx, bucket, object)
+ }(disk)
+ }
+ }
+ wg.Wait()
+ return nil
+}
+
+// healObjectDir - heals object directory specifically, this special call
+// is needed since we do not have a special backend format for directories.
+func (er *erasureObjects) healObjectDir(ctx context.Context, bucket, object string, dryRun bool, remove bool) (hr madmin.HealResultItem, err error) {
+ storageDisks := er.getDisks()
+ storageEndpoints := er.getEndpoints()
+
+ // Initialize heal result object
+ hr = madmin.HealResultItem{
+ Type: madmin.HealItemObject,
+ Bucket: bucket,
+ Object: object,
+ DiskCount: len(storageDisks),
+ ParityBlocks: er.defaultParityCount,
+ DataBlocks: len(storageDisks) - er.defaultParityCount,
+ ObjectSize: 0,
+ }
+
+ hr.Before.Drives = make([]madmin.HealDriveInfo, len(storageDisks))
+ hr.After.Drives = make([]madmin.HealDriveInfo, len(storageDisks))
+
+ errs := statAllDirs(ctx, storageDisks, bucket, object)
+ danglingObject := isObjectDirDangling(errs)
+ if danglingObject {
+ if !dryRun && remove {
+ var wg sync.WaitGroup
+ // Remove versions in bulk for each disk
+ for index, disk := range storageDisks {
+ if disk == nil {
+ continue
+ }
+ wg.Add(1)
+ go func(index int, disk StorageAPI) {
+ defer wg.Done()
+ _ = disk.Delete(ctx, bucket, object, DeleteOptions{
+ Recursive: false,
+ Immediate: false,
+ })
+ }(index, disk)
+ }
+ wg.Wait()
+ }
+ }
+
+ // Prepare object creation in all disks
+ for i, err := range errs {
+ drive := storageEndpoints[i].String()
+ switch err {
+ case nil:
+ hr.Before.Drives[i] = madmin.HealDriveInfo{Endpoint: drive, State: madmin.DriveStateOk}
+ hr.After.Drives[i] = madmin.HealDriveInfo{Endpoint: drive, State: madmin.DriveStateOk}
+ case errDiskNotFound:
+ hr.Before.Drives[i] = madmin.HealDriveInfo{State: madmin.DriveStateOffline}
+ hr.After.Drives[i] = madmin.HealDriveInfo{State: madmin.DriveStateOffline}
+ case errVolumeNotFound, errFileNotFound:
+ // Bucket or prefix/directory not found
+ hr.Before.Drives[i] = madmin.HealDriveInfo{Endpoint: drive, State: madmin.DriveStateMissing}
+ hr.After.Drives[i] = madmin.HealDriveInfo{Endpoint: drive, State: madmin.DriveStateMissing}
+ default:
+ hr.Before.Drives[i] = madmin.HealDriveInfo{Endpoint: drive, State: madmin.DriveStateCorrupt}
+ hr.After.Drives[i] = madmin.HealDriveInfo{Endpoint: drive, State: madmin.DriveStateCorrupt}
+ }
+ }
+ if danglingObject || isAllNotFound(errs) {
+ // Nothing to do, file is already gone.
+ return hr, errFileNotFound
+ }
+
+ if dryRun {
+ // Quit without try to heal the object dir
+ return hr, nil
+ }
+
+ for i, err := range errs {
+ if err == errVolumeNotFound || err == errFileNotFound {
+ // Bucket or prefix/directory not found
+ merr := storageDisks[i].MakeVol(ctx, pathJoin(bucket, object))
+ switch merr {
+ case nil, errVolumeExists:
+ hr.After.Drives[i].State = madmin.DriveStateOk
+ case errDiskNotFound:
+ hr.After.Drives[i].State = madmin.DriveStateOffline
+ default:
+ hr.After.Drives[i].State = madmin.DriveStateCorrupt
+ }
+ }
+ }
+ return hr, nil
+}
+
+// Populates default heal result item entries with possible values when we are returning prematurely.
+// This is to ensure that in any circumstance we are not returning empty arrays with wrong values.
+func (er *erasureObjects) defaultHealResult(lfi FileInfo, storageDisks []StorageAPI, storageEndpoints []Endpoint, errs []error, bucket, object, versionID string) madmin.HealResultItem {
+ // Initialize heal result object
+ result := madmin.HealResultItem{
+ Type: madmin.HealItemObject,
+ Bucket: bucket,
+ Object: object,
+ ObjectSize: lfi.Size,
+ VersionID: versionID,
+ DiskCount: len(storageDisks),
+ }
+
+ if lfi.IsValid() {
+ result.ParityBlocks = lfi.Erasure.ParityBlocks
+ } else {
+ // Default to most common configuration for erasure blocks.
+ result.ParityBlocks = er.defaultParityCount
+ }
+ result.DataBlocks = len(storageDisks) - result.ParityBlocks
+
+ for index, disk := range storageDisks {
+ if disk == nil {
+ result.Before.Drives = append(result.Before.Drives, madmin.HealDriveInfo{
+ UUID: "",
+ Endpoint: storageEndpoints[index].String(),
+ State: madmin.DriveStateOffline,
+ })
+ result.After.Drives = append(result.After.Drives, madmin.HealDriveInfo{
+ UUID: "",
+ Endpoint: storageEndpoints[index].String(),
+ State: madmin.DriveStateOffline,
+ })
+ continue
+ }
+ driveState := objectErrToDriveState(errs[index])
+ result.Before.Drives = append(result.Before.Drives, madmin.HealDriveInfo{
+ UUID: "",
+ Endpoint: storageEndpoints[index].String(),
+ State: driveState,
+ })
+ result.After.Drives = append(result.After.Drives, madmin.HealDriveInfo{
+ UUID: "",
+ Endpoint: storageEndpoints[index].String(),
+ State: driveState,
+ })
+ }
+
+ return result
+}
+
+// Stat all directories.
+func statAllDirs(ctx context.Context, storageDisks []StorageAPI, bucket, prefix string) []error {
+ g := errgroup.WithNErrs(len(storageDisks))
+ for index, disk := range storageDisks {
+ if disk == nil {
+ continue
+ }
+ index := index
+ g.Go(func() error {
+ entries, err := storageDisks[index].ListDir(ctx, "", bucket, prefix, 1)
+ if err != nil {
+ return err
+ }
+ if len(entries) > 0 {
+ return errVolumeNotEmpty
+ }
+ return nil
+ }, index)
+ }
+
+ return g.Wait()
+}
+
+func isAllVolumeNotFound(errs []error) bool {
+ return countErrs(errs, errVolumeNotFound) == len(errs)
+}
+
+// isAllNotFound will return if any element of the error slice is not
+// errFileNotFound, errFileVersionNotFound or errVolumeNotFound.
+// A 0 length slice will always return false.
+func isAllNotFound(errs []error) bool {
+ for _, err := range errs {
+ if err != nil {
+ switch err.Error() {
+ case errFileNotFound.Error():
+ fallthrough
+ case errVolumeNotFound.Error():
+ fallthrough
+ case errFileVersionNotFound.Error():
+ continue
+ }
+ }
+ return false
+ }
+ return len(errs) > 0
+}
+
+// isAllBucketsNotFound will return true if all the errors are either errFileNotFound
+// or errFileCorrupt
+// A 0 length slice will always return false.
+func isAllBucketsNotFound(errs []error) bool {
+ if len(errs) == 0 {
+ return false
+ }
+ notFoundCount := 0
+ for _, err := range errs {
+ if err != nil {
+ if errors.Is(err, errVolumeNotFound) {
+ notFoundCount++
+ } else if isErrBucketNotFound(err) {
+ notFoundCount++
+ }
+ }
+ }
+ return len(errs) == notFoundCount
+}
+
+// ObjectDir is considered dangling/corrupted if any only
+// if total disks - a combination of corrupted and missing
+// files is lesser than N/2+1 number of disks.
+// If no files were found false will be returned.
+func isObjectDirDangling(errs []error) (ok bool) {
+ var found int
+ var notFound int
+ var foundNotEmpty int
+ var otherFound int
+ for _, readErr := range errs {
+ switch readErr {
+ case nil:
+ found++
+ case errFileNotFound, errVolumeNotFound:
+ notFound++
+ case errVolumeNotEmpty:
+ foundNotEmpty++
+ default:
+ otherFound++
+ }
+ }
+ found = found + foundNotEmpty + otherFound
+ return found < notFound && found > 0
+}
+
+func danglingMetaErrsCount(cerrs []error) (notFoundCount int, nonActionableCount int) {
+ for _, readErr := range cerrs {
+ if readErr == nil {
+ continue
+ }
+ switch {
+ case errors.Is(readErr, errFileNotFound) || errors.Is(readErr, errFileVersionNotFound):
+ notFoundCount++
+ default:
+ // All other errors are non-actionable
+ nonActionableCount++
+ }
+ }
+ return notFoundCount, nonActionableCount
+}
+
+func danglingPartErrsCount(results []int) (notFoundCount int, nonActionableCount int) {
+ for _, partResult := range results {
+ switch partResult {
+ case checkPartSuccess:
+ continue
+ case checkPartFileNotFound:
+ notFoundCount++
+ default:
+ // All other errors are non-actionable
+ nonActionableCount++
+ }
+ }
+ return notFoundCount, nonActionableCount
+}
+
+// Object is considered dangling/corrupted if and only
+// if total disks - a combination of corrupted and missing
+// files is lesser than number of data blocks.
+func isObjectDangling(metaArr []FileInfo, errs []error, dataErrsByPart map[int][]int) (validMeta FileInfo, ok bool) {
+ // We can consider an object data not reliable
+ // when xl.meta is not found in read quorum disks.
+ // or when xl.meta is not readable in read quorum disks.
+ notFoundMetaErrs, nonActionableMetaErrs := danglingMetaErrsCount(errs)
+
+ notFoundPartsErrs, nonActionablePartsErrs := 0, 0
+ for _, dataErrs := range dataErrsByPart {
+ if nf, na := danglingPartErrsCount(dataErrs); nf > notFoundPartsErrs {
+ notFoundPartsErrs, nonActionablePartsErrs = nf, na
+ }
+ }
+
+ for _, m := range metaArr {
+ if m.IsValid() {
+ validMeta = m
+ break
+ }
+ }
+
+ if !validMeta.IsValid() {
+ // validMeta is invalid because all xl.meta is missing apparently
+ // we should figure out if dataDirs are also missing > dataBlocks.
+ dataBlocks := (len(metaArr) + 1) / 2
+ if notFoundPartsErrs > dataBlocks {
+ // Not using parity to ensure that we do not delete
+ // any valid content, if any is recoverable. But if
+ // notFoundDataDirs are already greater than the data
+ // blocks all bets are off and it is safe to purge.
+ //
+ // This is purely a defensive code, ideally parityBlocks
+ // is sufficient, however we can't know that since we
+ // do have the FileInfo{}.
+ return validMeta, true
+ }
+
+ // We have no idea what this file is, leave it as is.
+ return validMeta, false
+ }
+
+ if nonActionableMetaErrs > 0 || nonActionablePartsErrs > 0 {
+ return validMeta, false
+ }
+
+ if validMeta.Deleted {
+ // notFoundPartsErrs is ignored since
+ // - delete marker does not have any parts
+ dataBlocks := (len(errs) + 1) / 2
+ return validMeta, notFoundMetaErrs > dataBlocks
+ }
+
+ // TODO: It is possible to replay the object via just single
+ // xl.meta file, considering quorum number of data-dirs are still
+ // present on other drives.
+ //
+ // However this requires a bit of a rewrite, leave this up for
+ // future work.
+ if notFoundMetaErrs > 0 && notFoundMetaErrs > validMeta.Erasure.ParityBlocks {
+ // All xl.meta is beyond parity blocks missing, this is dangling
+ return validMeta, true
+ }
+
+ if !validMeta.IsRemote() && notFoundPartsErrs > 0 && notFoundPartsErrs > validMeta.Erasure.ParityBlocks {
+ // All data-dir is beyond parity blocks missing, this is dangling
+ return validMeta, true
+ }
+
+ return validMeta, false
+}
+
+// HealObject - heal the given object, automatically deletes the object if stale/corrupted if `remove` is true.
+func (er erasureObjects) HealObject(ctx context.Context, bucket, object, versionID string, opts madmin.HealOpts) (hr madmin.HealResultItem, err error) {
+ // Create context that also contains information about the object and bucket.
+ // The top level handler might not have this information.
+ reqInfo := logger.GetReqInfo(ctx)
+ var newReqInfo *logger.ReqInfo
+ if reqInfo != nil {
+ newReqInfo = logger.NewReqInfo(reqInfo.RemoteHost, reqInfo.UserAgent, reqInfo.DeploymentID, reqInfo.RequestID, reqInfo.API, bucket, object)
+ } else {
+ newReqInfo = logger.NewReqInfo("", "", globalDeploymentID(), "", "Heal", bucket, object)
+ }
+ healCtx := logger.SetReqInfo(GlobalContext, newReqInfo)
+
+ // Healing directories handle it separately.
+ if HasSuffix(object, SlashSeparator) {
+ hr, err := er.healObjectDir(healCtx, bucket, object, opts.DryRun, opts.Remove)
+ return hr, toObjectErr(err, bucket, object)
+ }
+
+ storageDisks := er.getDisks()
+ storageEndpoints := er.getEndpoints()
+
+ // When versionID is empty, we read directly from the `null` versionID for healing.
+ if versionID == "" {
+ versionID = nullVersionID
+ }
+
+ // Perform quick read without lock.
+ // This allows to quickly check if all is ok or all are missing.
+ _, errs := readAllFileInfo(healCtx, storageDisks, "", bucket, object, versionID, false, false)
+ if isAllNotFound(errs) {
+ err := errFileNotFound
+ if versionID != "" {
+ err = errFileVersionNotFound
+ }
+ // Nothing to do, file is already gone.
+ return er.defaultHealResult(FileInfo{}, storageDisks, storageEndpoints,
+ errs, bucket, object, versionID), toObjectErr(err, bucket, object, versionID)
+ }
+
+ // Heal the object.
+ hr, err = er.healObject(healCtx, bucket, object, versionID, opts)
+ if errors.Is(err, errFileCorrupt) && opts.ScanMode != madmin.HealDeepScan {
+ // Instead of returning an error when a bitrot error is detected
+ // during a normal heal scan, heal again with bitrot flag enabled.
+ opts.ScanMode = madmin.HealDeepScan
+ hr, err = er.healObject(healCtx, bucket, object, versionID, opts)
+ }
+ return hr, toObjectErr(err, bucket, object, versionID)
+}
+
+// healTrace sends healing results to trace output.
+func healTrace(funcName healingMetric, startTime time.Time, bucket, object string, opts *madmin.HealOpts, err error, result *madmin.HealResultItem) {
+ tr := madmin.TraceInfo{
+ TraceType: madmin.TraceHealing,
+ Time: startTime,
+ NodeName: globalLocalNodeName,
+ FuncName: "heal." + funcName.String(),
+ Duration: time.Since(startTime),
+ Path: pathJoin(bucket, decodeDirObject(object)),
+ }
+ if opts != nil {
+ tr.Custom = map[string]string{
+ "dry": fmt.Sprint(opts.DryRun),
+ "remove": fmt.Sprint(opts.Remove),
+ "mode": fmt.Sprint(opts.ScanMode),
+ }
+ if result != nil {
+ tr.Custom["version-id"] = result.VersionID
+ tr.Custom["disks"] = strconv.Itoa(result.DiskCount)
+ tr.Bytes = result.ObjectSize
+ }
+ }
+ if err != nil {
+ tr.Error = err.Error()
+ }
+ tr.HealResult = result
+ globalTrace.Publish(tr)
+}
diff --git a/cmd/erasure-healing_test.go b/cmd/erasure-healing_test.go
new file mode 100644
index 00000000..c19fddd0
--- /dev/null
+++ b/cmd/erasure-healing_test.go
@@ -0,0 +1,1769 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "crypto/rand"
+ "crypto/sha256"
+ "errors"
+ "io"
+ "os"
+ "path"
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/dustin/go-humanize"
+ uuid2 "github.com/google/uuid"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/config/storageclass"
+)
+
+// Tests isObjectDangling function
+func TestIsObjectDangling(t *testing.T) {
+ fi := newFileInfo("test-object", 2, 2)
+ fi.Erasure.Index = 1
+
+ ifi := newFileInfo("test-object", 2, 2)
+ ifi.SetInlineData()
+ ifi.Erasure.Index = 1
+
+ testCases := []struct {
+ name string
+ metaArr []FileInfo
+ errs []error
+ dataErrs map[int][]int
+ expectedMeta FileInfo
+ expectedDangling bool
+ }{
+ {
+ name: "FileInfoExists-case1",
+ metaArr: []FileInfo{
+ {},
+ {},
+ fi,
+ fi,
+ },
+ errs: []error{
+ errFileNotFound,
+ errDiskNotFound,
+ nil,
+ nil,
+ },
+ dataErrs: nil,
+ expectedMeta: fi,
+ expectedDangling: false,
+ },
+ {
+ name: "FileInfoExists-case2",
+ metaArr: []FileInfo{
+ {},
+ {},
+ fi,
+ fi,
+ },
+ errs: []error{
+ errFileNotFound,
+ errFileNotFound,
+ nil,
+ nil,
+ },
+ dataErrs: nil,
+ expectedMeta: fi,
+ expectedDangling: false,
+ },
+ {
+ name: "FileInfoUndecided-case1",
+ metaArr: []FileInfo{
+ {},
+ {},
+ {},
+ fi,
+ },
+ errs: []error{
+ errFileNotFound,
+ errDiskNotFound,
+ errDiskNotFound,
+ nil,
+ },
+ dataErrs: nil,
+ expectedMeta: fi,
+ expectedDangling: false,
+ },
+ {
+ name: "FileInfoUndecided-case2",
+ metaArr: []FileInfo{},
+ errs: []error{
+ errFileNotFound,
+ errDiskNotFound,
+ errDiskNotFound,
+ errFileNotFound,
+ },
+ dataErrs: nil,
+ expectedMeta: FileInfo{},
+ expectedDangling: false,
+ },
+ {
+ name: "FileInfoUndecided-case3(file deleted)",
+ metaArr: []FileInfo{},
+ errs: []error{
+ errFileNotFound,
+ errFileNotFound,
+ errFileNotFound,
+ errFileNotFound,
+ },
+ dataErrs: nil,
+ expectedMeta: FileInfo{},
+ expectedDangling: false,
+ },
+ {
+ name: "FileInfoUnDecided-case4",
+ metaArr: []FileInfo{
+ {},
+ {},
+ {},
+ ifi,
+ },
+ errs: []error{
+ errFileNotFound,
+ errFileCorrupt,
+ errFileCorrupt,
+ nil,
+ },
+ dataErrs: nil,
+ expectedMeta: ifi,
+ expectedDangling: false,
+ },
+ {
+ name: "FileInfoUnDecided-case5-(ignore errFileCorrupt error)",
+ metaArr: []FileInfo{
+ {},
+ {},
+ {},
+ fi,
+ },
+ errs: []error{
+ errFileNotFound,
+ errFileCorrupt,
+ nil,
+ nil,
+ },
+ dataErrs: map[int][]int{
+ 0: {checkPartFileCorrupt, checkPartFileNotFound, checkPartSuccess, checkPartFileCorrupt},
+ },
+ expectedMeta: fi,
+ expectedDangling: false,
+ },
+ {
+ name: "FileInfoUnDecided-case6-(data-dir intact)",
+ metaArr: []FileInfo{
+ {},
+ {},
+ {},
+ fi,
+ },
+ errs: []error{
+ errFileNotFound,
+ errFileNotFound,
+ errFileNotFound,
+ nil,
+ },
+ dataErrs: map[int][]int{
+ 0: {checkPartFileNotFound, checkPartFileCorrupt, checkPartSuccess, checkPartSuccess},
+ },
+ expectedMeta: fi,
+ expectedDangling: false,
+ },
+ {
+ name: "FileInfoDecided-case1",
+ metaArr: []FileInfo{
+ {},
+ {},
+ {},
+ ifi,
+ },
+ errs: []error{
+ errFileNotFound,
+ errFileNotFound,
+ errFileNotFound,
+ nil,
+ },
+ dataErrs: nil,
+ expectedMeta: ifi,
+ expectedDangling: true,
+ },
+ {
+ name: "FileInfoDecided-case2-delete-marker",
+ metaArr: []FileInfo{
+ {},
+ {},
+ {},
+ {Deleted: true},
+ },
+ errs: []error{
+ errFileNotFound,
+ errFileNotFound,
+ errFileNotFound,
+ nil,
+ },
+ dataErrs: nil,
+ expectedMeta: FileInfo{Deleted: true},
+ expectedDangling: true,
+ },
+ {
+ name: "FileInfoDecided-case3-(enough data-dir missing)",
+ metaArr: []FileInfo{
+ {},
+ {},
+ {},
+ fi,
+ },
+ errs: []error{
+ errFileNotFound,
+ errFileNotFound,
+ nil,
+ nil,
+ },
+ dataErrs: map[int][]int{
+ 0: {checkPartFileNotFound, checkPartFileNotFound, checkPartSuccess, checkPartFileNotFound},
+ },
+ expectedMeta: fi,
+ expectedDangling: true,
+ },
+ {
+ name: "FileInfoDecided-case4-(missing data-dir for part 2)",
+ metaArr: []FileInfo{
+ {},
+ {},
+ {},
+ fi,
+ },
+ errs: []error{
+ errFileNotFound,
+ errFileNotFound,
+ nil,
+ nil,
+ },
+ dataErrs: map[int][]int{
+ 0: {checkPartSuccess, checkPartSuccess, checkPartSuccess, checkPartSuccess},
+ 1: {checkPartSuccess, checkPartFileNotFound, checkPartFileNotFound, checkPartFileNotFound},
+ },
+ expectedMeta: fi,
+ expectedDangling: true,
+ },
+
+ {
+ name: "FileInfoDecided-case4-(enough data-dir existing for each part)",
+ metaArr: []FileInfo{
+ {},
+ {},
+ {},
+ fi,
+ },
+ errs: []error{
+ errFileNotFound,
+ errFileNotFound,
+ nil,
+ nil,
+ },
+ dataErrs: map[int][]int{
+ 0: {checkPartFileNotFound, checkPartSuccess, checkPartSuccess, checkPartSuccess},
+ 1: {checkPartSuccess, checkPartFileNotFound, checkPartSuccess, checkPartSuccess},
+ 2: {checkPartSuccess, checkPartSuccess, checkPartFileNotFound, checkPartSuccess},
+ 3: {checkPartSuccess, checkPartSuccess, checkPartSuccess, checkPartFileNotFound},
+ },
+ expectedMeta: fi,
+ expectedDangling: false,
+ },
+
+ // Add new cases as seen
+ }
+ for _, testCase := range testCases {
+ t.Run(testCase.name, func(t *testing.T) {
+ gotMeta, dangling := isObjectDangling(testCase.metaArr, testCase.errs, testCase.dataErrs)
+ if !gotMeta.Equals(testCase.expectedMeta) {
+ t.Errorf("Expected %#v, got %#v", testCase.expectedMeta, gotMeta)
+ }
+ if dangling != testCase.expectedDangling {
+ t.Errorf("Expected dangling %t, got %t", testCase.expectedDangling, dangling)
+ }
+ })
+ }
+}
+
+// Tests both object and bucket healing.
+func TestHealing(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ obj, fsDirs, err := prepareErasure16(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer obj.Shutdown(t.Context())
+
+ // initialize the server and obtain the credentials and root.
+ // credentials are necessary to sign the HTTP request.
+ if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil {
+ t.Fatalf("Unable to initialize server config. %s", err)
+ }
+
+ defer removeRoots(fsDirs)
+
+ z := obj.(*erasureServerPools)
+ er := z.serverPools[0].sets[0]
+
+ // Create "bucket"
+ err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ bucket := "bucket"
+ object := "object"
+
+ data := make([]byte, 1*humanize.MiByte)
+ length := int64(len(data))
+ _, err = rand.Read(data)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader(data), length, "", ""), ObjectOptions{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ disk := er.getDisks()[0]
+ fileInfoPreHeal, err := disk.ReadVersion(t.Context(), "", bucket, object, "", ReadOptions{ReadData: false, Healing: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Remove the object - to simulate the case where the disk was down when the object
+ // was created.
+ err = removeAll(pathJoin(disk.String(), bucket, object))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Checking abandoned parts should do nothing
+ err = er.checkAbandonedParts(ctx, bucket, object, madmin.HealOpts{ScanMode: madmin.HealNormalScan, Remove: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = er.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fileInfoPostHeal, err := disk.ReadVersion(t.Context(), "", bucket, object, "", ReadOptions{ReadData: false, Healing: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // After heal the meta file should be as expected.
+ if !fileInfoPreHeal.Equals(fileInfoPostHeal) {
+ t.Fatal("HealObject failed")
+ }
+
+ err = os.RemoveAll(path.Join(fsDirs[0], bucket, object, "xl.meta"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Write xl.meta with different modtime to simulate the case where a disk had
+ // gone down when an object was replaced by a new object.
+ fileInfoOutDated := fileInfoPreHeal
+ fileInfoOutDated.ModTime = time.Now()
+ err = disk.WriteMetadata(t.Context(), "", bucket, object, fileInfoOutDated)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = er.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealDeepScan})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fileInfoPostHeal, err = disk.ReadVersion(t.Context(), "", bucket, object, "", ReadOptions{ReadData: false, Healing: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // After heal the meta file should be as expected.
+ if !fileInfoPreHeal.Equals(fileInfoPostHeal) {
+ t.Fatal("HealObject failed")
+ }
+
+ uuid, _ := uuid2.NewRandom()
+ for _, drive := range fsDirs {
+ dir := path.Join(drive, bucket, object, uuid.String())
+ err = os.MkdirAll(dir, os.ModePerm)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = os.WriteFile(pathJoin(dir, "part.1"), []byte("some data"), os.ModePerm)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ // This should remove all the unreferenced parts.
+ err = er.checkAbandonedParts(ctx, bucket, object, madmin.HealOpts{ScanMode: madmin.HealNormalScan, Remove: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, drive := range fsDirs {
+ dir := path.Join(drive, bucket, object, uuid.String())
+ _, err := os.ReadFile(pathJoin(dir, "part.1"))
+ if err == nil {
+ t.Fatal("expected data dit to be cleaned up")
+ }
+ }
+
+ // Remove the bucket - to simulate the case where bucket was
+ // created when the disk was down.
+ err = os.RemoveAll(path.Join(fsDirs[0], bucket))
+ if err != nil {
+ t.Fatal(err)
+ }
+ // This would create the bucket.
+ _, err = obj.HealBucket(ctx, bucket, madmin.HealOpts{
+ DryRun: false,
+ Remove: false,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Stat the bucket to make sure that it was created.
+ _, err = er.getDisks()[0].StatVol(t.Context(), bucket)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+// Tests both object and bucket healing.
+func TestHealingVersioned(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ obj, fsDirs, err := prepareErasure16(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer obj.Shutdown(t.Context())
+
+ // initialize the server and obtain the credentials and root.
+ // credentials are necessary to sign the HTTP request.
+ if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil {
+ t.Fatalf("Unable to initialize server config. %s", err)
+ }
+
+ defer removeRoots(fsDirs)
+
+ z := obj.(*erasureServerPools)
+ er := z.serverPools[0].sets[0]
+
+ // Create "bucket"
+ err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{VersioningEnabled: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ bucket := "bucket"
+ object := "object"
+
+ data := make([]byte, 1*humanize.MiByte)
+ length := int64(len(data))
+ _, err = rand.Read(data)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ oi1, err := obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader(data), length, "", ""), ObjectOptions{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ // 2nd version.
+ _, _ = rand.Read(data)
+ oi2, err := obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader(data), length, "", ""), ObjectOptions{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ disk := er.getDisks()[0]
+ fileInfoPreHeal1, err := disk.ReadVersion(t.Context(), "", bucket, object, oi1.VersionID, ReadOptions{ReadData: false, Healing: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+ fileInfoPreHeal2, err := disk.ReadVersion(t.Context(), "", bucket, object, oi2.VersionID, ReadOptions{ReadData: false, Healing: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Remove the object - to simulate the case where the disk was down when the object
+ // was created.
+ err = removeAll(pathJoin(disk.String(), bucket, object))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Checking abandoned parts should do nothing
+ err = er.checkAbandonedParts(ctx, bucket, object, madmin.HealOpts{ScanMode: madmin.HealNormalScan, Remove: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = er.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fileInfoPostHeal1, err := disk.ReadVersion(t.Context(), "", bucket, object, oi1.VersionID, ReadOptions{ReadData: false, Healing: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+ fileInfoPostHeal2, err := disk.ReadVersion(t.Context(), "", bucket, object, oi2.VersionID, ReadOptions{ReadData: false, Healing: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // After heal the meta file should be as expected.
+ if !fileInfoPreHeal1.Equals(fileInfoPostHeal1) {
+ t.Fatal("HealObject failed")
+ }
+ if !fileInfoPreHeal1.Equals(fileInfoPostHeal2) {
+ t.Fatal("HealObject failed")
+ }
+
+ err = os.RemoveAll(path.Join(fsDirs[0], bucket, object, "xl.meta"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Write xl.meta with different modtime to simulate the case where a disk had
+ // gone down when an object was replaced by a new object.
+ fileInfoOutDated := fileInfoPreHeal1
+ fileInfoOutDated.ModTime = time.Now()
+ err = disk.WriteMetadata(t.Context(), "", bucket, object, fileInfoOutDated)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = er.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealDeepScan})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fileInfoPostHeal1, err = disk.ReadVersion(t.Context(), "", bucket, object, "", ReadOptions{ReadData: false, Healing: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // After heal the meta file should be as expected.
+ if !fileInfoPreHeal1.Equals(fileInfoPostHeal1) {
+ t.Fatal("HealObject failed")
+ }
+
+ fileInfoPostHeal2, err = disk.ReadVersion(t.Context(), "", bucket, object, "", ReadOptions{ReadData: false, Healing: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // After heal the meta file should be as expected.
+ if !fileInfoPreHeal2.Equals(fileInfoPostHeal2) {
+ t.Fatal("HealObject failed")
+ }
+
+ uuid, _ := uuid2.NewRandom()
+ for _, drive := range fsDirs {
+ dir := path.Join(drive, bucket, object, uuid.String())
+ err = os.MkdirAll(dir, os.ModePerm)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = os.WriteFile(pathJoin(dir, "part.1"), []byte("some data"), os.ModePerm)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ // This should remove all the unreferenced parts.
+ err = er.checkAbandonedParts(ctx, bucket, object, madmin.HealOpts{ScanMode: madmin.HealNormalScan, Remove: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, drive := range fsDirs {
+ dir := path.Join(drive, bucket, object, uuid.String())
+ _, err := os.ReadFile(pathJoin(dir, "part.1"))
+ if err == nil {
+ t.Fatal("expected data dit to be cleaned up")
+ }
+ }
+
+ // Remove the bucket - to simulate the case where bucket was
+ // created when the disk was down.
+ err = os.RemoveAll(path.Join(fsDirs[0], bucket))
+ if err != nil {
+ t.Fatal(err)
+ }
+ // This would create the bucket.
+ _, err = obj.HealBucket(ctx, bucket, madmin.HealOpts{
+ DryRun: false,
+ Remove: false,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Stat the bucket to make sure that it was created.
+ _, err = er.getDisks()[0].StatVol(t.Context(), bucket)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestHealingDanglingObject(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ resetGlobalHealState()
+ defer resetGlobalHealState()
+
+ // Set globalStorageClass.STANDARD to EC:4 for this test
+ saveSC := globalStorageClass
+ defer func() {
+ globalStorageClass.Update(saveSC)
+ }()
+ globalStorageClass.Update(storageclass.Config{
+ Standard: storageclass.StorageClass{
+ Parity: 4,
+ },
+ })
+
+ nDisks := 16
+ fsDirs, err := getRandomDisks(nDisks)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ defer removeRoots(fsDirs)
+
+ // Everything is fine, should return nil
+ objLayer, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, fsDirs...))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ setObjectLayer(objLayer)
+
+ bucket := getRandomBucketName()
+ object := getRandomObjectName()
+ data := bytes.Repeat([]byte("a"), 128*1024)
+
+ err = objLayer.MakeBucket(ctx, bucket, MakeBucketOptions{})
+ if err != nil {
+ t.Fatalf("Failed to make a bucket - %v", err)
+ }
+
+ disks := objLayer.(*erasureServerPools).serverPools[0].erasureDisks[0]
+ orgDisks := append([]StorageAPI{}, disks...)
+
+ // Enable versioning.
+ globalBucketMetadataSys.Update(ctx, bucket, bucketVersioningConfig, []byte(`Enabled`))
+
+ _, err = objLayer.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), ObjectOptions{
+ Versioned: true,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ setDisks := func(newDisks ...StorageAPI) {
+ objLayer.(*erasureServerPools).serverPools[0].erasureDisksMu.Lock()
+ copy(disks, newDisks)
+ objLayer.(*erasureServerPools).serverPools[0].erasureDisksMu.Unlock()
+ }
+ getDisk := func(n int) StorageAPI {
+ objLayer.(*erasureServerPools).serverPools[0].erasureDisksMu.Lock()
+ disk := disks[n]
+ objLayer.(*erasureServerPools).serverPools[0].erasureDisksMu.Unlock()
+ return disk
+ }
+
+ // Remove 4 disks.
+ setDisks(nil, nil, nil, nil)
+
+ // Create delete marker under quorum.
+ objInfo, err := objLayer.DeleteObject(ctx, bucket, object, ObjectOptions{Versioned: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Restore...
+ setDisks(orgDisks[:4]...)
+
+ fileInfoPreHeal, err := disks[0].ReadVersion(t.Context(), "", bucket, object, "", ReadOptions{ReadData: false, Healing: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if fileInfoPreHeal.NumVersions != 1 {
+ t.Fatalf("Expected versions 1, got %d", fileInfoPreHeal.NumVersions)
+ }
+
+ if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Recursive: true, Remove: true},
+ func(bucket, object, vid string, scanMode madmin.HealScanMode) error {
+ _, err := objLayer.HealObject(ctx, bucket, object, vid, madmin.HealOpts{ScanMode: scanMode, Remove: true})
+ return err
+ }); err != nil {
+ t.Fatal(err)
+ }
+
+ fileInfoPostHeal, err := disks[0].ReadVersion(t.Context(), "", bucket, object, "", ReadOptions{ReadData: false, Healing: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if fileInfoPostHeal.NumVersions != 2 {
+ t.Fatalf("Expected versions 2, got %d", fileInfoPreHeal.NumVersions)
+ }
+
+ if objInfo.DeleteMarker {
+ if _, err = objLayer.DeleteObject(ctx, bucket, object, ObjectOptions{
+ Versioned: true,
+ VersionID: objInfo.VersionID,
+ }); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ setDisks(nil, nil, nil, nil)
+
+ rd := mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", "")
+ _, err = objLayer.PutObject(ctx, bucket, object, rd, ObjectOptions{
+ Versioned: true,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ setDisks(orgDisks[:4]...)
+ disk := getDisk(0)
+ fileInfoPreHeal, err = disk.ReadVersion(t.Context(), "", bucket, object, "", ReadOptions{ReadData: false, Healing: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if fileInfoPreHeal.NumVersions != 1 {
+ t.Fatalf("Expected versions 1, got %d", fileInfoPreHeal.NumVersions)
+ }
+
+ if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Recursive: true, Remove: true},
+ func(bucket, object, vid string, scanMode madmin.HealScanMode) error {
+ _, err := objLayer.HealObject(ctx, bucket, object, vid, madmin.HealOpts{ScanMode: scanMode, Remove: true})
+ return err
+ }); err != nil {
+ t.Fatal(err)
+ }
+
+ disk = getDisk(0)
+ fileInfoPostHeal, err = disk.ReadVersion(t.Context(), "", bucket, object, "", ReadOptions{ReadData: false, Healing: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if fileInfoPostHeal.NumVersions != 2 {
+ t.Fatalf("Expected versions 2, got %d", fileInfoPreHeal.NumVersions)
+ }
+
+ rd = mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", "")
+ objInfo, err = objLayer.PutObject(ctx, bucket, object, rd, ObjectOptions{
+ Versioned: true,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ setDisks(nil, nil, nil, nil)
+
+ // Create delete marker under quorum.
+ _, err = objLayer.DeleteObject(ctx, bucket, object, ObjectOptions{
+ Versioned: true,
+ VersionID: objInfo.VersionID,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ setDisks(orgDisks[:4]...)
+
+ disk = getDisk(0)
+ fileInfoPreHeal, err = disk.ReadVersion(t.Context(), "", bucket, object, "", ReadOptions{ReadData: false, Healing: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if fileInfoPreHeal.NumVersions != 3 {
+ t.Fatalf("Expected versions 3, got %d", fileInfoPreHeal.NumVersions)
+ }
+
+ if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Recursive: true, Remove: true},
+ func(bucket, object, vid string, scanMode madmin.HealScanMode) error {
+ _, err := objLayer.HealObject(ctx, bucket, object, vid, madmin.HealOpts{ScanMode: scanMode, Remove: true})
+ return err
+ }); err != nil {
+ t.Fatal(err)
+ }
+
+ disk = getDisk(0)
+ fileInfoPostHeal, err = disk.ReadVersion(t.Context(), "", bucket, object, "", ReadOptions{ReadData: false, Healing: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if fileInfoPostHeal.NumVersions != 2 {
+ t.Fatalf("Expected versions 2, got %d", fileInfoPreHeal.NumVersions)
+ }
+}
+
+func TestHealCorrectQuorum(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ resetGlobalHealState()
+ defer resetGlobalHealState()
+
+ nDisks := 32
+ fsDirs, err := getRandomDisks(nDisks)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ defer removeRoots(fsDirs)
+
+ pools := mustGetPoolEndpoints(0, fsDirs[:16]...)
+ pools = append(pools, mustGetPoolEndpoints(1, fsDirs[16:]...)...)
+
+ // Everything is fine, should return nil
+ objLayer, _, err := initObjectLayer(ctx, pools)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ bucket := getRandomBucketName()
+ object := getRandomObjectName()
+ data := bytes.Repeat([]byte("a"), 5*1024*1024)
+ var opts ObjectOptions
+
+ err = objLayer.MakeBucket(ctx, bucket, MakeBucketOptions{})
+ if err != nil {
+ t.Fatalf("Failed to make a bucket - %v", err)
+ }
+
+ // Create an object with multiple parts uploaded in decreasing
+ // part number.
+ res, err := objLayer.NewMultipartUpload(ctx, bucket, object, opts)
+ if err != nil {
+ t.Fatalf("Failed to create a multipart upload - %v", err)
+ }
+
+ var uploadedParts []CompletePart
+ for _, partID := range []int{2, 1} {
+ pInfo, err1 := objLayer.PutObjectPart(ctx, bucket, object, res.UploadID, partID, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), opts)
+ if err1 != nil {
+ t.Fatalf("Failed to upload a part - %v", err1)
+ }
+ uploadedParts = append(uploadedParts, CompletePart{
+ PartNumber: pInfo.PartNumber,
+ ETag: pInfo.ETag,
+ })
+ }
+
+ _, err = objLayer.CompleteMultipartUpload(ctx, bucket, object, res.UploadID, uploadedParts, ObjectOptions{})
+ if err != nil {
+ t.Fatalf("Failed to complete multipart upload - got: %v", err)
+ }
+
+ cfgFile := pathJoin(bucketMetaPrefix, bucket, ".test.bin")
+ if err = saveConfig(ctx, objLayer, cfgFile, data); err != nil {
+ t.Fatal(err)
+ }
+
+ hopts := madmin.HealOpts{
+ DryRun: false,
+ Remove: true,
+ ScanMode: madmin.HealNormalScan,
+ }
+
+ // Test 1: Remove the object backend files from the first disk.
+ z := objLayer.(*erasureServerPools)
+ for _, set := range z.serverPools {
+ er := set.sets[0]
+ erasureDisks := er.getDisks()
+
+ fileInfos, errs := readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true)
+ nfi, err := getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs)
+ if errors.Is(err, errFileNotFound) {
+ continue
+ }
+ if err != nil {
+ t.Fatalf("Failed to getLatestFileInfo - %v", err)
+ }
+
+ for i := 0; i < nfi.Erasure.ParityBlocks; i++ {
+ erasureDisks[i].Delete(t.Context(), bucket, pathJoin(object, xlStorageFormatFile), DeleteOptions{
+ Recursive: false,
+ Immediate: false,
+ })
+ }
+
+ // Try healing now, it should heal the content properly.
+ _, err = objLayer.HealObject(ctx, bucket, object, "", hopts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fileInfos, errs = readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true)
+ if countErrs(errs, nil) != len(fileInfos) {
+ t.Fatal("Expected all xl.meta healed, but partial heal detected")
+ }
+
+ fileInfos, errs = readAllFileInfo(ctx, erasureDisks, "", minioMetaBucket, cfgFile, "", false, true)
+ nfi, err = getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs)
+ if errors.Is(err, errFileNotFound) {
+ continue
+ }
+ if err != nil {
+ t.Fatalf("Failed to getLatestFileInfo - %v", err)
+ }
+
+ for i := 0; i < nfi.Erasure.ParityBlocks; i++ {
+ erasureDisks[i].Delete(t.Context(), minioMetaBucket, pathJoin(cfgFile, xlStorageFormatFile), DeleteOptions{
+ Recursive: false,
+ Immediate: false,
+ })
+ }
+
+ // Try healing now, it should heal the content properly.
+ _, err = objLayer.HealObject(ctx, minioMetaBucket, cfgFile, "", hopts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fileInfos, errs = readAllFileInfo(ctx, erasureDisks, "", minioMetaBucket, cfgFile, "", false, true)
+ if countErrs(errs, nil) != len(fileInfos) {
+ t.Fatal("Expected all xl.meta healed, but partial heal detected")
+ }
+ }
+}
+
+func TestHealObjectCorruptedPools(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ resetGlobalHealState()
+ defer resetGlobalHealState()
+
+ nDisks := 32
+ fsDirs, err := getRandomDisks(nDisks)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ defer removeRoots(fsDirs)
+
+ pools := mustGetPoolEndpoints(0, fsDirs[:16]...)
+ pools = append(pools, mustGetPoolEndpoints(1, fsDirs[16:]...)...)
+
+ // Everything is fine, should return nil
+ objLayer, _, err := initObjectLayer(ctx, pools)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ bucket := getRandomBucketName()
+ object := getRandomObjectName()
+ data := bytes.Repeat([]byte("a"), 5*1024*1024)
+ var opts ObjectOptions
+
+ err = objLayer.MakeBucket(ctx, bucket, MakeBucketOptions{})
+ if err != nil {
+ t.Fatalf("Failed to make a bucket - %v", err)
+ }
+
+ // Upload a multipart object in the second pool
+ z := objLayer.(*erasureServerPools)
+ set := z.serverPools[1]
+
+ res, err := set.NewMultipartUpload(ctx, bucket, object, opts)
+ if err != nil {
+ t.Fatalf("Failed to create a multipart upload - %v", err)
+ }
+ uploadID := res.UploadID
+
+ var uploadedParts []CompletePart
+ for _, partID := range []int{2, 1} {
+ pInfo, err1 := set.PutObjectPart(ctx, bucket, object, uploadID, partID, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), opts)
+ if err1 != nil {
+ t.Fatalf("Failed to upload a part - %v", err1)
+ }
+ uploadedParts = append(uploadedParts, CompletePart{
+ PartNumber: pInfo.PartNumber,
+ ETag: pInfo.ETag,
+ })
+ }
+
+ _, err = set.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, ObjectOptions{})
+ if err != nil {
+ t.Fatalf("Failed to complete multipart upload - %v", err)
+ }
+
+ // Test 1: Remove the object backend files from the first disk.
+ er := set.sets[0]
+ erasureDisks := er.getDisks()
+ firstDisk := erasureDisks[0]
+ err = firstDisk.Delete(t.Context(), bucket, pathJoin(object, xlStorageFormatFile), DeleteOptions{
+ Recursive: false,
+ Immediate: false,
+ })
+ if err != nil {
+ t.Fatalf("Failed to delete a file - %v", err)
+ }
+
+ _, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
+ if err != nil {
+ t.Fatalf("Failed to heal object - %v", err)
+ }
+
+ fileInfos, errs := readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true)
+ fi, err := getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs)
+ if err != nil {
+ t.Fatalf("Failed to getLatestFileInfo - %v", err)
+ }
+
+ if _, err = firstDisk.StatInfoFile(t.Context(), bucket, object+"/"+xlStorageFormatFile, false); err != nil {
+ t.Errorf("Expected xl.meta file to be present but stat failed - %v", err)
+ }
+
+ err = firstDisk.Delete(t.Context(), bucket, pathJoin(object, fi.DataDir, "part.1"), DeleteOptions{
+ Recursive: false,
+ Immediate: false,
+ })
+ if err != nil {
+ t.Errorf("Failure during deleting part.1 - %v", err)
+ }
+
+ err = firstDisk.WriteAll(t.Context(), bucket, pathJoin(object, fi.DataDir, "part.1"), []byte{})
+ if err != nil {
+ t.Errorf("Failure during creating part.1 - %v", err)
+ }
+
+ _, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{DryRun: false, Remove: true, ScanMode: madmin.HealDeepScan})
+ if err != nil {
+ t.Errorf("Expected nil but received %v", err)
+ }
+
+ fileInfos, errs = readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true)
+ nfi, err := getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs)
+ if err != nil {
+ t.Fatalf("Failed to getLatestFileInfo - %v", err)
+ }
+
+ if !reflect.DeepEqual(fi, nfi) {
+ t.Fatalf("FileInfo not equal after healing: %v != %v", fi, nfi)
+ }
+
+ err = firstDisk.Delete(t.Context(), bucket, pathJoin(object, fi.DataDir, "part.1"), DeleteOptions{
+ Recursive: false,
+ Immediate: false,
+ })
+ if err != nil {
+ t.Errorf("Failure during deleting part.1 - %v", err)
+ }
+
+ bdata := bytes.Repeat([]byte("b"), int(nfi.Size))
+ err = firstDisk.WriteAll(t.Context(), bucket, pathJoin(object, fi.DataDir, "part.1"), bdata)
+ if err != nil {
+ t.Errorf("Failure during creating part.1 - %v", err)
+ }
+
+ _, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{DryRun: false, Remove: true, ScanMode: madmin.HealDeepScan})
+ if err != nil {
+ t.Errorf("Expected nil but received %v", err)
+ }
+
+ fileInfos, errs = readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true)
+ nfi, err = getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs)
+ if err != nil {
+ t.Fatalf("Failed to getLatestFileInfo - %v", err)
+ }
+
+ if !reflect.DeepEqual(fi, nfi) {
+ t.Fatalf("FileInfo not equal after healing: %v != %v", fi, nfi)
+ }
+
+ // Test 4: checks if HealObject returns an error when xl.meta is not found
+ // in more than read quorum number of disks, to create a corrupted situation.
+ for i := 0; i <= nfi.Erasure.DataBlocks; i++ {
+ erasureDisks[i].Delete(t.Context(), bucket, pathJoin(object, xlStorageFormatFile), DeleteOptions{
+ Recursive: false,
+ Immediate: false,
+ })
+ }
+
+ // Try healing now, expect to receive errFileNotFound.
+ _, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{DryRun: false, Remove: true, ScanMode: madmin.HealDeepScan})
+ if err != nil {
+ if _, ok := err.(ObjectNotFound); !ok {
+ t.Errorf("Expect %v but received %v", ObjectNotFound{Bucket: bucket, Object: object}, err)
+ }
+ }
+
+ // since majority of xl.meta's are not available, object should be successfully deleted.
+ _, err = objLayer.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
+ if _, ok := err.(ObjectNotFound); !ok {
+ t.Errorf("Expect %v but received %v", ObjectNotFound{Bucket: bucket, Object: object}, err)
+ }
+
+ for i := 0; i < (nfi.Erasure.DataBlocks + nfi.Erasure.ParityBlocks); i++ {
+ stats, _ := erasureDisks[i].StatInfoFile(t.Context(), bucket, pathJoin(object, xlStorageFormatFile), false)
+ if len(stats) != 0 {
+ t.Errorf("Expected xl.meta file to be not present, but succeeded")
+ }
+ }
+}
+
+func TestHealObjectCorruptedXLMeta(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ resetGlobalHealState()
+ defer resetGlobalHealState()
+
+ nDisks := 16
+ fsDirs, err := getRandomDisks(nDisks)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ defer removeRoots(fsDirs)
+
+ // Everything is fine, should return nil
+ objLayer, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, fsDirs...))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ bucket := getRandomBucketName()
+ object := getRandomObjectName()
+ data := bytes.Repeat([]byte("a"), 5*1024*1024)
+ var opts ObjectOptions
+
+ err = objLayer.MakeBucket(ctx, bucket, MakeBucketOptions{})
+ if err != nil {
+ t.Fatalf("Failed to make a bucket - %v", err)
+ }
+
+ // Create an object with multiple parts uploaded in decreasing
+ // part number.
+ res, err := objLayer.NewMultipartUpload(ctx, bucket, object, opts)
+ if err != nil {
+ t.Fatalf("Failed to create a multipart upload - %v", err)
+ }
+
+ var uploadedParts []CompletePart
+ for _, partID := range []int{2, 1} {
+ pInfo, err1 := objLayer.PutObjectPart(ctx, bucket, object, res.UploadID, partID, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), opts)
+ if err1 != nil {
+ t.Fatalf("Failed to upload a part - %v", err1)
+ }
+ uploadedParts = append(uploadedParts, CompletePart{
+ PartNumber: pInfo.PartNumber,
+ ETag: pInfo.ETag,
+ })
+ }
+
+ _, err = objLayer.CompleteMultipartUpload(ctx, bucket, object, res.UploadID, uploadedParts, ObjectOptions{})
+ if err != nil {
+ t.Fatalf("Failed to complete multipart upload - %v", err)
+ }
+
+ z := objLayer.(*erasureServerPools)
+ er := z.serverPools[0].sets[0]
+ erasureDisks := er.getDisks()
+ firstDisk := erasureDisks[0]
+
+ // Test 1: Remove the object backend files from the first disk.
+ fileInfos, errs := readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true)
+ fi, err := getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs)
+ if err != nil {
+ t.Fatalf("Failed to getLatestFileInfo - %v", err)
+ }
+
+ err = firstDisk.Delete(t.Context(), bucket, pathJoin(object, xlStorageFormatFile), DeleteOptions{
+ Recursive: false,
+ Immediate: false,
+ })
+ if err != nil {
+ t.Fatalf("Failed to delete a file - %v", err)
+ }
+
+ _, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
+ if err != nil {
+ t.Fatalf("Failed to heal object - %v", err)
+ }
+
+ if _, err = firstDisk.StatInfoFile(t.Context(), bucket, object+"/"+xlStorageFormatFile, false); err != nil {
+ t.Errorf("Expected xl.meta file to be present but stat failed - %v", err)
+ }
+
+ fileInfos, errs = readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true)
+ nfi1, err := getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs)
+ if err != nil {
+ t.Fatalf("Failed to getLatestFileInfo - %v", err)
+ }
+
+ if !reflect.DeepEqual(fi, nfi1) {
+ t.Fatalf("FileInfo not equal after healing")
+ }
+
+ // Test 2: Test with a corrupted xl.meta
+ err = firstDisk.WriteAll(t.Context(), bucket, pathJoin(object, xlStorageFormatFile), []byte("abcd"))
+ if err != nil {
+ t.Errorf("Failure during creating part.1 - %v", err)
+ }
+
+ _, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
+ if err != nil {
+ t.Errorf("Expected nil but received %v", err)
+ }
+
+ fileInfos, errs = readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true)
+ nfi2, err := getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs)
+ if err != nil {
+ t.Fatalf("Failed to getLatestFileInfo - %v", err)
+ }
+
+ if !reflect.DeepEqual(fi, nfi2) {
+ t.Fatalf("FileInfo not equal after healing")
+ }
+
+ // Test 3: checks if HealObject returns an error when xl.meta is not found
+ // in more than read quorum number of disks, to create a corrupted situation.
+ for i := 0; i <= nfi2.Erasure.DataBlocks; i++ {
+ erasureDisks[i].Delete(t.Context(), bucket, pathJoin(object, xlStorageFormatFile), DeleteOptions{
+ Recursive: false,
+ Immediate: false,
+ })
+ }
+
+ // Try healing now, expect to receive errFileNotFound.
+ _, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{DryRun: false, Remove: true, ScanMode: madmin.HealDeepScan})
+ if err != nil {
+ if _, ok := err.(ObjectNotFound); !ok {
+ t.Errorf("Expect %v but received %v", ObjectNotFound{Bucket: bucket, Object: object}, err)
+ }
+ }
+
+ // since majority of xl.meta's are not available, object should be successfully deleted.
+ _, err = objLayer.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
+ if _, ok := err.(ObjectNotFound); !ok {
+ t.Errorf("Expect %v but received %v", ObjectNotFound{Bucket: bucket, Object: object}, err)
+ }
+}
+
+func TestHealObjectCorruptedParts(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ resetGlobalHealState()
+ defer resetGlobalHealState()
+
+ nDisks := 16
+ fsDirs, err := getRandomDisks(nDisks)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ defer removeRoots(fsDirs)
+
+ // Everything is fine, should return nil
+ objLayer, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, fsDirs...))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ bucket := getRandomBucketName()
+ object := getRandomObjectName()
+ data := bytes.Repeat([]byte("a"), 5*1024*1024)
+ var opts ObjectOptions
+
+ err = objLayer.MakeBucket(ctx, bucket, MakeBucketOptions{})
+ if err != nil {
+ t.Fatalf("Failed to make a bucket - %v", err)
+ }
+
+ // Create an object with multiple parts uploaded in decreasing
+ // part number.
+ res, err := objLayer.NewMultipartUpload(ctx, bucket, object, opts)
+ if err != nil {
+ t.Fatalf("Failed to create a multipart upload - %v", err)
+ }
+
+ var uploadedParts []CompletePart
+ for _, partID := range []int{2, 1} {
+ pInfo, err1 := objLayer.PutObjectPart(ctx, bucket, object, res.UploadID, partID, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), opts)
+ if err1 != nil {
+ t.Fatalf("Failed to upload a part - %v", err1)
+ }
+ uploadedParts = append(uploadedParts, CompletePart{
+ PartNumber: pInfo.PartNumber,
+ ETag: pInfo.ETag,
+ })
+ }
+
+ _, err = objLayer.CompleteMultipartUpload(ctx, bucket, object, res.UploadID, uploadedParts, ObjectOptions{})
+ if err != nil {
+ t.Fatalf("Failed to complete multipart upload - %v", err)
+ }
+
+ // Test 1: Remove the object backend files from the first disk.
+ z := objLayer.(*erasureServerPools)
+ er := z.serverPools[0].sets[0]
+ erasureDisks := er.getDisks()
+ firstDisk := erasureDisks[0]
+ secondDisk := erasureDisks[1]
+
+ fileInfos, errs := readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true)
+ fi, err := getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs)
+ if err != nil {
+ t.Fatalf("Failed to getLatestFileInfo - %v", err)
+ }
+
+ part1Disk1Origin, err := firstDisk.ReadAll(t.Context(), bucket, pathJoin(object, fi.DataDir, "part.1"))
+ if err != nil {
+ t.Fatalf("Failed to read a file - %v", err)
+ }
+
+ part1Disk2Origin, err := secondDisk.ReadAll(t.Context(), bucket, pathJoin(object, fi.DataDir, "part.1"))
+ if err != nil {
+ t.Fatalf("Failed to read a file - %v", err)
+ }
+
+ // Test 1, remove part.1
+ err = firstDisk.Delete(t.Context(), bucket, pathJoin(object, fi.DataDir, "part.1"), DeleteOptions{
+ Recursive: false,
+ Immediate: false,
+ })
+ if err != nil {
+ t.Fatalf("Failed to delete a file - %v", err)
+ }
+
+ _, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
+ if err != nil {
+ t.Fatalf("Failed to heal object - %v", err)
+ }
+
+ part1Replaced, err := firstDisk.ReadAll(t.Context(), bucket, pathJoin(object, fi.DataDir, "part.1"))
+ if err != nil {
+ t.Fatalf("Failed to read a file - %v", err)
+ }
+
+ if !reflect.DeepEqual(part1Disk1Origin, part1Replaced) {
+ t.Fatalf("part.1 not healed correctly")
+ }
+
+ // Test 2, Corrupt part.1
+ err = firstDisk.WriteAll(t.Context(), bucket, pathJoin(object, fi.DataDir, "part.1"), []byte("foobytes"))
+ if err != nil {
+ t.Fatalf("Failed to write a file - %v", err)
+ }
+
+ _, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
+ if err != nil {
+ t.Fatalf("Failed to heal object - %v", err)
+ }
+
+ part1Replaced, err = firstDisk.ReadAll(t.Context(), bucket, pathJoin(object, fi.DataDir, "part.1"))
+ if err != nil {
+ t.Fatalf("Failed to read a file - %v", err)
+ }
+
+ if !reflect.DeepEqual(part1Disk1Origin, part1Replaced) {
+ t.Fatalf("part.1 not healed correctly")
+ }
+
+ // Test 3, Corrupt one part and remove data in another disk
+ err = firstDisk.WriteAll(t.Context(), bucket, pathJoin(object, fi.DataDir, "part.1"), []byte("foobytes"))
+ if err != nil {
+ t.Fatalf("Failed to write a file - %v", err)
+ }
+
+ err = secondDisk.Delete(t.Context(), bucket, object, DeleteOptions{
+ Recursive: true,
+ Immediate: false,
+ })
+ if err != nil {
+ t.Fatalf("Failed to delete a file - %v", err)
+ }
+
+ _, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
+ if err != nil {
+ t.Fatalf("Failed to heal object - %v", err)
+ }
+
+ partReconstructed, err := firstDisk.ReadAll(t.Context(), bucket, pathJoin(object, fi.DataDir, "part.1"))
+ if err != nil {
+ t.Fatalf("Failed to read a file - %v", err)
+ }
+
+ if !reflect.DeepEqual(part1Disk1Origin, partReconstructed) {
+ t.Fatalf("part.1 not healed correctly")
+ }
+
+ partReconstructed, err = secondDisk.ReadAll(t.Context(), bucket, pathJoin(object, fi.DataDir, "part.1"))
+ if err != nil {
+ t.Fatalf("Failed to read a file - %v", err)
+ }
+
+ if !reflect.DeepEqual(part1Disk2Origin, partReconstructed) {
+ t.Fatalf("part.1 not healed correctly")
+ }
+}
+
+// Tests healing of object.
+func TestHealObjectErasure(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ nDisks := 16
+ fsDirs, err := getRandomDisks(nDisks)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ defer removeRoots(fsDirs)
+
+ // Everything is fine, should return nil
+ obj, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, fsDirs...))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ bucket := "bucket"
+ object := "object"
+ data := bytes.Repeat([]byte("a"), 5*1024*1024)
+ var opts ObjectOptions
+
+ err = obj.MakeBucket(ctx, bucket, MakeBucketOptions{})
+ if err != nil {
+ t.Fatalf("Failed to make a bucket - %v", err)
+ }
+
+ // Create an object with multiple parts uploaded in decreasing
+ // part number.
+ res, err := obj.NewMultipartUpload(ctx, bucket, object, opts)
+ if err != nil {
+ t.Fatalf("Failed to create a multipart upload - %v", err)
+ }
+
+ var uploadedParts []CompletePart
+ for _, partID := range []int{2, 1} {
+ pInfo, err1 := obj.PutObjectPart(ctx, bucket, object, res.UploadID, partID, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), opts)
+ if err1 != nil {
+ t.Fatalf("Failed to upload a part - %v", err1)
+ }
+ uploadedParts = append(uploadedParts, CompletePart{
+ PartNumber: pInfo.PartNumber,
+ ETag: pInfo.ETag,
+ })
+ }
+
+ // Remove the object backend files from the first disk.
+ z := obj.(*erasureServerPools)
+ er := z.serverPools[0].sets[0]
+ firstDisk := er.getDisks()[0]
+
+ _, err = obj.CompleteMultipartUpload(ctx, bucket, object, res.UploadID, uploadedParts, ObjectOptions{})
+ if err != nil {
+ t.Fatalf("Failed to complete multipart upload - %v", err)
+ }
+
+ // Delete the whole object folder
+ err = firstDisk.Delete(t.Context(), bucket, object, DeleteOptions{
+ Recursive: true,
+ Immediate: false,
+ })
+ if err != nil {
+ t.Fatalf("Failed to delete a file - %v", err)
+ }
+
+ _, err = obj.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
+ if err != nil {
+ t.Fatalf("Failed to heal object - %v", err)
+ }
+
+ if _, err = firstDisk.StatInfoFile(t.Context(), bucket, object+"/"+xlStorageFormatFile, false); err != nil {
+ t.Errorf("Expected xl.meta file to be present but stat failed - %v", err)
+ }
+
+ erasureDisks := er.getDisks()
+ z.serverPools[0].erasureDisksMu.Lock()
+ er.getDisks = func() []StorageAPI {
+ // Nil more than half the disks, to remove write quorum.
+ for i := 0; i <= len(erasureDisks)/2; i++ {
+ err := erasureDisks[i].Delete(t.Context(), bucket, object, DeleteOptions{
+ Recursive: true,
+ Immediate: false,
+ })
+ if err != nil {
+ t.Fatalf("Failed to delete a file - %v", err)
+ }
+ }
+ return erasureDisks
+ }
+ z.serverPools[0].erasureDisksMu.Unlock()
+
+ // Try healing now, expect to receive errDiskNotFound.
+ _, err = obj.HealObject(ctx, bucket, object, "", madmin.HealOpts{
+ ScanMode: madmin.HealDeepScan,
+ })
+ // since majority of xl.meta's are not available, object quorum
+ // can't be read properly will be deleted automatically and
+ // err is nil
+ if !isErrObjectNotFound(err) {
+ t.Fatal(err)
+ }
+}
+
+// Tests healing of empty directories
+func TestHealEmptyDirectoryErasure(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ nDisks := 16
+ fsDirs, err := getRandomDisks(nDisks)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer removeRoots(fsDirs)
+
+ // Everything is fine, should return nil
+ obj, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, fsDirs...))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ bucket := "bucket"
+ object := "empty-dir/"
+ var opts ObjectOptions
+
+ err = obj.MakeBucket(ctx, bucket, MakeBucketOptions{})
+ if err != nil {
+ t.Fatalf("Failed to make a bucket - %v", err)
+ }
+
+ // Upload an empty directory
+ _, err = obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t,
+ bytes.NewReader([]byte{}), 0, "", ""), opts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Remove the object backend files from the first disk.
+ z := obj.(*erasureServerPools)
+ er := z.serverPools[0].sets[0]
+ firstDisk := er.getDisks()[0]
+ err = firstDisk.DeleteVol(t.Context(), pathJoin(bucket, encodeDirObject(object)), true)
+ if err != nil {
+ t.Fatalf("Failed to delete a file - %v", err)
+ }
+
+ // Heal the object
+ hr, err := obj.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
+ if err != nil {
+ t.Fatalf("Failed to heal object - %v", err)
+ }
+
+ // Check if the empty directory is restored in the first disk
+ _, err = firstDisk.StatVol(t.Context(), pathJoin(bucket, encodeDirObject(object)))
+ if err != nil {
+ t.Fatalf("Expected object to be present but stat failed - %v", err)
+ }
+
+ // Check the state of the object in the first disk (should be missing)
+ if hr.Before.Drives[0].State != madmin.DriveStateMissing {
+ t.Fatalf("Unexpected drive state: %v", hr.Before.Drives[0].State)
+ }
+
+ // Check the state of all other disks (should be ok)
+ for i, h := range append(hr.Before.Drives[1:], hr.After.Drives...) {
+ if h.State != madmin.DriveStateOk {
+ t.Fatalf("Unexpected drive state (%d): %v", i+1, h.State)
+ }
+ }
+
+ // Heal the same object again
+ hr, err = obj.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
+ if err != nil {
+ t.Fatalf("Failed to heal object - %v", err)
+ }
+
+ // Check that Before & After states are all okay
+ for i, h := range append(hr.Before.Drives, hr.After.Drives...) {
+ if h.State != madmin.DriveStateOk {
+ t.Fatalf("Unexpected drive state (%d): %v", i+1, h.State)
+ }
+ }
+}
+
+func TestHealLastDataShard(t *testing.T) {
+ tests := []struct {
+ name string
+ dataSize int64
+ }{
+ {"4KiB", 4 * humanize.KiByte},
+ {"64KiB", 64 * humanize.KiByte},
+ {"128KiB", 128 * humanize.KiByte},
+ {"1MiB", 1 * humanize.MiByte},
+ {"5MiB", 5 * humanize.MiByte},
+ {"10MiB", 10 * humanize.MiByte},
+ {"5MiB-1KiB", 5*humanize.MiByte - 1*humanize.KiByte},
+ {"10MiB-1Kib", 10*humanize.MiByte - 1*humanize.KiByte},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ nDisks := 16
+ fsDirs, err := getRandomDisks(nDisks)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ defer removeRoots(fsDirs)
+
+ obj, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, fsDirs...))
+ if err != nil {
+ t.Fatal(err)
+ }
+ bucket := "bucket"
+ object := "object"
+
+ data := make([]byte, test.dataSize)
+ _, err = rand.Read(data)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var opts ObjectOptions
+
+ err = obj.MakeBucket(ctx, bucket, MakeBucketOptions{})
+ if err != nil {
+ t.Fatalf("Failed to make a bucket - %v", err)
+ }
+
+ _, err = obj.PutObject(ctx, bucket, object,
+ mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), opts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ actualH := sha256.New()
+ _, err = io.Copy(actualH, bytes.NewReader(data))
+ if err != nil {
+ return
+ }
+ actualSha256 := actualH.Sum(nil)
+
+ z := obj.(*erasureServerPools)
+ er := z.serverPools[0].getHashedSet(object)
+
+ disks := er.getDisks()
+ distribution := hashOrder(pathJoin(bucket, object), nDisks)
+ shuffledDisks := shuffleDisks(disks, distribution)
+
+ // remove last data shard
+ err = removeAll(pathJoin(shuffledDisks[11].String(), bucket, object))
+ if err != nil {
+ t.Fatalf("Failed to delete a file - %v", err)
+ }
+ _, err = obj.HealObject(ctx, bucket, object, "", madmin.HealOpts{
+ ScanMode: madmin.HealNormalScan,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ firstGr, err := obj.GetObjectNInfo(ctx, bucket, object, nil, nil, ObjectOptions{NoLock: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer firstGr.Close()
+
+ firstHealedH := sha256.New()
+ _, err = io.Copy(firstHealedH, firstGr)
+ if err != nil {
+ t.Fatal(err)
+ }
+ firstHealedDataSha256 := firstHealedH.Sum(nil)
+
+ if !bytes.Equal(actualSha256, firstHealedDataSha256) {
+ t.Fatalf("object healed wrong, expected %x, got %x",
+ actualSha256, firstHealedDataSha256)
+ }
+
+ // remove another data shard
+ if err = removeAll(pathJoin(shuffledDisks[1].String(), bucket, object)); err != nil {
+ t.Fatalf("Failed to delete a file - %v", err)
+ }
+
+ _, err = obj.HealObject(ctx, bucket, object, "", madmin.HealOpts{
+ ScanMode: madmin.HealNormalScan,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ secondGr, err := obj.GetObjectNInfo(ctx, bucket, object, nil, nil, ObjectOptions{NoLock: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer secondGr.Close()
+
+ secondHealedH := sha256.New()
+ _, err = io.Copy(secondHealedH, secondGr)
+ if err != nil {
+ t.Fatal(err)
+ }
+ secondHealedDataSha256 := secondHealedH.Sum(nil)
+
+ if !bytes.Equal(actualSha256, secondHealedDataSha256) {
+ t.Fatalf("object healed wrong, expected %x, got %x",
+ actualSha256, secondHealedDataSha256)
+ }
+ })
+ }
+}
diff --git a/cmd/erasure-metadata-utils.go b/cmd/erasure-metadata-utils.go
new file mode 100644
index 00000000..1409d99e
--- /dev/null
+++ b/cmd/erasure-metadata-utils.go
@@ -0,0 +1,381 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/binary"
+ "errors"
+ "hash/crc32"
+
+ "github.com/minio/pkg/v3/sync/errgroup"
+)
+
+// counterMap type adds GetValueWithQuorum method to a map[T]int used to count occurrences of values of type T.
+type counterMap[T comparable] map[T]int
+
+// GetValueWithQuorum returns the first key which occurs >= quorum number of times.
+func (c counterMap[T]) GetValueWithQuorum(quorum int) (T, bool) {
+ var zero T
+ for x, count := range c {
+ if count >= quorum {
+ return x, true
+ }
+ }
+ return zero, false
+}
+
+// figure out the most commonVersions across disk that satisfies
+// the 'writeQuorum' this function returns "" if quorum cannot
+// be achieved and disks have too many inconsistent versions.
+func reduceCommonVersions(diskVersions [][]byte, writeQuorum int) (versions []byte) {
+ diskVersionsCount := make(map[uint64]int)
+ for _, versions := range diskVersions {
+ if len(versions) > 0 {
+ diskVersionsCount[binary.BigEndian.Uint64(versions)]++
+ }
+ }
+
+ var commonVersions uint64
+ maxCnt := 0
+ for versions, count := range diskVersionsCount {
+ if maxCnt < count {
+ maxCnt = count
+ commonVersions = versions
+ }
+ }
+
+ if maxCnt >= writeQuorum {
+ for _, versions := range diskVersions {
+ if binary.BigEndian.Uint64(versions) == commonVersions {
+ return versions
+ }
+ }
+ }
+
+ return []byte{}
+}
+
+// figure out the most commonVersions across disk that satisfies
+// the 'writeQuorum' this function returns '0' if quorum cannot
+// be achieved and disks have too many inconsistent versions.
+func reduceCommonDataDir(dataDirs []string, writeQuorum int) (dataDir string) {
+ dataDirsCount := make(map[string]int)
+ for _, ddir := range dataDirs {
+ dataDirsCount[ddir]++
+ }
+
+ maxCnt := 0
+ for ddir, count := range dataDirsCount {
+ if maxCnt < count {
+ maxCnt = count
+ dataDir = ddir
+ }
+ }
+
+ if maxCnt >= writeQuorum {
+ return dataDir
+ }
+
+ return ""
+}
+
+// Returns number of errors that occurred the most (incl. nil) and the
+// corresponding error value. NB When there is more than one error value that
+// occurs maximum number of times, the error value returned depends on how
+// golang's map orders keys. This doesn't affect correctness as long as quorum
+// value is greater than or equal to simple majority, since none of the equally
+// maximal values would occur quorum or more number of times.
+func reduceErrs(errs []error, ignoredErrs []error) (maxCount int, maxErr error) {
+ errorCounts := make(map[error]int)
+ for _, err := range errs {
+ if IsErrIgnored(err, ignoredErrs...) {
+ continue
+ }
+ // Errors due to context cancellation may be wrapped - group them by context.Canceled.
+ if errors.Is(err, context.Canceled) {
+ errorCounts[context.Canceled]++
+ continue
+ }
+ errorCounts[err]++
+ }
+
+ maxCnt := 0
+ for err, count := range errorCounts {
+ switch {
+ case maxCnt < count:
+ maxCnt = count
+ maxErr = err
+
+ // Prefer `nil` over other error values with the same
+ // number of occurrences.
+ case maxCnt == count && err == nil:
+ maxErr = err
+ }
+ }
+ return maxCnt, maxErr
+}
+
+// reduceQuorumErrs behaves like reduceErrs by only for returning
+// values of maximally occurring errors validated against a generic
+// quorum number that can be read or write quorum depending on usage.
+func reduceQuorumErrs(ctx context.Context, errs []error, ignoredErrs []error, quorum int, quorumErr error) error {
+ if contextCanceled(ctx) {
+ return context.Canceled
+ }
+ maxCount, maxErr := reduceErrs(errs, ignoredErrs)
+ if maxCount >= quorum {
+ return maxErr
+ }
+ return quorumErr
+}
+
+// reduceReadQuorumErrs behaves like reduceErrs but only for returning
+// values of maximally occurring errors validated against readQuorum.
+func reduceReadQuorumErrs(ctx context.Context, errs []error, ignoredErrs []error, readQuorum int) (maxErr error) {
+ return reduceQuorumErrs(ctx, errs, ignoredErrs, readQuorum, errErasureReadQuorum)
+}
+
+// reduceWriteQuorumErrs behaves like reduceErrs but only for returning
+// values of maximally occurring errors validated against writeQuorum.
+func reduceWriteQuorumErrs(ctx context.Context, errs []error, ignoredErrs []error, writeQuorum int) (maxErr error) {
+ return reduceQuorumErrs(ctx, errs, ignoredErrs, writeQuorum, errErasureWriteQuorum)
+}
+
+// Similar to 'len(slice)' but returns the actual elements count
+// skipping the unallocated elements.
+func diskCount(disks []StorageAPI) int {
+ diskCount := 0
+ for _, disk := range disks {
+ if disk == nil {
+ continue
+ }
+ diskCount++
+ }
+ return diskCount
+}
+
+// hashOrder - hashes input key to return consistent
+// hashed integer slice. Returned integer order is salted
+// with an input key. This results in consistent order.
+// NOTE: collisions are fine, we are not looking for uniqueness
+// in the slices returned.
+func hashOrder(key string, cardinality int) []int {
+ if cardinality <= 0 {
+ // Returns an empty int slice for cardinality < 0.
+ return nil
+ }
+
+ nums := make([]int, cardinality)
+ keyCrc := crc32.Checksum([]byte(key), crc32.IEEETable)
+
+ start := int(keyCrc % uint32(cardinality))
+ for i := 1; i <= cardinality; i++ {
+ nums[i-1] = 1 + ((start + i) % cardinality)
+ }
+ return nums
+}
+
+// Reads all `xl.meta` metadata as a FileInfo slice.
+// Returns error slice indicating the failed metadata reads.
+func readAllFileInfo(ctx context.Context, disks []StorageAPI, origbucket string, bucket, object, versionID string, readData, healing bool) ([]FileInfo, []error) {
+ metadataArray := make([]FileInfo, len(disks))
+
+ opts := ReadOptions{
+ ReadData: readData,
+ Healing: healing,
+ }
+
+ g := errgroup.WithNErrs(len(disks))
+ // Read `xl.meta` in parallel across disks.
+ for index := range disks {
+ g.Go(func() (err error) {
+ if disks[index] == nil {
+ return errDiskNotFound
+ }
+ metadataArray[index], err = disks[index].ReadVersion(ctx, origbucket, bucket, object, versionID, opts)
+ return err
+ }, index)
+ }
+
+ return metadataArray, g.Wait()
+}
+
+// shuffleDisksAndPartsMetadataByIndex this function should be always used by GetObjectNInfo()
+// and CompleteMultipartUpload code path, it is not meant to be used with PutObject,
+// NewMultipartUpload metadata shuffling.
+func shuffleDisksAndPartsMetadataByIndex(disks []StorageAPI, metaArr []FileInfo, fi FileInfo) (shuffledDisks []StorageAPI, shuffledPartsMetadata []FileInfo) {
+ shuffledDisks = make([]StorageAPI, len(disks))
+ shuffledPartsMetadata = make([]FileInfo, len(disks))
+ distribution := fi.Erasure.Distribution
+
+ var inconsistent int
+ for i, meta := range metaArr {
+ if disks[i] == nil {
+ // Assuming offline drives as inconsistent,
+ // to be safe and fallback to original
+ // distribution order.
+ inconsistent++
+ continue
+ }
+ if !meta.IsValid() {
+ inconsistent++
+ continue
+ }
+ if meta.XLV1 != fi.XLV1 {
+ inconsistent++
+ continue
+ }
+ // check if erasure distribution order matches the index
+ // position if this is not correct we discard the disk
+ // and move to collect others
+ if distribution[i] != meta.Erasure.Index {
+ inconsistent++ // keep track of inconsistent entries
+ continue
+ }
+ shuffledDisks[meta.Erasure.Index-1] = disks[i]
+ shuffledPartsMetadata[meta.Erasure.Index-1] = metaArr[i]
+ }
+
+ // Inconsistent meta info is with in the limit of
+ // expected quorum, proceed with EcIndex based
+ // disk order.
+ if inconsistent < fi.Erasure.ParityBlocks {
+ return shuffledDisks, shuffledPartsMetadata
+ }
+
+ // fall back to original distribution based order.
+ return shuffleDisksAndPartsMetadata(disks, metaArr, fi)
+}
+
+// Return shuffled partsMetadata depending on fi.Distribution.
+// additional validation is attempted and invalid metadata is
+// automatically skipped only when fi.ModTime is non-zero
+// indicating that this is called during read-phase
+func shuffleDisksAndPartsMetadata(disks []StorageAPI, partsMetadata []FileInfo, fi FileInfo) (shuffledDisks []StorageAPI, shuffledPartsMetadata []FileInfo) {
+ shuffledDisks = make([]StorageAPI, len(disks))
+ shuffledPartsMetadata = make([]FileInfo, len(partsMetadata))
+ distribution := fi.Erasure.Distribution
+
+ init := fi.ModTime.IsZero()
+ // Shuffle slice xl metadata for expected distribution.
+ for index := range partsMetadata {
+ if disks[index] == nil {
+ continue
+ }
+ if !init && !partsMetadata[index].IsValid() {
+ // Check for parts metadata validity for only
+ // fi.ModTime is not empty - ModTime is always set,
+ // if object was ever written previously.
+ continue
+ }
+ if !init && fi.XLV1 != partsMetadata[index].XLV1 {
+ continue
+ }
+ blockIndex := distribution[index]
+ shuffledPartsMetadata[blockIndex-1] = partsMetadata[index]
+ shuffledDisks[blockIndex-1] = disks[index]
+ }
+ return shuffledDisks, shuffledPartsMetadata
+}
+
+func shuffleWithDist[T any](input []T, distribution []int) []T {
+ if distribution == nil {
+ return input
+ }
+ shuffled := make([]T, len(input))
+ for index := range input {
+ blockIndex := distribution[index]
+ shuffled[blockIndex-1] = input[index]
+ }
+ 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
+// erasure distribution. Return shuffled slice of disks with
+// their expected distribution.
+func shuffleDisks(disks []StorageAPI, distribution []int) []StorageAPI {
+ return shuffleWithDist[StorageAPI](disks, distribution)
+}
+
+// evalDisks - returns a new slice of disks where nil is set if
+// the corresponding error in errs slice is not nil
+func evalDisks(disks []StorageAPI, errs []error) []StorageAPI {
+ if len(errs) != len(disks) {
+ bugLogIf(GlobalContext, errors.New("unexpected drives/errors slice length"))
+ return nil
+ }
+ newDisks := make([]StorageAPI, len(disks))
+ for index := range errs {
+ if errs[index] == nil {
+ newDisks[index] = disks[index]
+ } else {
+ newDisks[index] = nil
+ }
+ }
+ return newDisks
+}
+
+// Errors specifically generated by calculatePartSizeFromIdx function.
+var (
+ errPartSizeZero = errors.New("Part size cannot be zero")
+ errPartSizeIndex = errors.New("Part index cannot be smaller than 1")
+)
+
+// calculatePartSizeFromIdx calculates the part size according to input index.
+// returns error if totalSize is -1, partSize is 0, partIndex is 0.
+func calculatePartSizeFromIdx(ctx context.Context, totalSize int64, partSize int64, partIndex int) (currPartSize int64, err error) {
+ if totalSize < -1 {
+ return 0, errInvalidArgument
+ }
+ if partSize == 0 {
+ return 0, errPartSizeZero
+ }
+ if partIndex < 1 {
+ return 0, errPartSizeIndex
+ }
+ if totalSize == -1 {
+ return -1, nil
+ }
+ if totalSize > 0 {
+ // Compute the total count of parts
+ partsCount := totalSize/partSize + 1
+ // Return the part's size
+ switch {
+ case int64(partIndex) < partsCount:
+ currPartSize = partSize
+ case int64(partIndex) == partsCount:
+ // Size of last part
+ currPartSize = totalSize % partSize
+ default:
+ currPartSize = 0
+ }
+ }
+ return currPartSize, nil
+}
diff --git a/cmd/erasure-metadata-utils_test.go b/cmd/erasure-metadata-utils_test.go
new file mode 100644
index 00000000..b14cc25d
--- /dev/null
+++ b/cmd/erasure-metadata-utils_test.go
@@ -0,0 +1,234 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/hex"
+ "fmt"
+ "math/rand"
+ "reflect"
+ "testing"
+)
+
+// Tests caclculating disk count.
+func TestDiskCount(t *testing.T) {
+ testCases := []struct {
+ disks []StorageAPI
+ diskCount int
+ }{
+ // Test case - 1
+ {
+ disks: []StorageAPI{&xlStorage{}, &xlStorage{}, &xlStorage{}, &xlStorage{}},
+ diskCount: 4,
+ },
+ // Test case - 2
+ {
+ disks: []StorageAPI{nil, &xlStorage{}, &xlStorage{}, &xlStorage{}},
+ diskCount: 3,
+ },
+ }
+ for i, testCase := range testCases {
+ cdiskCount := diskCount(testCase.disks)
+ if cdiskCount != testCase.diskCount {
+ t.Errorf("Test %d: Expected %d, got %d", i+1, testCase.diskCount, cdiskCount)
+ }
+ }
+}
+
+// Test for reduceErrs, reduceErr reduces collection
+// of errors into a single maximal error with in the list.
+func TestReduceErrs(t *testing.T) {
+ canceledErrs := make([]error, 0, 5)
+ for i := range 5 {
+ canceledErrs = append(canceledErrs, fmt.Errorf("error %d: %w", i, context.Canceled))
+ }
+ // List all of all test cases to validate various cases of reduce errors.
+ testCases := []struct {
+ errs []error
+ ignoredErrs []error
+ err error
+ }{
+ // Validate if have reduced properly.
+ {[]error{
+ errDiskNotFound,
+ errDiskNotFound,
+ errDiskFull,
+ }, []error{}, errErasureReadQuorum},
+ // Validate if have no consensus.
+ {[]error{
+ errDiskFull,
+ errDiskNotFound,
+ nil, nil,
+ }, []error{}, errErasureReadQuorum},
+ // Validate if have consensus and errors ignored.
+ {[]error{
+ errVolumeNotFound,
+ errVolumeNotFound,
+ errVolumeNotFound,
+ errVolumeNotFound,
+ errVolumeNotFound,
+ errDiskNotFound,
+ errDiskNotFound,
+ }, []error{errDiskNotFound}, errVolumeNotFound},
+ {[]error{}, []error{}, errErasureReadQuorum},
+ {
+ []error{
+ errFileNotFound, errFileNotFound, errFileNotFound,
+ errFileNotFound, errFileNotFound, nil, nil, nil, nil, nil,
+ },
+ nil, nil,
+ },
+ // Checks if wrapped context cancellation errors are grouped as one.
+ {canceledErrs, nil, context.Canceled},
+ }
+ // Validates list of all the testcases for returning valid errors.
+ for i, testCase := range testCases {
+ gotErr := reduceReadQuorumErrs(t.Context(), testCase.errs, testCase.ignoredErrs, 5)
+ if gotErr != testCase.err {
+ t.Errorf("Test %d : expected %s, got %s", i+1, testCase.err, gotErr)
+ }
+ gotNewErr := reduceWriteQuorumErrs(t.Context(), testCase.errs, testCase.ignoredErrs, 6)
+ if gotNewErr != errErasureWriteQuorum {
+ t.Errorf("Test %d : expected %s, got %s", i+1, errErasureWriteQuorum, gotErr)
+ }
+ }
+}
+
+// TestHashOrder - test order of ints in array
+func TestHashOrder(t *testing.T) {
+ testCases := []struct {
+ objectName string
+ hashedOrder []int
+ }{
+ // cases which should pass the test.
+ // passing in valid object name.
+ {"object", []int{14, 15, 16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}},
+ {"The Shining Script .pdf", []int{16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}},
+ {"Cost Benefit Analysis (2009-2010).pptx", []int{15, 16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}},
+ {"117Gn8rfHL2ACARPAhaFd0AGzic9pUbIA/5OCn5A", []int{3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 1, 2}},
+ {"SHØRT", []int{11, 12, 13, 14, 15, 16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}},
+ {"There are far too many object names, and far too few bucket names!", []int{15, 16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}},
+ {"a/b/c/", []int{3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 1, 2}},
+ {"/a/b/c", []int{6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 1, 2, 3, 4, 5}},
+ {string([]byte{0xff, 0xfe, 0xfd}), []int{15, 16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}},
+ }
+
+ // Tests hashing order to be consistent.
+ for i, testCase := range testCases {
+ hashedOrder := hashOrder(testCase.objectName, 16)
+ if !reflect.DeepEqual(testCase.hashedOrder, hashedOrder) {
+ t.Errorf("Test case %d: Expected \"%v\" but failed \"%v\"", i+1, testCase.hashedOrder, hashedOrder)
+ }
+ }
+
+ // Tests hashing order to fail for when order is '-1'.
+ if hashedOrder := hashOrder("This will fail", -1); hashedOrder != nil {
+ t.Errorf("Test: Expect \"nil\" but failed \"%#v\"", hashedOrder)
+ }
+
+ if hashedOrder := hashOrder("This will fail", 0); hashedOrder != nil {
+ t.Errorf("Test: Expect \"nil\" but failed \"%#v\"", hashedOrder)
+ }
+}
+
+func TestShuffleDisks(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ nDisks := 16
+ disks, err := getRandomDisks(nDisks)
+ if err != nil {
+ t.Fatal(err)
+ }
+ objLayer, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, disks...))
+ if err != nil {
+ removeRoots(disks)
+ t.Fatal(err)
+ }
+ defer removeRoots(disks)
+ z := objLayer.(*erasureServerPools)
+ testShuffleDisks(t, z)
+}
+
+// Test shuffleDisks which returns shuffled slice of disks for their actual distribution.
+func testShuffleDisks(t *testing.T, z *erasureServerPools) {
+ disks := z.serverPools[0].GetDisks(0)()
+ distribution := []int{16, 14, 12, 10, 8, 6, 4, 2, 1, 3, 5, 7, 9, 11, 13, 15}
+ shuffledDisks := shuffleDisks(disks, distribution)
+ // From the "distribution" above you can notice that:
+ // 1st data block is in the 9th disk (i.e distribution index 8)
+ // 2nd data block is in the 8th disk (i.e distribution index 7) and so on.
+ if shuffledDisks[0] != disks[8] ||
+ shuffledDisks[1] != disks[7] ||
+ shuffledDisks[2] != disks[9] ||
+ shuffledDisks[3] != disks[6] ||
+ shuffledDisks[4] != disks[10] ||
+ shuffledDisks[5] != disks[5] ||
+ shuffledDisks[6] != disks[11] ||
+ shuffledDisks[7] != disks[4] ||
+ shuffledDisks[8] != disks[12] ||
+ shuffledDisks[9] != disks[3] ||
+ shuffledDisks[10] != disks[13] ||
+ shuffledDisks[11] != disks[2] ||
+ shuffledDisks[12] != disks[14] ||
+ shuffledDisks[13] != disks[1] ||
+ shuffledDisks[14] != disks[15] ||
+ shuffledDisks[15] != disks[0] {
+ t.Errorf("shuffleDisks returned incorrect order.")
+ }
+}
+
+// TestEvalDisks tests the behavior of evalDisks
+func TestEvalDisks(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ nDisks := 16
+ disks, err := getRandomDisks(nDisks)
+ if err != nil {
+ t.Fatal(err)
+ }
+ objLayer, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, disks...))
+ if err != nil {
+ removeRoots(disks)
+ t.Fatal(err)
+ }
+ defer removeRoots(disks)
+ z := objLayer.(*erasureServerPools)
+ testShuffleDisks(t, z)
+}
+
+func Test_hashOrder(t *testing.T) {
+ for x := 1; x < 17; x++ {
+ t.Run(fmt.Sprintf("%d", x), func(t *testing.T) {
+ var first [17]int
+ rng := rand.New(rand.NewSource(0))
+ var tmp [16]byte
+ rng.Read(tmp[:])
+ prefix := hex.EncodeToString(tmp[:])
+ for range 10000 {
+ rng.Read(tmp[:])
+
+ y := hashOrder(fmt.Sprintf("%s/%x", prefix, hex.EncodeToString(tmp[:3])), x)
+ first[y[0]]++
+ }
+ t.Log("first:", first[:x])
+ })
+ }
+}
diff --git a/cmd/erasure-metadata.go b/cmd/erasure-metadata.go
new file mode 100644
index 00000000..f4b8798b
--- /dev/null
+++ b/cmd/erasure-metadata.go
@@ -0,0 +1,684 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/hex"
+ "fmt"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/minio/minio/internal/amztime"
+ "github.com/minio/minio/internal/bucket/lifecycle"
+ "github.com/minio/minio/internal/bucket/replication"
+ "github.com/minio/minio/internal/crypto"
+ "github.com/minio/minio/internal/hash/sha256"
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/pkg/v3/sync/errgroup"
+)
+
+// Object was stored with additional erasure codes due to degraded system at upload time
+const minIOErasureUpgraded = "x-minio-internal-erasure-upgraded"
+
+const erasureAlgorithm = "rs-vandermonde"
+
+// GetChecksumInfo - get checksum of a part.
+func (e ErasureInfo) GetChecksumInfo(partNumber int) (ckSum ChecksumInfo) {
+ for _, sum := range e.Checksums {
+ if sum.PartNumber == partNumber {
+ // Return the checksum
+ return sum
+ }
+ }
+ return ChecksumInfo{Algorithm: DefaultBitrotAlgorithm}
+}
+
+// ShardFileSize - returns final erasure size from original size.
+func (e ErasureInfo) ShardFileSize(totalLength int64) int64 {
+ if totalLength == 0 {
+ return 0
+ }
+ if totalLength == -1 {
+ return -1
+ }
+ numShards := totalLength / e.BlockSize
+ lastBlockSize := totalLength % e.BlockSize
+ lastShardSize := ceilFrac(lastBlockSize, int64(e.DataBlocks))
+ return numShards*e.ShardSize() + lastShardSize
+}
+
+// ShardSize - returns actual shared size from erasure blockSize.
+func (e ErasureInfo) ShardSize() int64 {
+ return ceilFrac(e.BlockSize, int64(e.DataBlocks))
+}
+
+// IsValid - tells if erasure info fields are valid.
+func (fi FileInfo) IsValid() bool {
+ if fi.Deleted {
+ // Delete marker has no data, no need to check
+ // for erasure coding information
+ return true
+ }
+ dataBlocks := fi.Erasure.DataBlocks
+ parityBlocks := fi.Erasure.ParityBlocks
+ correctIndexes := (fi.Erasure.Index > 0 &&
+ fi.Erasure.Index <= dataBlocks+parityBlocks &&
+ len(fi.Erasure.Distribution) == (dataBlocks+parityBlocks))
+ return ((dataBlocks >= parityBlocks) &&
+ (dataBlocks > 0) && (parityBlocks >= 0) &&
+ correctIndexes)
+}
+
+// ToObjectInfo - Converts metadata to object info.
+func (fi FileInfo) ToObjectInfo(bucket, object string, versioned bool) ObjectInfo {
+ object = decodeDirObject(object)
+ versionID := fi.VersionID
+ if versioned && versionID == "" {
+ versionID = nullVersionID
+ }
+
+ objInfo := ObjectInfo{
+ IsDir: HasSuffix(object, SlashSeparator),
+ Bucket: bucket,
+ Name: object,
+ ParityBlocks: fi.Erasure.ParityBlocks,
+ DataBlocks: fi.Erasure.DataBlocks,
+ VersionID: versionID,
+ IsLatest: fi.IsLatest,
+ DeleteMarker: fi.Deleted,
+ Size: fi.Size,
+ ModTime: fi.ModTime,
+ Legacy: fi.XLV1,
+ ContentType: fi.Metadata["content-type"],
+ ContentEncoding: fi.Metadata["content-encoding"],
+ NumVersions: fi.NumVersions,
+ SuccessorModTime: fi.SuccessorModTime,
+ CacheControl: fi.Metadata["cache-control"],
+ }
+
+ if exp, ok := fi.Metadata["expires"]; ok {
+ if t, err := amztime.ParseHeader(exp); err == nil {
+ objInfo.Expires = t.UTC()
+ }
+ }
+
+ // Extract etag from metadata.
+ objInfo.ETag = extractETag(fi.Metadata)
+
+ // Add user tags to the object info
+ tags := fi.Metadata[xhttp.AmzObjectTagging]
+ if len(tags) != 0 {
+ objInfo.UserTags = tags
+ }
+
+ // Add replication status to the object info
+ objInfo.ReplicationStatusInternal = fi.ReplicationState.ReplicationStatusInternal
+ objInfo.VersionPurgeStatusInternal = fi.ReplicationState.VersionPurgeStatusInternal
+ objInfo.ReplicationStatus = fi.ReplicationStatus()
+ if objInfo.ReplicationStatus.Empty() { // overlay x-amx-replication-status if present for replicas
+ if st, ok := fi.Metadata[xhttp.AmzBucketReplicationStatus]; ok && st == string(replication.Replica) {
+ objInfo.ReplicationStatus = replication.StatusType(st)
+ }
+ }
+ objInfo.VersionPurgeStatus = fi.VersionPurgeStatus()
+
+ objInfo.TransitionedObject = TransitionedObject{
+ Name: fi.TransitionedObjName,
+ VersionID: fi.TransitionVersionID,
+ Status: fi.TransitionStatus,
+ FreeVersion: fi.TierFreeVersion(),
+ Tier: fi.TransitionTier,
+ }
+
+ // etag/md5Sum has already been extracted. We need to
+ // remove to avoid it from appearing as part of
+ // response headers. e.g, X-Minio-* or X-Amz-*.
+ // Tags have also been extracted, we remove that as well.
+ objInfo.UserDefined = cleanMetadata(fi.Metadata)
+
+ // All the parts per object.
+ objInfo.Parts = fi.Parts
+
+ // Update storage class
+ if fi.TransitionTier != "" {
+ objInfo.StorageClass = fi.TransitionTier
+ } else if sc, ok := fi.Metadata[xhttp.AmzStorageClass]; ok {
+ objInfo.StorageClass = sc
+ } else {
+ objInfo.StorageClass = globalMinioDefaultStorageClass
+ }
+
+ // set restore status for transitioned object
+ restoreHdr, ok := fi.Metadata[xhttp.AmzRestore]
+ if ok {
+ if restoreStatus, err := parseRestoreObjStatus(restoreHdr); err == nil {
+ objInfo.RestoreOngoing = restoreStatus.Ongoing()
+ objInfo.RestoreExpires, _ = restoreStatus.Expiry()
+ }
+ }
+ objInfo.Checksum = fi.Checksum
+ objInfo.decryptPartsChecksums(nil)
+ objInfo.Inlined = fi.InlineData()
+ // Success.
+ return objInfo
+}
+
+// TransitionInfoEquals returns true if transition related information are equal, false otherwise.
+func (fi FileInfo) TransitionInfoEquals(ofi FileInfo) bool {
+ switch {
+ case fi.TransitionStatus != ofi.TransitionStatus,
+ fi.TransitionTier != ofi.TransitionTier,
+ fi.TransitionedObjName != ofi.TransitionedObjName,
+ fi.TransitionVersionID != ofi.TransitionVersionID:
+ return false
+ }
+ return true
+}
+
+// MetadataEquals returns true if FileInfos Metadata maps are equal, false otherwise.
+func (fi FileInfo) MetadataEquals(ofi FileInfo) bool {
+ if len(fi.Metadata) != len(ofi.Metadata) {
+ return false
+ }
+ for k, v := range fi.Metadata {
+ if ov, ok := ofi.Metadata[k]; !ok || ov != v {
+ return false
+ }
+ }
+ return true
+}
+
+// ReplicationInfoEquals returns true if server-side replication related fields are equal, false otherwise.
+func (fi FileInfo) ReplicationInfoEquals(ofi FileInfo) bool {
+ switch {
+ case fi.MarkDeleted != ofi.MarkDeleted,
+ !fi.ReplicationState.Equal(ofi.ReplicationState):
+ return false
+ }
+ return true
+}
+
+// objectPartIndex - returns the index of matching object part number.
+// Returns -1 if the part cannot be found.
+func objectPartIndex(parts []ObjectPartInfo, partNumber int) int {
+ for i, part := range parts {
+ if partNumber == part.Number {
+ return i
+ }
+ }
+ return -1
+}
+
+// objectPartIndexNums returns the index of the specified part number.
+// Returns -1 if the part cannot be found.
+func objectPartIndexNums(parts []int, partNumber int) int {
+ for i, part := range parts {
+ if part != 0 && partNumber == part {
+ return i
+ }
+ }
+ return -1
+}
+
+// AddObjectPart - add a new object part in order.
+func (fi *FileInfo) AddObjectPart(partNumber int, partETag string, partSize, actualSize int64, modTime time.Time, idx []byte, checksums map[string]string) {
+ partInfo := ObjectPartInfo{
+ Number: partNumber,
+ ETag: partETag,
+ Size: partSize,
+ ActualSize: actualSize,
+ ModTime: modTime,
+ Index: idx,
+ Checksums: checksums,
+ }
+
+ // Update part info if it already exists.
+ for i, part := range fi.Parts {
+ if partNumber == part.Number {
+ fi.Parts[i] = partInfo
+ return
+ }
+ }
+
+ // Proceed to include new part info.
+ fi.Parts = append(fi.Parts, partInfo)
+
+ // Parts in FileInfo should be in sorted order by part number.
+ sort.Slice(fi.Parts, func(i, j int) bool { return fi.Parts[i].Number < fi.Parts[j].Number })
+}
+
+// ObjectToPartOffset - translate offset of an object to offset of its individual part.
+func (fi FileInfo) ObjectToPartOffset(ctx context.Context, offset int64) (partIndex int, partOffset int64, err error) {
+ if offset == 0 {
+ // Special case - if offset is 0, then partIndex and partOffset are always 0.
+ return 0, 0, nil
+ }
+ partOffset = offset
+ // Seek until object offset maps to a particular part offset.
+ for i, part := range fi.Parts {
+ partIndex = i
+ // Offset is smaller than size we have reached the proper part offset.
+ if partOffset < part.Size {
+ return partIndex, partOffset, nil
+ }
+ // Continue to towards the next part.
+ partOffset -= part.Size
+ }
+ internalLogIf(ctx, InvalidRange{})
+ // Offset beyond the size of the object return InvalidRange.
+ return 0, 0, InvalidRange{}
+}
+
+func findFileInfoInQuorum(ctx context.Context, metaArr []FileInfo, modTime time.Time, etag string, quorum int) (FileInfo, error) {
+ // with less quorum return error.
+ if quorum < 1 {
+ return FileInfo{}, InsufficientReadQuorum{Err: errErasureReadQuorum, Type: RQInsufficientOnlineDrives}
+ }
+ metaHashes := make([]string, len(metaArr))
+ h := sha256.New()
+ for i, meta := range metaArr {
+ if !meta.IsValid() {
+ continue
+ }
+ etagOnly := modTime.Equal(timeSentinel) && (etag != "" && etag == meta.Metadata["etag"])
+ mtimeValid := meta.ModTime.Equal(modTime)
+ if mtimeValid || etagOnly {
+ fmt.Fprintf(h, "%v", meta.XLV1)
+ for _, part := range meta.Parts {
+ fmt.Fprintf(h, "part.%d", part.Number)
+ fmt.Fprintf(h, "part.%d", part.Size)
+ }
+ // Previously we checked if we had quorum on DataDir value.
+ // We have removed this check to allow reading objects with different DataDir
+ // values in a few drives (due to a rebalance-stop race bug)
+ // provided their their etags or ModTimes match.
+
+ if !meta.Deleted && meta.Size != 0 {
+ fmt.Fprintf(h, "%v+%v", meta.Erasure.DataBlocks, meta.Erasure.ParityBlocks)
+ fmt.Fprintf(h, "%v", meta.Erasure.Distribution)
+ }
+
+ if meta.IsRemote() {
+ // ILM transition fields
+ fmt.Fprint(h, meta.TransitionStatus)
+ fmt.Fprint(h, meta.TransitionTier)
+ fmt.Fprint(h, meta.TransitionedObjName)
+ fmt.Fprint(h, meta.TransitionVersionID)
+ }
+
+ // If metadata says encrypted, ask for it in quorum.
+ if etyp, ok := crypto.IsEncrypted(meta.Metadata); ok {
+ fmt.Fprint(h, etyp)
+ }
+
+ // If compressed, look for compressed FileInfo only
+ if meta.IsCompressed() {
+ fmt.Fprint(h, meta.Metadata[ReservedMetadataPrefix+"compression"])
+ }
+
+ metaHashes[i] = hex.EncodeToString(h.Sum(nil))
+ h.Reset()
+ }
+ }
+
+ metaHashCountMap := make(map[string]int)
+ for _, hash := range metaHashes {
+ if hash == "" {
+ continue
+ }
+ metaHashCountMap[hash]++
+ }
+
+ maxHash := ""
+ maxCount := 0
+ for hash, count := range metaHashCountMap {
+ if count > maxCount {
+ maxCount = count
+ maxHash = hash
+ }
+ }
+
+ if maxCount < quorum {
+ return FileInfo{}, InsufficientReadQuorum{Err: errErasureReadQuorum, Type: RQInconsistentMeta}
+ }
+
+ // objProps represents properties that go beyond a single version
+ type objProps struct {
+ succModTime time.Time
+ numVersions int
+ }
+ // Find the successor mod time and numVersions in quorum, otherwise leave the
+ // candidate as found
+ otherPropsMap := make(counterMap[objProps])
+ var candidate FileInfo
+ var found bool
+ for i, hash := range metaHashes {
+ if hash == maxHash {
+ if metaArr[i].IsValid() {
+ if !found {
+ candidate = metaArr[i]
+ found = true
+ }
+ props := objProps{
+ succModTime: metaArr[i].SuccessorModTime,
+ numVersions: metaArr[i].NumVersions,
+ }
+ otherPropsMap[props]++
+ }
+ }
+ }
+
+ if found {
+ // Update candidate FileInfo with succModTime and numVersions in quorum when available
+ if props, ok := otherPropsMap.GetValueWithQuorum(quorum); ok {
+ candidate.SuccessorModTime = props.succModTime
+ candidate.IsLatest = props.succModTime.IsZero()
+ candidate.NumVersions = props.numVersions
+ }
+ return candidate, nil
+ }
+ return FileInfo{}, InsufficientReadQuorum{Err: errErasureReadQuorum, Type: RQInconsistentMeta}
+}
+
+// pickValidFileInfo - picks one valid FileInfo content and returns from a
+// slice of FileInfo.
+func pickValidFileInfo(ctx context.Context, metaArr []FileInfo, modTime time.Time, etag string, quorum int) (FileInfo, error) {
+ return findFileInfoInQuorum(ctx, metaArr, modTime, etag, quorum)
+}
+
+func writeAllMetadataWithRevert(ctx context.Context, disks []StorageAPI, origbucket, bucket, prefix string, files []FileInfo, quorum int, revert bool) ([]StorageAPI, error) {
+ g := errgroup.WithNErrs(len(disks))
+
+ // Start writing `xl.meta` to all disks in parallel.
+ for index := range disks {
+ g.Go(func() error {
+ if disks[index] == nil {
+ return errDiskNotFound
+ }
+ // Pick one FileInfo for a disk at index.
+ fi := files[index]
+ fi.Erasure.Index = index + 1
+ if fi.IsValid() {
+ return disks[index].WriteMetadata(ctx, origbucket, bucket, prefix, fi)
+ }
+ return errFileCorrupt
+ }, index)
+ }
+
+ // Wait for all the routines.
+ mErrs := g.Wait()
+
+ err := reduceWriteQuorumErrs(ctx, mErrs, objectOpIgnoredErrs, quorum)
+ if err != nil && revert {
+ ng := errgroup.WithNErrs(len(disks))
+ for index := range disks {
+ if mErrs[index] != nil {
+ continue
+ }
+ index := index
+ ng.Go(func() error {
+ if disks[index] == nil {
+ return errDiskNotFound
+ }
+ return disks[index].Delete(ctx, bucket, pathJoin(prefix, xlStorageFormatFile), DeleteOptions{
+ Recursive: true,
+ })
+ }, index)
+ }
+ ng.Wait()
+ }
+
+ return evalDisks(disks, mErrs), err
+}
+
+func writeAllMetadata(ctx context.Context, disks []StorageAPI, origbucket, bucket, prefix string, files []FileInfo, quorum int) ([]StorageAPI, error) {
+ return writeAllMetadataWithRevert(ctx, disks, origbucket, bucket, prefix, files, quorum, true)
+}
+
+// writeUniqueFileInfo - writes unique `xl.meta` content for each disk concurrently.
+func writeUniqueFileInfo(ctx context.Context, disks []StorageAPI, origbucket, bucket, prefix string, files []FileInfo, quorum int) ([]StorageAPI, error) {
+ return writeAllMetadataWithRevert(ctx, disks, origbucket, bucket, prefix, files, quorum, false)
+}
+
+func commonParity(parities []int, defaultParityCount int) int {
+ N := len(parities)
+
+ occMap := make(map[int]int)
+ for _, p := range parities {
+ occMap[p]++
+ }
+
+ var maxOcc, cparity int
+ for parity, occ := range occMap {
+ if parity == -1 {
+ // Ignore non defined parity
+ continue
+ }
+
+ readQuorum := N - parity
+ if defaultParityCount > 0 && parity == 0 {
+ // In this case, parity == 0 implies that this object version is a
+ // delete marker
+ readQuorum = N/2 + 1
+ }
+ if occ < readQuorum {
+ // Ignore this parity since we don't have enough shards for read quorum
+ continue
+ }
+
+ if occ > maxOcc {
+ maxOcc = occ
+ cparity = parity
+ }
+ }
+
+ if maxOcc == 0 {
+ // Did not found anything useful
+ return -1
+ }
+ return cparity
+}
+
+func listObjectParities(partsMetadata []FileInfo, errs []error) (parities []int) {
+ totalShards := len(partsMetadata)
+ parities = make([]int, len(partsMetadata))
+ for index, metadata := range partsMetadata {
+ if errs[index] != nil {
+ parities[index] = -1
+ continue
+ }
+ if !metadata.IsValid() {
+ parities[index] = -1
+ continue
+ }
+ //nolint:gocritic
+ // Delete marker or zero byte objects take highest parity.
+ if metadata.Deleted || metadata.Size == 0 {
+ parities[index] = totalShards / 2
+ } 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.
+ // However, we never go below EcM, in case of a EcM=EcN setup.
+ parities[index] = max(totalShards-(totalShards/2+1), metadata.Erasure.ParityBlocks)
+ } else {
+ parities[index] = metadata.Erasure.ParityBlocks
+ }
+ }
+ return parities
+}
+
+// Returns per object readQuorum and writeQuorum
+// readQuorum is the min required disks to read data.
+// writeQuorum is the min required disks to write data.
+func objectQuorumFromMeta(ctx context.Context, partsMetaData []FileInfo, errs []error, defaultParityCount int) (objectReadQuorum, objectWriteQuorum int, err error) {
+ // There should be at least half correct entries, if not return failure
+ expectedRQuorum := len(partsMetaData) / 2
+ if defaultParityCount == 0 {
+ // if parity count is '0', we expected all entries to be present.
+ expectedRQuorum = len(partsMetaData)
+ }
+
+ reducedErr := reduceReadQuorumErrs(ctx, errs, objectOpIgnoredErrs, expectedRQuorum)
+ if reducedErr != nil {
+ return -1, -1, reducedErr
+ }
+
+ // special case when parity is '0'
+ if defaultParityCount == 0 {
+ return len(partsMetaData), len(partsMetaData), nil
+ }
+
+ parities := listObjectParities(partsMetaData, errs)
+ parityBlocks := commonParity(parities, defaultParityCount)
+ if parityBlocks < 0 {
+ return -1, -1, InsufficientReadQuorum{Err: errErasureReadQuorum, Type: RQInsufficientOnlineDrives}
+ }
+
+ dataBlocks := len(partsMetaData) - parityBlocks
+
+ writeQuorum := dataBlocks
+ if dataBlocks == parityBlocks {
+ writeQuorum++
+ }
+
+ // Since all the valid erasure code meta updated at the same time are equivalent, pass dataBlocks
+ // from latestFileInfo to get the quorum
+ return dataBlocks, writeQuorum, nil
+}
+
+const (
+ tierFVID = "tier-free-versionID"
+ tierFVMarker = "tier-free-marker"
+ tierSkipFVID = "tier-skip-fvid"
+)
+
+// SetTierFreeVersionID sets free-version's versionID. This method is used by
+// object layer to pass down a versionID to set for a free-version that may be
+// created.
+func (fi *FileInfo) SetTierFreeVersionID(versionID string) {
+ if fi.Metadata == nil {
+ fi.Metadata = make(map[string]string)
+ }
+ fi.Metadata[ReservedMetadataPrefixLower+tierFVID] = versionID
+}
+
+// TierFreeVersionID returns the free-version's version id.
+func (fi *FileInfo) TierFreeVersionID() string {
+ return fi.Metadata[ReservedMetadataPrefixLower+tierFVID]
+}
+
+// SetTierFreeVersion sets fi as a free-version. This method is used by
+// lower layers to indicate a free-version.
+func (fi *FileInfo) SetTierFreeVersion() {
+ if fi.Metadata == nil {
+ fi.Metadata = make(map[string]string)
+ }
+ fi.Metadata[ReservedMetadataPrefixLower+tierFVMarker] = ""
+}
+
+// SetSkipTierFreeVersion indicates to skip adding a tier free version id.
+// Note: Used only when expiring tiered objects and the remote content has
+// already been scheduled for deletion
+func (fi *FileInfo) SetSkipTierFreeVersion() {
+ if fi.Metadata == nil {
+ fi.Metadata = make(map[string]string)
+ }
+ fi.Metadata[ReservedMetadataPrefixLower+tierSkipFVID] = ""
+}
+
+// SkipTierFreeVersion returns true if set, false otherwise.
+// See SetSkipTierVersion for its purpose.
+func (fi *FileInfo) SkipTierFreeVersion() bool {
+ _, ok := fi.Metadata[ReservedMetadataPrefixLower+tierSkipFVID]
+ return ok
+}
+
+// TierFreeVersion returns true if version is a free-version.
+func (fi *FileInfo) TierFreeVersion() bool {
+ _, ok := fi.Metadata[ReservedMetadataPrefixLower+tierFVMarker]
+ return ok
+}
+
+// IsRestoreObjReq returns true if fi corresponds to a RestoreObject request.
+func (fi *FileInfo) IsRestoreObjReq() bool {
+ if restoreHdr, ok := fi.Metadata[xhttp.AmzRestore]; ok {
+ if restoreStatus, err := parseRestoreObjStatus(restoreHdr); err == nil {
+ if !restoreStatus.Ongoing() {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+// VersionPurgeStatus returns overall version purge status for this object version across targets
+func (fi *FileInfo) VersionPurgeStatus() VersionPurgeStatusType {
+ return fi.ReplicationState.CompositeVersionPurgeStatus()
+}
+
+// ReplicationStatus returns overall version replication status for this object version across targets
+func (fi *FileInfo) ReplicationStatus() replication.StatusType {
+ return fi.ReplicationState.CompositeReplicationStatus()
+}
+
+// DeleteMarkerReplicationStatus returns overall replication status for this delete marker version across targets
+func (fi *FileInfo) DeleteMarkerReplicationStatus() replication.StatusType {
+ if fi.Deleted {
+ return fi.ReplicationState.CompositeReplicationStatus()
+ }
+ return replication.StatusType("")
+}
+
+// GetInternalReplicationState is a wrapper method to fetch internal replication state from the map m
+func GetInternalReplicationState(m map[string][]byte) ReplicationState {
+ m1 := make(map[string]string, len(m))
+ for k, v := range m {
+ m1[k] = string(v)
+ }
+ return getInternalReplicationState(m1)
+}
+
+// getInternalReplicationState fetches internal replication state from the map m
+func getInternalReplicationState(m map[string]string) ReplicationState {
+ d := ReplicationState{}
+ for k, v := range m {
+ switch {
+ case equals(k, ReservedMetadataPrefixLower+ReplicationTimestamp):
+ d.ReplicaTimeStamp, _ = amztime.ParseReplicationTS(v)
+ case equals(k, ReservedMetadataPrefixLower+ReplicaTimestamp):
+ d.ReplicaTimeStamp, _ = amztime.ParseReplicationTS(v)
+ case equals(k, ReservedMetadataPrefixLower+ReplicaStatus):
+ d.ReplicaStatus = replication.StatusType(v)
+ case equals(k, ReservedMetadataPrefixLower+ReplicationStatus):
+ d.ReplicationStatusInternal = v
+ d.Targets = replicationStatusesMap(v)
+ case equals(k, VersionPurgeStatusKey):
+ d.VersionPurgeStatusInternal = v
+ d.PurgeTargets = versionPurgeStatusesMap(v)
+ case strings.HasPrefix(k, ReservedMetadataPrefixLower+ReplicationReset):
+ arn := strings.TrimPrefix(k, fmt.Sprintf("%s-", ReservedMetadataPrefixLower+ReplicationReset))
+ if d.ResetStatusesMap == nil {
+ d.ResetStatusesMap = make(map[string]string, 1)
+ }
+ d.ResetStatusesMap[arn] = v
+ }
+ }
+ return d
+}
diff --git a/cmd/erasure-metadata_test.go b/cmd/erasure-metadata_test.go
new file mode 100644
index 00000000..76ee2e10
--- /dev/null
+++ b/cmd/erasure-metadata_test.go
@@ -0,0 +1,482 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "fmt"
+ "slices"
+ "strconv"
+ "testing"
+ "time"
+
+ "github.com/dustin/go-humanize"
+)
+
+const ActualSize = 1000
+
+// Test FileInfo.AddObjectPart()
+func TestAddObjectPart(t *testing.T) {
+ testCases := []struct {
+ partNum int
+ expectedIndex int
+ }{
+ {1, 0},
+ {2, 1},
+ {4, 2},
+ {5, 3},
+ {7, 4},
+ // Insert part.
+ {3, 2},
+ // Replace existing part.
+ {4, 3},
+ // Missing part.
+ {6, -1},
+ }
+
+ // Setup.
+ fi := newFileInfo("test-object", 8, 8)
+ fi.Erasure.Index = 1
+ if !fi.IsValid() {
+ t.Fatalf("unable to get xl meta")
+ }
+
+ // Test them.
+ for _, testCase := range testCases {
+ if testCase.expectedIndex > -1 {
+ partNumString := strconv.Itoa(testCase.partNum)
+ fi.AddObjectPart(testCase.partNum, "etag."+partNumString, int64(testCase.partNum+humanize.MiByte), ActualSize, UTCNow(), nil, nil)
+ }
+
+ if index := objectPartIndex(fi.Parts, testCase.partNum); index != testCase.expectedIndex {
+ t.Fatalf("%+v: expected = %d, got: %d", testCase, testCase.expectedIndex, index)
+ }
+ }
+}
+
+// Test objectPartIndex(). generates a sample FileInfo data and asserts
+// the output of objectPartIndex() with the expected value.
+func TestObjectPartIndex(t *testing.T) {
+ testCases := []struct {
+ partNum int
+ expectedIndex int
+ }{
+ {2, 1},
+ {1, 0},
+ {5, 3},
+ {4, 2},
+ {7, 4},
+ }
+
+ // Setup.
+ fi := newFileInfo("test-object", 8, 8)
+ fi.Erasure.Index = 1
+ if !fi.IsValid() {
+ t.Fatalf("unable to get xl meta")
+ }
+
+ // Add some parts for testing.
+ for _, testCase := range testCases {
+ partNumString := strconv.Itoa(testCase.partNum)
+ fi.AddObjectPart(testCase.partNum, "etag."+partNumString, int64(testCase.partNum+humanize.MiByte), ActualSize, UTCNow(), nil, nil)
+ }
+
+ // Add failure test case.
+ testCases = append(testCases, struct {
+ partNum int
+ expectedIndex int
+ }{6, -1})
+
+ // Test them.
+ for _, testCase := range testCases {
+ if index := objectPartIndex(fi.Parts, testCase.partNum); index != testCase.expectedIndex {
+ t.Fatalf("%+v: expected = %d, got: %d", testCase, testCase.expectedIndex, index)
+ }
+ }
+}
+
+// Test FileInfo.ObjectToPartOffset().
+func TestObjectToPartOffset(t *testing.T) {
+ // Setup.
+ fi := newFileInfo("test-object", 8, 8)
+ fi.Erasure.Index = 1
+ if !fi.IsValid() {
+ t.Fatalf("unable to get xl meta")
+ }
+
+ // Add some parts for testing.
+ // Total size of all parts is 5,242,899 bytes.
+ for _, partNum := range []int{1, 2, 4, 5, 7} {
+ partNumString := strconv.Itoa(partNum)
+ fi.AddObjectPart(partNum, "etag."+partNumString, int64(partNum+humanize.MiByte), ActualSize, UTCNow(), nil, nil)
+ }
+
+ testCases := []struct {
+ offset int64
+ expectedIndex int
+ expectedOffset int64
+ expectedErr error
+ }{
+ {0, 0, 0, nil},
+ {1 * humanize.MiByte, 0, 1 * humanize.MiByte, nil},
+ {1 + humanize.MiByte, 1, 0, nil},
+ {2 + humanize.MiByte, 1, 1, nil},
+ // Its valid for zero sized object.
+ {-1, 0, -1, nil},
+ // Max fffset is always (size - 1).
+ {(1 + 2 + 4 + 5 + 7) + (5 * humanize.MiByte) - 1, 4, 1048582, nil},
+ // Error if offset is size.
+ {(1 + 2 + 4 + 5 + 7) + (5 * humanize.MiByte), 0, 0, InvalidRange{}},
+ }
+
+ // Test them.
+ for _, testCase := range testCases {
+ index, offset, err := fi.ObjectToPartOffset(t.Context(), testCase.offset)
+ if err != testCase.expectedErr {
+ t.Fatalf("%+v: expected = %s, got: %s", testCase, testCase.expectedErr, err)
+ }
+ if index != testCase.expectedIndex {
+ t.Fatalf("%+v: index: expected = %d, got: %d", testCase, testCase.expectedIndex, index)
+ }
+ if offset != testCase.expectedOffset {
+ t.Fatalf("%+v: offset: expected = %d, got: %d", testCase, testCase.expectedOffset, offset)
+ }
+ }
+}
+
+func TestFindFileInfoInQuorum(t *testing.T) {
+ getNFInfo := func(n int, quorum int, t int64, dataDir string, succModTimes []time.Time, numVersions []int) []FileInfo {
+ fi := newFileInfo("test", 8, 8)
+ fi.AddObjectPart(1, "etag", 100, 100, UTCNow(), nil, nil)
+ fi.ModTime = time.Unix(t, 0)
+ fi.DataDir = dataDir
+ fis := make([]FileInfo, n)
+ for i := range fis {
+ fis[i] = fi
+ fis[i].Erasure.Index = i + 1
+ if succModTimes != nil {
+ fis[i].SuccessorModTime = succModTimes[i]
+ fis[i].IsLatest = succModTimes[i].IsZero()
+ }
+ if numVersions != nil {
+ fis[i].NumVersions = numVersions[i]
+ }
+ quorum--
+ if quorum == 0 {
+ break
+ }
+ }
+ return fis
+ }
+
+ commonSuccModTime := time.Date(2023, time.August, 25, 0, 0, 0, 0, time.UTC)
+ succModTimesInQuorum := make([]time.Time, 16)
+ succModTimesNoQuorum := make([]time.Time, 16)
+ commonNumVersions := 2
+ numVersionsInQuorum := make([]int, 16)
+ numVersionsNoQuorum := make([]int, 16)
+ for i := range 16 {
+ if i < 4 {
+ continue
+ }
+ succModTimesInQuorum[i] = commonSuccModTime
+ numVersionsInQuorum[i] = commonNumVersions
+ if i < 9 {
+ continue
+ }
+ succModTimesNoQuorum[i] = commonSuccModTime
+ numVersionsNoQuorum[i] = commonNumVersions
+ }
+ tests := []struct {
+ fis []FileInfo
+ modTime time.Time
+ succmodTimes []time.Time
+ numVersions []int
+ expectedErr error
+ expectedQuorum int
+ expectedSuccModTime time.Time
+ expectedNumVersions int
+ expectedIsLatest bool
+ }{
+ {
+ fis: getNFInfo(16, 16, 1603863445, "36a21454-a2ca-11eb-bbaa-93a81c686f21", nil, nil),
+ modTime: time.Unix(1603863445, 0),
+ expectedErr: nil,
+ expectedQuorum: 8,
+ },
+ {
+ fis: getNFInfo(16, 7, 1603863445, "36a21454-a2ca-11eb-bbaa-93a81c686f21", nil, nil),
+ modTime: time.Unix(1603863445, 0),
+ expectedErr: InsufficientReadQuorum{},
+ expectedQuorum: 8,
+ },
+ {
+ fis: getNFInfo(16, 16, 1603863445, "36a21454-a2ca-11eb-bbaa-93a81c686f21", nil, nil),
+ modTime: time.Unix(1603863445, 0),
+ expectedErr: InsufficientReadQuorum{},
+ expectedQuorum: 0,
+ },
+ {
+ fis: getNFInfo(16, 16, 1603863445, "36a21454-a2ca-11eb-bbaa-93a81c686f21", succModTimesInQuorum, nil),
+ modTime: time.Unix(1603863445, 0),
+ succmodTimes: succModTimesInQuorum,
+ expectedErr: nil,
+ expectedQuorum: 12,
+ expectedSuccModTime: commonSuccModTime,
+ expectedIsLatest: false,
+ },
+ {
+ fis: getNFInfo(16, 16, 1603863445, "36a21454-a2ca-11eb-bbaa-93a81c686f21", succModTimesNoQuorum, nil),
+ modTime: time.Unix(1603863445, 0),
+ succmodTimes: succModTimesNoQuorum,
+ expectedErr: nil,
+ expectedQuorum: 12,
+ expectedSuccModTime: time.Time{},
+ expectedIsLatest: true,
+ },
+ {
+ fis: getNFInfo(16, 16, 1603863445, "36a21454-a2ca-11eb-bbaa-93a81c686f21", nil, numVersionsInQuorum),
+ modTime: time.Unix(1603863445, 0),
+ numVersions: numVersionsInQuorum,
+ expectedErr: nil,
+ expectedQuorum: 12,
+ expectedIsLatest: true,
+ expectedNumVersions: 2,
+ },
+ {
+ fis: getNFInfo(16, 16, 1603863445, "36a21454-a2ca-11eb-bbaa-93a81c686f21", nil, numVersionsNoQuorum),
+ modTime: time.Unix(1603863445, 0),
+ numVersions: numVersionsNoQuorum,
+ expectedErr: nil,
+ expectedQuorum: 12,
+ expectedIsLatest: true,
+ expectedNumVersions: 0,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run("", func(t *testing.T) {
+ fi, err := findFileInfoInQuorum(t.Context(), test.fis, test.modTime, "", test.expectedQuorum)
+ _, ok1 := err.(InsufficientReadQuorum)
+ _, ok2 := test.expectedErr.(InsufficientReadQuorum)
+ if ok1 != ok2 {
+ t.Errorf("Expected %s, got %s", test.expectedErr, err)
+ }
+ if test.succmodTimes != nil {
+ if !test.expectedSuccModTime.Equal(fi.SuccessorModTime) {
+ t.Errorf("Expected successor mod time to be %v but got %v", test.expectedSuccModTime, fi.SuccessorModTime)
+ }
+ if test.expectedIsLatest != fi.IsLatest {
+ t.Errorf("Expected IsLatest to be %v but got %v", test.expectedIsLatest, fi.IsLatest)
+ }
+ }
+ if test.numVersions != nil && test.expectedNumVersions > 0 {
+ if test.expectedNumVersions != fi.NumVersions {
+ t.Errorf("Expected Numversions to be %d but got %d", test.expectedNumVersions, fi.NumVersions)
+ }
+ }
+ })
+ }
+}
+
+func TestTransitionInfoEquals(t *testing.T) {
+ inputs := []struct {
+ tier string
+ remoteObjName string
+ remoteVersionID string
+ status string
+ }{
+ {
+ tier: "S3TIER-1",
+ remoteObjName: mustGetUUID(),
+ remoteVersionID: mustGetUUID(),
+ status: "complete",
+ },
+ {
+ tier: "S3TIER-2",
+ remoteObjName: mustGetUUID(),
+ remoteVersionID: mustGetUUID(),
+ status: "complete",
+ },
+ }
+
+ var i uint
+ for i = range uint(8) {
+ fi := FileInfo{
+ TransitionTier: inputs[0].tier,
+ TransitionedObjName: inputs[0].remoteObjName,
+ TransitionVersionID: inputs[0].remoteVersionID,
+ TransitionStatus: inputs[0].status,
+ }
+ ofi := fi
+ if i&(1<<0) != 0 {
+ ofi.TransitionTier = inputs[1].tier
+ }
+ if i&(1<<1) != 0 {
+ ofi.TransitionedObjName = inputs[1].remoteObjName
+ }
+ if i&(1<<2) != 0 {
+ ofi.TransitionVersionID = inputs[1].remoteVersionID
+ }
+ actual := fi.TransitionInfoEquals(ofi)
+ if i == 0 && !actual {
+ t.Fatalf("Test %d: Expected FileInfo's transition info to be equal: fi %v ofi %v", i, fi, ofi)
+ }
+ if i != 0 && actual {
+ t.Fatalf("Test %d: Expected FileInfo's transition info to be inequal: fi %v ofi %v", i, fi, ofi)
+ }
+ }
+ fi := FileInfo{
+ TransitionTier: inputs[0].tier,
+ TransitionedObjName: inputs[0].remoteObjName,
+ TransitionVersionID: inputs[0].remoteVersionID,
+ TransitionStatus: inputs[0].status,
+ }
+ ofi := FileInfo{}
+ if fi.TransitionInfoEquals(ofi) {
+ t.Fatalf("Expected to be inequal: fi %v ofi %v", fi, ofi)
+ }
+}
+
+func TestSkipTierFreeVersion(t *testing.T) {
+ fi := newFileInfo("object", 8, 8)
+ fi.SetSkipTierFreeVersion()
+ if ok := fi.SkipTierFreeVersion(); !ok {
+ t.Fatal("Expected SkipTierFreeVersion to be set on FileInfo but wasn't")
+ }
+}
+
+func TestListObjectParities(t *testing.T) {
+ mkMetaArr := func(N, parity, agree int) []FileInfo {
+ fi := newFileInfo("obj-1", N-parity, parity)
+ fi.TransitionTier = "WARM-TIER"
+ fi.TransitionedObjName = mustGetUUID()
+ fi.TransitionStatus = "complete"
+ fi.Size = 1 << 20
+
+ metaArr := make([]FileInfo, N)
+ for i := range N {
+ fi.Erasure.Index = i + 1
+ metaArr[i] = fi
+ if i < agree {
+ continue
+ }
+ metaArr[i].TransitionTier, metaArr[i].TransitionedObjName = "", ""
+ metaArr[i].TransitionStatus = ""
+ }
+ return metaArr
+ }
+ mkParities := func(N, agreedParity, disagreedParity, agree int) []int {
+ ps := make([]int, N)
+ for i := range N {
+ if i < agree {
+ ps[i] = agreedParity
+ continue
+ }
+ ps[i] = disagreedParity // disagree
+ }
+ return ps
+ }
+
+ mkTest := func(N, parity, agree int) (res struct {
+ metaArr []FileInfo
+ errs []error
+ parities []int
+ parity int
+ },
+ ) {
+ res.metaArr = mkMetaArr(N, parity, agree)
+ res.parities = mkParities(N, N-(N/2+1), parity, agree)
+ res.errs = make([]error, N)
+ if agree >= N/2+1 { // simple majority consensus
+ res.parity = N - (N/2 + 1)
+ } else {
+ res.parity = -1
+ }
+ return res
+ }
+
+ nonTieredTest := func(N, parity, agree int) (res struct {
+ metaArr []FileInfo
+ errs []error
+ parities []int
+ parity int
+ },
+ ) {
+ fi := newFileInfo("obj-1", N-parity, parity)
+ fi.Size = 1 << 20
+ metaArr := make([]FileInfo, N)
+ parities := make([]int, N)
+ for i := range N {
+ fi.Erasure.Index = i + 1
+ metaArr[i] = fi
+ parities[i] = parity
+ if i < agree {
+ continue
+ }
+ metaArr[i].Erasure.Index = 0 // creates invalid fi on remaining drives
+ parities[i] = -1 // invalid fi are assigned parity -1
+ }
+ res.metaArr = metaArr
+ res.parities = parities
+ res.errs = make([]error, N)
+ if agree >= N-parity {
+ res.parity = parity
+ } else {
+ res.parity = -1
+ }
+
+ return res
+ }
+ tests := []struct {
+ metaArr []FileInfo
+ errs []error
+ parities []int
+ parity int
+ }{
+ // More than simple majority consensus
+ mkTest(15, 3, 11),
+ // No simple majority consensus
+ mkTest(15, 3, 7),
+ // Exact simple majority consensus
+ mkTest(15, 3, 8),
+ // More than simple majority consensus
+ mkTest(16, 4, 11),
+ // No simple majority consensus
+ mkTest(16, 4, 8),
+ // Exact simple majority consensus
+ mkTest(16, 4, 9),
+ // non-tiered object require read quorum of EcM
+ nonTieredTest(15, 3, 12),
+ // non-tiered object with fewer than EcM in consensus
+ nonTieredTest(15, 3, 11),
+ // non-tiered object require read quorum of EcM
+ nonTieredTest(16, 4, 12),
+ // non-tiered object with fewer than EcM in consensus
+ nonTieredTest(16, 4, 11),
+ }
+ for i, test := range tests {
+ t.Run(fmt.Sprintf("Test %d", i+1), func(t *testing.T) {
+ if got := listObjectParities(test.metaArr, test.errs); !slices.Equal(got, test.parities) {
+ t.Fatalf("Expected parities %v but got %v", test.parities, got)
+ }
+ if got := commonParity(test.parities, len(test.metaArr)/2); got != test.parity {
+ t.Fatalf("Expected common parity %v but got %v", test.parity, got)
+ }
+ })
+ }
+}
diff --git a/cmd/erasure-multipart.go b/cmd/erasure-multipart.go
new file mode 100644
index 00000000..2d85e9dc
--- /dev/null
+++ b/cmd/erasure-multipart.go
@@ -0,0 +1,1528 @@
+// Copyright (c) 2015-2025 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/klauspost/readahead"
+ "github.com/minio/minio-go/v7/pkg/set"
+ "github.com/minio/minio/internal/config/storageclass"
+ "github.com/minio/minio/internal/crypto"
+ "github.com/minio/minio/internal/hash"
+ xhttp "github.com/minio/minio/internal/http"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/mimedb"
+ "github.com/minio/pkg/v3/sync/errgroup"
+ "github.com/minio/sio"
+)
+
+func (er erasureObjects) getUploadIDDir(bucket, object, uploadID string) string {
+ uploadUUID := uploadID
+ uploadBytes, err := base64.RawURLEncoding.DecodeString(uploadID)
+ if err == nil {
+ slc := strings.SplitN(string(uploadBytes), ".", 2)
+ if len(slc) == 2 {
+ uploadUUID = slc[1]
+ }
+ }
+ return pathJoin(er.getMultipartSHADir(bucket, object), uploadUUID)
+}
+
+func (er erasureObjects) getMultipartSHADir(bucket, object string) string {
+ return getSHA256Hash([]byte(pathJoin(bucket, object)))
+}
+
+// checkUploadIDExists - verify if a given uploadID exists and is valid.
+func (er erasureObjects) checkUploadIDExists(ctx context.Context, bucket, object, uploadID string, write bool) (fi FileInfo, metArr []FileInfo, err error) {
+ defer func() {
+ if errors.Is(err, errFileNotFound) {
+ err = errUploadIDNotFound
+ }
+ }()
+
+ uploadIDPath := er.getUploadIDDir(bucket, object, uploadID)
+
+ storageDisks := er.getDisks()
+
+ // Read metadata associated with the object from all disks.
+ partsMetadata, errs := readAllFileInfo(ctx, storageDisks, bucket, minioMetaMultipartBucket,
+ uploadIDPath, "", false, false)
+
+ readQuorum, writeQuorum, err := objectQuorumFromMeta(ctx, partsMetadata, errs, er.defaultParityCount)
+ if err != nil {
+ return fi, nil, err
+ }
+
+ if readQuorum < 0 {
+ return fi, nil, errErasureReadQuorum
+ }
+
+ if writeQuorum < 0 {
+ return fi, nil, errErasureWriteQuorum
+ }
+
+ quorum := readQuorum
+ if write {
+ quorum = writeQuorum
+ }
+
+ // List all online disks.
+ _, modTime, etag := listOnlineDisks(storageDisks, partsMetadata, errs, quorum)
+
+ if write {
+ err = reduceWriteQuorumErrs(ctx, errs, objectOpIgnoredErrs, writeQuorum)
+ } else {
+ err = reduceReadQuorumErrs(ctx, errs, objectOpIgnoredErrs, readQuorum)
+ }
+ if err != nil {
+ return fi, nil, err
+ }
+
+ // Pick one from the first valid metadata.
+ fi, err = pickValidFileInfo(ctx, partsMetadata, modTime, etag, quorum)
+ return fi, partsMetadata, err
+}
+
+// cleanupMultipartPath removes all extraneous files and parts from the multipart folder, this is used per CompleteMultipart.
+// do not use this function outside of completeMultipartUpload()
+func (er erasureObjects) cleanupMultipartPath(ctx context.Context, paths ...string) {
+ storageDisks := er.getDisks()
+
+ g := errgroup.WithNErrs(len(storageDisks))
+ for index, disk := range storageDisks {
+ if disk == nil {
+ continue
+ }
+ index := index
+ g.Go(func() error {
+ _ = storageDisks[index].DeleteBulk(ctx, minioMetaMultipartBucket, paths...)
+ return nil
+ }, index)
+ }
+ g.Wait()
+}
+
+// Clean-up the old multipart uploads. Should be run in a Go routine.
+func (er erasureObjects) cleanupStaleUploads(ctx context.Context) {
+ // run multiple cleanup's local to this server.
+ var wg sync.WaitGroup
+ for _, disk := range er.getLocalDisks() {
+ if disk != nil {
+ wg.Add(1)
+ go func(disk StorageAPI) {
+ defer wg.Done()
+ er.cleanupStaleUploadsOnDisk(ctx, disk)
+ }(disk)
+ }
+ }
+ wg.Wait()
+}
+
+func (er erasureObjects) deleteAll(ctx context.Context, bucket, prefix string) {
+ var wg sync.WaitGroup
+ for _, disk := range er.getDisks() {
+ if disk == nil {
+ continue
+ }
+ wg.Add(1)
+ go func(disk StorageAPI) {
+ defer wg.Done()
+ disk.Delete(ctx, bucket, prefix, DeleteOptions{
+ Recursive: true,
+ Immediate: false,
+ })
+ }(disk)
+ }
+ wg.Wait()
+}
+
+// Remove the old multipart uploads on the given disk.
+func (er erasureObjects) cleanupStaleUploadsOnDisk(ctx context.Context, disk StorageAPI) {
+ drivePath := disk.Endpoint().Path
+
+ readDirFn(pathJoin(drivePath, minioMetaMultipartBucket), func(shaDir string, typ os.FileMode) error {
+ readDirFn(pathJoin(drivePath, minioMetaMultipartBucket, shaDir), func(uploadIDDir string, typ os.FileMode) error {
+ uploadIDPath := pathJoin(shaDir, uploadIDDir)
+ var modTime time.Time
+ // Upload IDs are of the form base64_url(x), we can extract the time from the UUID.
+ if b64, err := base64.RawURLEncoding.DecodeString(uploadIDDir); err == nil {
+ if split := strings.Split(string(b64), "x"); len(split) == 2 {
+ t, err := strconv.ParseInt(split[1], 10, 64)
+ if err == nil {
+ modTime = time.Unix(0, t)
+ }
+ }
+ }
+ // Fallback for older uploads without time in the ID.
+ if modTime.IsZero() {
+ wait := deleteMultipartCleanupSleeper.Timer(ctx)
+ fi, err := disk.ReadVersion(ctx, "", minioMetaMultipartBucket, uploadIDPath, "", ReadOptions{})
+ if err != nil {
+ return nil
+ }
+ modTime = fi.ModTime
+ wait()
+ }
+ if time.Since(modTime) < globalAPIConfig.getStaleUploadsExpiry() {
+ return nil
+ }
+ w := xioutil.NewDeadlineWorker(globalDriveConfig.GetMaxTimeout())
+ return w.Run(func() error {
+ wait := deleteMultipartCleanupSleeper.Timer(ctx)
+ pathUUID := mustGetUUID()
+ targetPath := pathJoin(drivePath, minioMetaTmpDeletedBucket, pathUUID)
+ renameAll(pathJoin(drivePath, minioMetaMultipartBucket, uploadIDPath), targetPath, pathJoin(drivePath, minioMetaBucket))
+ wait()
+ return nil
+ })
+ })
+ // Get the modtime of the shaDir.
+ vi, err := disk.StatVol(ctx, pathJoin(minioMetaMultipartBucket, shaDir))
+ if err != nil {
+ return nil
+ }
+ // Modtime is returned in the Created field. See (*xlStorage).StatVol
+ if time.Since(vi.Created) < globalAPIConfig.getStaleUploadsExpiry() {
+ return nil
+ }
+ w := xioutil.NewDeadlineWorker(globalDriveConfig.GetMaxTimeout())
+ return w.Run(func() error {
+ wait := deleteMultipartCleanupSleeper.Timer(ctx)
+ pathUUID := mustGetUUID()
+ targetPath := pathJoin(drivePath, minioMetaTmpDeletedBucket, pathUUID)
+
+ // We are not deleting shaDir recursively here, if shaDir is empty
+ // and its older then we can happily delete it.
+ Rename(pathJoin(drivePath, minioMetaMultipartBucket, shaDir), targetPath)
+ wait()
+ return nil
+ })
+ })
+
+ readDirFn(pathJoin(drivePath, minioMetaTmpBucket), func(tmpDir string, typ os.FileMode) error {
+ if strings.HasPrefix(tmpDir, ".trash") {
+ // do not remove .trash/ here, it has its own routines
+ return nil
+ }
+ vi, err := disk.StatVol(ctx, pathJoin(minioMetaTmpBucket, tmpDir))
+ if err != nil {
+ return nil
+ }
+ w := xioutil.NewDeadlineWorker(globalDriveConfig.GetMaxTimeout())
+ return w.Run(func() error {
+ wait := deleteMultipartCleanupSleeper.Timer(ctx)
+ if time.Since(vi.Created) > globalAPIConfig.getStaleUploadsExpiry() {
+ pathUUID := mustGetUUID()
+ targetPath := pathJoin(drivePath, minioMetaTmpDeletedBucket, pathUUID)
+
+ renameAll(pathJoin(drivePath, minioMetaTmpBucket, tmpDir), targetPath, pathJoin(drivePath, minioMetaBucket))
+ }
+ wait()
+ return nil
+ })
+ })
+}
+
+// ListMultipartUploads - lists all the pending multipart
+// uploads for a particular object in a bucket.
+//
+// Implements minimal S3 compatible ListMultipartUploads API. We do
+// not support prefix based listing, this is a deliberate attempt
+// towards simplification of multipart APIs.
+// The resulting ListMultipartsInfo structure is unmarshalled directly as XML.
+func (er erasureObjects) ListMultipartUploads(ctx context.Context, bucket, object, keyMarker, uploadIDMarker, delimiter string, maxUploads int) (result ListMultipartsInfo, err error) {
+ auditObjectErasureSet(ctx, "ListMultipartUploads", object, &er)
+
+ result.MaxUploads = maxUploads
+ result.KeyMarker = keyMarker
+ result.Prefix = object
+ result.Delimiter = delimiter
+
+ var uploadIDs []string
+ var disk StorageAPI
+ disks := er.getOnlineLocalDisks()
+ if len(disks) == 0 {
+ // If no local, get non-healing disks.
+ var ok bool
+ if disks, ok = er.getOnlineDisksWithHealing(false); !ok {
+ disks = er.getOnlineDisks()
+ }
+ }
+
+ for _, disk = range disks {
+ if disk == nil {
+ continue
+ }
+ if !disk.IsOnline() {
+ continue
+ }
+ uploadIDs, err = disk.ListDir(ctx, bucket, minioMetaMultipartBucket, er.getMultipartSHADir(bucket, object), -1)
+ if err != nil {
+ if errors.Is(err, errDiskNotFound) {
+ continue
+ }
+ if errors.Is(err, errFileNotFound) {
+ return result, nil
+ }
+ return result, toObjectErr(err, bucket, object)
+ }
+ break
+ }
+
+ for i := range uploadIDs {
+ uploadIDs[i] = strings.TrimSuffix(uploadIDs[i], SlashSeparator)
+ }
+
+ // S3 spec says uploadIDs should be sorted based on initiated time, we need
+ // to read the metadata entry.
+ var uploads []MultipartInfo
+
+ populatedUploadIDs := set.NewStringSet()
+
+ for _, uploadID := range uploadIDs {
+ if populatedUploadIDs.Contains(uploadID) {
+ continue
+ }
+ // If present, use time stored in ID.
+ startTime := time.Now()
+ if split := strings.Split(uploadID, "x"); len(split) == 2 {
+ t, err := strconv.ParseInt(split[1], 10, 64)
+ if err == nil {
+ startTime = time.Unix(0, t)
+ }
+ }
+ uploads = append(uploads, MultipartInfo{
+ Bucket: bucket,
+ Object: object,
+ UploadID: base64.RawURLEncoding.EncodeToString(fmt.Appendf(nil, "%s.%s", globalDeploymentID(), uploadID)),
+ Initiated: startTime,
+ })
+ populatedUploadIDs.Add(uploadID)
+ }
+
+ sort.Slice(uploads, func(i int, j int) bool {
+ return uploads[i].Initiated.Before(uploads[j].Initiated)
+ })
+
+ uploadIndex := 0
+ if uploadIDMarker != "" {
+ for uploadIndex < len(uploads) {
+ if uploads[uploadIndex].UploadID != uploadIDMarker {
+ uploadIndex++
+ continue
+ }
+ if uploads[uploadIndex].UploadID == uploadIDMarker {
+ uploadIndex++
+ break
+ }
+ uploadIndex++
+ }
+ }
+ for uploadIndex < len(uploads) {
+ result.Uploads = append(result.Uploads, uploads[uploadIndex])
+ result.NextUploadIDMarker = uploads[uploadIndex].UploadID
+ uploadIndex++
+ if len(result.Uploads) == maxUploads {
+ break
+ }
+ }
+
+ result.IsTruncated = uploadIndex < len(uploads)
+
+ if !result.IsTruncated {
+ result.NextKeyMarker = ""
+ result.NextUploadIDMarker = ""
+ }
+
+ return result, nil
+}
+
+// newMultipartUpload - wrapper for initializing a new multipart
+// request; returns a unique upload id.
+//
+// Internally this function creates 'uploads.json' associated for the
+// incoming object at
+// '.minio.sys/multipart/bucket/object/uploads.json' on all the
+// disks. `uploads.json` carries metadata regarding on-going multipart
+// operation(s) on the object.
+func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string, object string, opts ObjectOptions) (*NewMultipartUploadResult, error) {
+ if opts.CheckPrecondFn != nil {
+ if !opts.NoLock {
+ ns := er.NewNSLock(bucket, object)
+ lkctx, err := ns.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ return nil, err
+ }
+ ctx = lkctx.Context()
+ defer ns.Unlock(lkctx)
+ opts.NoLock = true
+ }
+
+ obj, err := er.getObjectInfo(ctx, bucket, object, opts)
+ if err == nil && opts.CheckPrecondFn(obj) {
+ return nil, PreConditionFailed{}
+ }
+ if err != nil && !isErrVersionNotFound(err) && !isErrObjectNotFound(err) && !isErrReadQuorum(err) {
+ return nil, err
+ }
+
+ // if object doesn't exist return error for If-Match conditional requests
+ // If-None-Match should be allowed to proceed for non-existent objects
+ if err != nil && opts.HasIfMatch && (isErrObjectNotFound(err) || isErrVersionNotFound(err)) {
+ return nil, err
+ }
+ }
+
+ userDefined := cloneMSS(opts.UserDefined)
+ if opts.PreserveETag != "" {
+ userDefined["etag"] = opts.PreserveETag
+ }
+ onlineDisks := er.getDisks()
+
+ // Get parity and data drive count based on storage class metadata
+ parityDrives := globalStorageClass.GetParityForSC(userDefined[xhttp.AmzStorageClass])
+ if parityDrives < 0 {
+ parityDrives = er.defaultParityCount
+ }
+
+ if globalStorageClass.AvailabilityOptimized() {
+ // If we have offline disks upgrade the number of erasure codes for this object.
+ parityOrig := parityDrives
+
+ var offlineDrives int
+ for _, disk := range onlineDisks {
+ if disk == nil || !disk.IsOnline() {
+ parityDrives++
+ offlineDrives++
+ continue
+ }
+ }
+
+ if offlineDrives >= (len(onlineDisks)+1)/2 {
+ // if offline drives are more than 50% of the drives
+ // we have no quorum, we shouldn't proceed just
+ // fail at that point.
+ return nil, toObjectErr(errErasureWriteQuorum, bucket, object)
+ }
+
+ if parityDrives >= len(onlineDisks)/2 {
+ parityDrives = len(onlineDisks) / 2
+ }
+
+ if parityOrig != parityDrives {
+ userDefined[minIOErasureUpgraded] = strconv.Itoa(parityOrig) + "->" + strconv.Itoa(parityDrives)
+ }
+ }
+
+ dataDrives := len(onlineDisks) - parityDrives
+
+ // we now know the number of blocks this object needs for data and parity.
+ // establish the writeQuorum using this data
+ writeQuorum := dataDrives
+ if dataDrives == parityDrives {
+ writeQuorum++
+ }
+
+ // Initialize parts metadata
+ partsMetadata := make([]FileInfo, len(onlineDisks))
+
+ fi := newFileInfo(pathJoin(bucket, object), dataDrives, parityDrives)
+ fi.VersionID = opts.VersionID
+ if opts.Versioned && fi.VersionID == "" {
+ fi.VersionID = mustGetUUID()
+ }
+ fi.DataDir = mustGetUUID()
+
+ if ckSum := userDefined[ReplicationSsecChecksumHeader]; ckSum != "" {
+ v, err := base64.StdEncoding.DecodeString(ckSum)
+ if err == nil {
+ fi.Checksum = v
+ }
+ delete(userDefined, ReplicationSsecChecksumHeader)
+ }
+
+ // Initialize erasure metadata.
+ for index := range partsMetadata {
+ partsMetadata[index] = fi
+ }
+
+ // Guess content-type from the extension if possible.
+ if userDefined["content-type"] == "" {
+ userDefined["content-type"] = mimedb.TypeByExtension(path.Ext(object))
+ }
+
+ // if storageClass is standard no need to save it as part of metadata.
+ if userDefined[xhttp.AmzStorageClass] == storageclass.STANDARD {
+ delete(userDefined, xhttp.AmzStorageClass)
+ }
+
+ if opts.WantChecksum != nil && opts.WantChecksum.Type.IsSet() {
+ userDefined[hash.MinIOMultipartChecksum] = opts.WantChecksum.Type.String()
+ userDefined[hash.MinIOMultipartChecksumType] = opts.WantChecksum.Type.ObjType()
+ }
+
+ modTime := opts.MTime
+ if opts.MTime.IsZero() {
+ modTime = UTCNow()
+ }
+
+ onlineDisks, partsMetadata = shuffleDisksAndPartsMetadata(onlineDisks, partsMetadata, fi)
+
+ // Fill all the necessary metadata.
+ // Update `xl.meta` content on each disks.
+ for index := range partsMetadata {
+ partsMetadata[index].Fresh = true
+ partsMetadata[index].ModTime = modTime
+ partsMetadata[index].Metadata = userDefined
+ }
+ uploadUUID := fmt.Sprintf("%sx%d", mustGetUUID(), modTime.UnixNano())
+ uploadID := base64.RawURLEncoding.EncodeToString(fmt.Appendf(nil, "%s.%s", globalDeploymentID(), uploadUUID))
+ uploadIDPath := er.getUploadIDDir(bucket, object, uploadUUID)
+
+ // Write updated `xl.meta` to all disks.
+ if _, err := writeAllMetadata(ctx, onlineDisks, bucket, minioMetaMultipartBucket, uploadIDPath, partsMetadata, writeQuorum); err != nil {
+ return nil, toObjectErr(err, bucket, object)
+ }
+
+ return &NewMultipartUploadResult{
+ UploadID: uploadID,
+ ChecksumAlgo: userDefined[hash.MinIOMultipartChecksum],
+ ChecksumType: userDefined[hash.MinIOMultipartChecksumType],
+ }, nil
+}
+
+// NewMultipartUpload - initialize a new multipart upload, returns a
+// unique id. The unique id returned here is of UUID form, for each
+// subsequent request each UUID is unique.
+//
+// Implements S3 compatible initiate multipart API.
+func (er erasureObjects) NewMultipartUpload(ctx context.Context, bucket, object string, opts ObjectOptions) (*NewMultipartUploadResult, error) {
+ if !opts.NoAuditLog {
+ auditObjectErasureSet(ctx, "NewMultipartUpload", object, &er)
+ }
+
+ return er.newMultipartUpload(ctx, bucket, object, opts)
+}
+
+// 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, skipParent string) ([]StorageAPI, error) {
+ paths := []string{
+ dstEntry,
+ dstEntry + ".meta",
+ }
+
+ // cleanup existing paths first across all drives.
+ er.cleanupMultipartPath(ctx, paths...)
+
+ g := errgroup.WithNErrs(len(disks))
+
+ // Rename file on all underlying storage disks.
+ for index := range disks {
+ g.Go(func() error {
+ if disks[index] == nil {
+ return errDiskNotFound
+ }
+ return disks[index].RenamePart(ctx, srcBucket, srcEntry, dstBucket, dstEntry, optsMeta, skipParent)
+ }, index)
+ }
+
+ // Wait for all renames to finish.
+ errs := g.Wait()
+
+ err := reduceWriteQuorumErrs(ctx, errs, objectOpIgnoredErrs, writeQuorum)
+ if err != nil {
+ er.cleanupMultipartPath(ctx, paths...)
+ }
+
+ // We can safely allow RenameFile errors up to len(er.getDisks()) - writeQuorum
+ // otherwise return failure. Cleanup successful renames.
+ return evalDisks(disks, errs), err
+}
+
+// PutObjectPart - reads incoming stream and internally erasure codes
+// them. This call is similar to single put operation but it is part
+// of the multipart transaction.
+//
+// Implements S3 compatible Upload Part API.
+func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, r *PutObjReader, opts ObjectOptions) (pi PartInfo, err error) {
+ if !opts.NoAuditLog {
+ auditObjectErasureSet(ctx, "PutObjectPart", object, &er)
+ }
+
+ data := r.Reader
+ // Validate input data size and it can never be less than zero.
+ if data.Size() < -1 {
+ bugLogIf(ctx, errInvalidArgument, logger.ErrorKind)
+ return pi, toObjectErr(errInvalidArgument)
+ }
+
+ uploadIDPath := er.getUploadIDDir(bucket, object, uploadID)
+ // Validates if upload ID exists.
+ fi, _, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, true)
+ if err != nil {
+ if errors.Is(err, errVolumeNotFound) {
+ return pi, toObjectErr(err, bucket)
+ }
+ return pi, toObjectErr(err, bucket, object, uploadID)
+ }
+
+ onlineDisks := er.getDisks()
+ writeQuorum := fi.WriteQuorum(er.defaultWQuorum())
+
+ if cs := fi.Metadata[hash.MinIOMultipartChecksum]; cs != "" {
+ if r.ContentCRCType().String() != cs {
+ return pi, InvalidArgument{
+ Bucket: bucket,
+ Object: fi.Name,
+ Err: fmt.Errorf("checksum missing, want %q, got %q", cs, r.ContentCRCType().String()),
+ }
+ }
+ }
+ onlineDisks = shuffleDisks(onlineDisks, fi.Erasure.Distribution)
+
+ // Need a unique name for the part being written in minioMetaBucket to
+ // accommodate concurrent PutObjectPart requests
+
+ partSuffix := fmt.Sprintf("part.%d", partID)
+ // Random UUID and timestamp for temporary part file.
+ tmpPart := fmt.Sprintf("%sx%d", mustGetUUID(), time.Now().UnixNano())
+ tmpPartPath := pathJoin(tmpPart, partSuffix)
+
+ // Delete the temporary object part. If PutObjectPart succeeds there would be nothing to delete.
+ defer func() {
+ if countOnlineDisks(onlineDisks) != len(onlineDisks) {
+ er.deleteAll(context.Background(), minioMetaTmpBucket, tmpPart)
+ }
+ }()
+
+ erasure, err := NewErasure(ctx, fi.Erasure.DataBlocks, fi.Erasure.ParityBlocks, fi.Erasure.BlockSize)
+ if err != nil {
+ return pi, toObjectErr(err, bucket, object)
+ }
+
+ // Fetch buffer for I/O, returns from the pool if not allocates a new one and returns.
+ var buffer []byte
+ switch size := data.Size(); {
+ case size == 0:
+ buffer = make([]byte, 1) // Allocate at least a byte to reach EOF
+ case size >= fi.Erasure.BlockSize || size == -1:
+ if int64(globalBytePoolCap.Load().Width()) < fi.Erasure.BlockSize {
+ buffer = make([]byte, fi.Erasure.BlockSize, 2*fi.Erasure.BlockSize)
+ } else {
+ buffer = globalBytePoolCap.Load().Get()
+ defer globalBytePoolCap.Load().Put(buffer)
+ }
+ case size < fi.Erasure.BlockSize:
+ // 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))
+ }
+
+ if len(buffer) > int(fi.Erasure.BlockSize) {
+ buffer = buffer[:fi.Erasure.BlockSize]
+ }
+ writers := make([]io.Writer, len(onlineDisks))
+ for i, disk := range onlineDisks {
+ if disk == nil {
+ continue
+ }
+ writers[i] = newBitrotWriter(disk, bucket, minioMetaTmpBucket, tmpPartPath, erasure.ShardFileSize(data.Size()), DefaultBitrotAlgorithm, erasure.ShardSize())
+ }
+
+ toEncode := io.Reader(data)
+ if data.Size() > bigFileThreshold {
+ // Add input readahead.
+ // We use 2 buffers, so we always have a full buffer of input.
+ pool := globalBytePoolCap.Load()
+ bufA := pool.Get()
+ bufB := pool.Get()
+ defer pool.Put(bufA)
+ defer pool.Put(bufB)
+ ra, err := readahead.NewReaderBuffer(data, [][]byte{bufA[:fi.Erasure.BlockSize], bufB[:fi.Erasure.BlockSize]})
+ if err == nil {
+ toEncode = ra
+ defer ra.Close()
+ }
+ }
+
+ n, err := erasure.Encode(ctx, toEncode, writers, buffer, writeQuorum)
+ closeErrs := closeBitrotWriters(writers)
+ if err != nil {
+ 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
+ // than specified in request header.
+ if n < data.Size() {
+ return pi, IncompleteBody{Bucket: bucket, Object: object}
+ }
+
+ for i := range writers {
+ if writers[i] == nil {
+ onlineDisks[i] = nil
+ }
+ }
+
+ // Rename temporary part file to its final location.
+ partPath := pathJoin(uploadIDPath, fi.DataDir, partSuffix)
+
+ md5hex := r.MD5CurrentHexString()
+ if opts.PreserveETag != "" {
+ md5hex = opts.PreserveETag
+ }
+
+ var index []byte
+ if opts.IndexCB != nil {
+ index = opts.IndexCB()
+ }
+
+ actualSize := data.ActualSize()
+ if actualSize < 0 {
+ _, encrypted := crypto.IsEncrypted(fi.Metadata)
+ compressed := fi.IsCompressed()
+ switch {
+ case compressed:
+ // ... nothing changes for compressed stream.
+ // if actualSize is -1 we have no known way to
+ // determine what is the actualSize.
+ case encrypted:
+ decSize, err := sio.DecryptedSize(uint64(n))
+ if err == nil {
+ actualSize = int64(decSize)
+ }
+ default:
+ actualSize = n
+ }
+ }
+
+ partInfo := ObjectPartInfo{
+ Number: partID,
+ ETag: md5hex,
+ Size: n,
+ ActualSize: actualSize,
+ ModTime: UTCNow(),
+ Index: index,
+ Checksums: r.ContentCRC(),
+ }
+
+ partFI, err := partInfo.MarshalMsg(nil)
+ if err != nil {
+ return pi, toObjectErr(err, minioMetaMultipartBucket, partPath)
+ }
+
+ // Serialize concurrent part uploads.
+ partIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID, strconv.Itoa(partID)))
+ plkctx, err := partIDLock.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ return PartInfo{}, err
+ }
+
+ ctx = plkctx.Context()
+ 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, uploadIDPath)
+ if err != nil {
+ if errors.Is(err, errUploadIDNotFound) {
+ return pi, toObjectErr(errUploadIDNotFound, bucket, object, uploadID)
+ }
+ if errors.Is(err, errFileNotFound) {
+ // An in-quorum errFileNotFound means that client stream
+ // prematurely closed and we do not find any xl.meta or
+ // part.1's - in such a scenario we must return as if client
+ // disconnected. This means that erasure.Encode() CreateFile()
+ // did not do anything.
+ return pi, IncompleteBody{Bucket: bucket, Object: object}
+ }
+
+ return pi, toObjectErr(err, minioMetaMultipartBucket, partPath)
+ }
+
+ // Return success.
+ return PartInfo{
+ PartNumber: partInfo.Number,
+ ETag: partInfo.ETag,
+ LastModified: partInfo.ModTime,
+ Size: partInfo.Size,
+ ActualSize: partInfo.ActualSize,
+ ChecksumCRC32: partInfo.Checksums["CRC32"],
+ ChecksumCRC32C: partInfo.Checksums["CRC32C"],
+ ChecksumSHA1: partInfo.Checksums["SHA1"],
+ ChecksumSHA256: partInfo.Checksums["SHA256"],
+ ChecksumCRC64NVME: partInfo.Checksums["CRC64NVME"],
+ }, nil
+}
+
+// GetMultipartInfo returns multipart metadata uploaded during newMultipartUpload, used
+// by callers to verify object states
+// - encrypted
+// - compressed
+// Does not contain currently uploaded parts by design.
+func (er erasureObjects) GetMultipartInfo(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) (MultipartInfo, error) {
+ if !opts.NoAuditLog {
+ auditObjectErasureSet(ctx, "GetMultipartInfo", object, &er)
+ }
+
+ result := MultipartInfo{
+ Bucket: bucket,
+ Object: object,
+ UploadID: uploadID,
+ }
+
+ fi, _, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, false)
+ if err != nil {
+ if errors.Is(err, errVolumeNotFound) {
+ return result, toObjectErr(err, bucket)
+ }
+ return result, toObjectErr(err, bucket, object, uploadID)
+ }
+
+ result.UserDefined = cloneMSS(fi.Metadata)
+ return result, nil
+}
+
+func (er erasureObjects) listParts(ctx context.Context, onlineDisks []StorageAPI, partPath string, readQuorum int) ([]int, error) {
+ g := errgroup.WithNErrs(len(onlineDisks))
+
+ objectParts := make([][]string, len(onlineDisks))
+ // List uploaded parts from drives.
+ for index := range onlineDisks {
+ g.Go(func() (err error) {
+ if onlineDisks[index] == nil {
+ return errDiskNotFound
+ }
+ objectParts[index], err = onlineDisks[index].ListDir(ctx, minioMetaMultipartBucket, minioMetaMultipartBucket, partPath, -1)
+ return err
+ }, index)
+ }
+
+ if err := reduceReadQuorumErrs(ctx, g.Wait(), objectOpIgnoredErrs, readQuorum); err != nil {
+ return nil, err
+ }
+
+ partQuorumMap := make(map[int]int)
+ for _, driveParts := range objectParts {
+ partsWithMetaCount := make(map[int]int, len(driveParts))
+ // part files can be either part.N or part.N.meta
+ for _, partPath := range driveParts {
+ var partNum int
+ if _, err := fmt.Sscanf(partPath, "part.%d", &partNum); err == nil {
+ partsWithMetaCount[partNum]++
+ continue
+ }
+ if _, err := fmt.Sscanf(partPath, "part.%d.meta", &partNum); err == nil {
+ partsWithMetaCount[partNum]++
+ }
+ }
+ // Include only part.N.meta files with corresponding part.N
+ for partNum, cnt := range partsWithMetaCount {
+ if cnt < 2 {
+ continue
+ }
+ partQuorumMap[partNum]++
+ }
+ }
+
+ var partNums []int
+ for partNum, count := range partQuorumMap {
+ if count < readQuorum {
+ continue
+ }
+ partNums = append(partNums, partNum)
+ }
+
+ sort.Ints(partNums)
+ return partNums, nil
+}
+
+// ListObjectParts - lists all previously uploaded parts for a given
+// object and uploadID. Takes additional input of part-number-marker
+// to indicate where the listing should begin from.
+//
+// Implements S3 compatible ListObjectParts API. The resulting
+// ListPartsInfo structure is marshaled directly into XML and
+// replied back to the client.
+func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker, maxParts int, opts ObjectOptions) (result ListPartsInfo, err error) {
+ if !opts.NoAuditLog {
+ auditObjectErasureSet(ctx, "ListObjectParts", object, &er)
+ }
+
+ fi, _, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, false)
+ if err != nil {
+ return result, toObjectErr(err, bucket, object, uploadID)
+ }
+
+ uploadIDPath := er.getUploadIDDir(bucket, object, uploadID)
+ if partNumberMarker < 0 {
+ partNumberMarker = 0
+ }
+
+ // Limit output to maxPartsList.
+ if maxParts > maxPartsList {
+ maxParts = maxPartsList
+ }
+
+ // Populate the result stub.
+ result.Bucket = bucket
+ result.Object = object
+ result.UploadID = uploadID
+ result.MaxParts = maxParts
+ result.PartNumberMarker = partNumberMarker
+ result.UserDefined = cloneMSS(fi.Metadata)
+ result.ChecksumAlgorithm = fi.Metadata[hash.MinIOMultipartChecksum]
+ result.ChecksumType = fi.Metadata[hash.MinIOMultipartChecksumType]
+
+ if maxParts == 0 {
+ return result, nil
+ }
+
+ onlineDisks := er.getDisks()
+ readQuorum := fi.ReadQuorum(er.defaultRQuorum())
+ // Read Part info for all parts
+ partPath := pathJoin(uploadIDPath, fi.DataDir) + SlashSeparator
+
+ // List parts in quorum
+ partNums, err := er.listParts(ctx, onlineDisks, partPath, readQuorum)
+ if err != nil {
+ // This means that fi.DataDir, is not yet populated so we
+ // return an empty response.
+ if errors.Is(err, errFileNotFound) {
+ return result, nil
+ }
+ return result, toObjectErr(err, bucket, object, uploadID)
+ }
+
+ if len(partNums) == 0 {
+ return result, nil
+ }
+
+ start := objectPartIndexNums(partNums, partNumberMarker)
+ if partNumberMarker > 0 && start == -1 {
+ // Marker not present among what is present on the
+ // server, we return an empty list.
+ return result, nil
+ }
+
+ if partNumberMarker > 0 && start != -1 {
+ if start+1 >= len(partNums) {
+ // Marker indicates that we are the end
+ // of the list, so we simply return empty
+ return result, nil
+ }
+
+ partNums = partNums[start+1:]
+ }
+
+ result.Parts = make([]PartInfo, 0, len(partNums))
+ partMetaPaths := make([]string, len(partNums))
+ for i, part := range partNums {
+ partMetaPaths[i] = pathJoin(partPath, fmt.Sprintf("part.%d.meta", part))
+ }
+
+ // Read parts in quorum
+ objParts, err := readParts(ctx, onlineDisks, minioMetaMultipartBucket, partMetaPaths,
+ partNums, readQuorum)
+ if err != nil {
+ return result, toObjectErr(err, bucket, object, uploadID)
+ }
+
+ count := maxParts
+ for _, objPart := range objParts {
+ result.Parts = append(result.Parts, PartInfo{
+ PartNumber: objPart.Number,
+ LastModified: objPart.ModTime,
+ ETag: objPart.ETag,
+ Size: objPart.Size,
+ ActualSize: objPart.ActualSize,
+ ChecksumCRC32: objPart.Checksums["CRC32"],
+ ChecksumCRC32C: objPart.Checksums["CRC32C"],
+ ChecksumSHA1: objPart.Checksums["SHA1"],
+ ChecksumSHA256: objPart.Checksums["SHA256"],
+ ChecksumCRC64NVME: objPart.Checksums["CRC64NVME"],
+ })
+ count--
+ if count == 0 {
+ break
+ }
+ }
+
+ if len(objParts) > len(result.Parts) {
+ result.IsTruncated = true
+ // Make sure to fill next part number marker if IsTruncated is true for subsequent listing.
+ result.NextPartNumberMarker = result.Parts[len(result.Parts)-1].PartNumber
+ }
+
+ return result, nil
+}
+
+func readParts(ctx context.Context, disks []StorageAPI, bucket string, partMetaPaths []string, partNumbers []int, readQuorum int) ([]ObjectPartInfo, error) {
+ g := errgroup.WithNErrs(len(disks))
+
+ objectPartInfos := make([][]*ObjectPartInfo, len(disks))
+ // Rename file on all underlying storage disks.
+ for index := range disks {
+ g.Go(func() (err error) {
+ if disks[index] == nil {
+ return errDiskNotFound
+ }
+ objectPartInfos[index], err = disks[index].ReadParts(ctx, bucket, partMetaPaths...)
+ return err
+ }, index)
+ }
+
+ if err := reduceReadQuorumErrs(ctx, g.Wait(), objectOpIgnoredErrs, readQuorum); err != nil {
+ return nil, err
+ }
+
+ partInfosInQuorum := make([]ObjectPartInfo, len(partMetaPaths))
+ for pidx := range partMetaPaths {
+ // partMetaQuorumMap uses
+ // - path/to/part.N as key to collate errors from failed drives.
+ // - part ETag to collate part metadata
+ partMetaQuorumMap := make(map[string]int, len(partNumbers))
+ var pinfos []*ObjectPartInfo
+ for idx := range disks {
+ if len(objectPartInfos[idx]) != len(partMetaPaths) {
+ partMetaQuorumMap[partMetaPaths[pidx]]++
+ continue
+ }
+
+ pinfo := objectPartInfos[idx][pidx]
+ if pinfo != nil && pinfo.ETag != "" {
+ pinfos = append(pinfos, pinfo)
+ partMetaQuorumMap[pinfo.ETag]++
+ continue
+ }
+ partMetaQuorumMap[partMetaPaths[pidx]]++
+ }
+
+ var maxQuorum int
+ var maxETag string
+ var maxPartMeta string
+ for etag, quorum := range partMetaQuorumMap {
+ if maxQuorum < quorum {
+ maxQuorum = quorum
+ maxETag = etag
+ maxPartMeta = etag
+ }
+ }
+ // found is a representative ObjectPartInfo which either has the maximally occurring ETag or an error.
+ var found *ObjectPartInfo
+ for _, pinfo := range pinfos {
+ if pinfo == nil {
+ continue
+ }
+ if maxETag != "" && pinfo.ETag == maxETag {
+ found = pinfo
+ break
+ }
+ if pinfo.ETag == "" && maxPartMeta != "" && path.Base(maxPartMeta) == fmt.Sprintf("part.%d.meta", pinfo.Number) {
+ found = pinfo
+ break
+ }
+ }
+
+ if found != nil && found.ETag != "" && partMetaQuorumMap[maxETag] >= readQuorum {
+ partInfosInQuorum[pidx] = *found
+ continue
+ }
+ partInfosInQuorum[pidx] = ObjectPartInfo{
+ Number: partNumbers[pidx],
+ Error: InvalidPart{
+ PartNumber: partNumbers[pidx],
+ }.Error(),
+ }
+ }
+ return partInfosInQuorum, nil
+}
+
+func objPartToPartErr(part ObjectPartInfo) error {
+ if strings.Contains(part.Error, "file not found") {
+ return InvalidPart{PartNumber: part.Number}
+ }
+ if strings.Contains(part.Error, "Specified part could not be found") {
+ return InvalidPart{PartNumber: part.Number}
+ }
+ if strings.Contains(part.Error, errErasureReadQuorum.Error()) {
+ return errErasureReadQuorum
+ }
+ return errors.New(part.Error)
+}
+
+// CompleteMultipartUpload - completes an ongoing multipart
+// transaction after receiving all the parts indicated by the client.
+// Returns an md5sum calculated by concatenating all the individual
+// md5sums of all the parts.
+//
+// Implements S3 compatible Complete multipart API.
+func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket string, object string, uploadID string, parts []CompletePart, opts ObjectOptions) (oi ObjectInfo, err error) {
+ if !opts.NoAuditLog {
+ auditObjectErasureSet(ctx, "CompleteMultipartUpload", object, &er)
+ }
+
+ if opts.CheckPrecondFn != nil {
+ if !opts.NoLock {
+ ns := er.NewNSLock(bucket, object)
+ lkctx, err := ns.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ return ObjectInfo{}, err
+ }
+ ctx = lkctx.Context()
+ defer ns.Unlock(lkctx)
+ opts.NoLock = true
+ }
+
+ obj, err := er.getObjectInfo(ctx, bucket, object, opts)
+ if err == nil && opts.CheckPrecondFn(obj) {
+ return ObjectInfo{}, PreConditionFailed{}
+ }
+ if err != nil && !isErrVersionNotFound(err) && !isErrObjectNotFound(err) && !isErrReadQuorum(err) {
+ return ObjectInfo{}, err
+ }
+
+ // if object doesn't exist return error for If-Match conditional requests
+ // If-None-Match should be allowed to proceed for non-existent objects
+ if err != nil && opts.HasIfMatch && (isErrObjectNotFound(err) || isErrVersionNotFound(err)) {
+ return ObjectInfo{}, err
+ }
+ }
+
+ fi, partsMetadata, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, true)
+ if err != nil {
+ if errors.Is(err, errVolumeNotFound) {
+ return oi, toObjectErr(err, bucket)
+ }
+ return oi, toObjectErr(err, bucket, object, uploadID)
+ }
+
+ uploadIDPath := er.getUploadIDDir(bucket, object, uploadID)
+ onlineDisks := er.getDisks()
+ writeQuorum := fi.WriteQuorum(er.defaultWQuorum())
+ readQuorum := fi.ReadQuorum(er.defaultRQuorum())
+
+ // Read Part info for all parts
+ partPath := pathJoin(uploadIDPath, fi.DataDir) + SlashSeparator
+ partMetaPaths := make([]string, len(parts))
+ partNumbers := make([]int, len(parts))
+ for idx, part := range parts {
+ partMetaPaths[idx] = pathJoin(partPath, fmt.Sprintf("part.%d.meta", part.PartNumber))
+ partNumbers[idx] = part.PartNumber
+ }
+
+ partInfoFiles, err := readParts(ctx, onlineDisks, minioMetaMultipartBucket, partMetaPaths, partNumbers, readQuorum)
+ if err != nil {
+ return oi, err
+ }
+
+ if len(partInfoFiles) != len(parts) {
+ // Should only happen through internal error
+ err := fmt.Errorf("unexpected part result count: %d, want %d", len(partInfoFiles), len(parts))
+ bugLogIf(ctx, err)
+ return oi, toObjectErr(err, bucket, object)
+ }
+
+ // Checksum type set when upload started.
+ var checksumType hash.ChecksumType
+ if cs := fi.Metadata[hash.MinIOMultipartChecksum]; cs != "" {
+ checksumType = hash.NewChecksumType(cs, fi.Metadata[hash.MinIOMultipartChecksumType])
+ if opts.WantChecksum != nil && !opts.WantChecksum.Type.Is(checksumType) {
+ return oi, InvalidArgument{
+ Bucket: bucket,
+ Object: fi.Name,
+ Err: fmt.Errorf("checksum type mismatch. got %q (%s) expected %q (%s)", checksumType.String(), checksumType.ObjType(), opts.WantChecksum.Type.String(), opts.WantChecksum.Type.ObjType()),
+ }
+ }
+ checksumType |= hash.ChecksumMultipart | hash.ChecksumIncludesMultipart
+ }
+
+ var checksumCombined []byte
+
+ // However, in case of encryption, the persisted part ETags don't match
+ // what we have sent to the client during PutObjectPart. The reason is
+ // that ETags are encrypted. Hence, the client will send a list of complete
+ // part ETags of which may not match the ETag of any part. For example
+ // ETag (client): 30902184f4e62dd8f98f0aaff810c626
+ // ETag (server-internal): 20000f00ce5dc16e3f3b124f586ae1d88e9caa1c598415c2759bbb50e84a59f630902184f4e62dd8f98f0aaff810c626
+ //
+ // Therefore, we adjust all ETags sent by the client to match what is stored
+ // on the backend.
+ kind, _ := crypto.IsEncrypted(fi.Metadata)
+
+ var objectEncryptionKey []byte
+ switch kind {
+ case crypto.SSEC:
+ if checksumType.IsSet() {
+ if opts.EncryptFn == nil {
+ return oi, crypto.ErrMissingCustomerKey
+ }
+ baseKey := opts.EncryptFn("", nil)
+ if len(baseKey) != 32 {
+ return oi, crypto.ErrInvalidCustomerKey
+ }
+ objectEncryptionKey, err = decryptObjectMeta(baseKey, bucket, object, fi.Metadata)
+ if err != nil {
+ return oi, err
+ }
+ }
+ case crypto.S3, crypto.S3KMS:
+ objectEncryptionKey, err = decryptObjectMeta(nil, bucket, object, fi.Metadata)
+ if err != nil {
+ return oi, err
+ }
+ }
+ if len(objectEncryptionKey) == 32 {
+ var key crypto.ObjectKey
+ copy(key[:], objectEncryptionKey)
+ opts.EncryptFn = metadataEncrypter(key)
+ }
+
+ for idx, part := range partInfoFiles {
+ if part.Error != "" {
+ err = objPartToPartErr(part)
+ bugLogIf(ctx, err)
+ return oi, err
+ }
+
+ if parts[idx].PartNumber != part.Number {
+ internalLogIf(ctx, fmt.Errorf("part.%d.meta has incorrect corresponding part number: expected %d, got %d", parts[idx].PartNumber, parts[idx].PartNumber, part.Number))
+ return oi, InvalidPart{
+ PartNumber: part.Number,
+ }
+ }
+
+ // Add the current part.
+ fi.AddObjectPart(part.Number, part.ETag, part.Size, part.ActualSize, part.ModTime, part.Index, part.Checksums)
+ }
+
+ // Calculate full object size.
+ var objectSize int64
+
+ // Calculate consolidated actual size.
+ var objectActualSize int64
+
+ // Order online disks in accordance with distribution order.
+ // Order parts metadata in accordance with distribution order.
+ onlineDisks, partsMetadata = shuffleDisksAndPartsMetadataByIndex(onlineDisks, partsMetadata, fi)
+
+ // Save current erasure metadata for validation.
+ currentFI := fi
+
+ // Allocate parts similar to incoming slice.
+ fi.Parts = make([]ObjectPartInfo, len(parts))
+
+ var checksum hash.Checksum
+ checksum.Type = checksumType
+
+ // Validate each part and then commit to disk.
+ for i, part := range parts {
+ partIdx := objectPartIndex(currentFI.Parts, part.PartNumber)
+ // All parts should have same part number.
+ if partIdx == -1 {
+ invp := InvalidPart{
+ PartNumber: part.PartNumber,
+ GotETag: part.ETag,
+ }
+ return oi, invp
+ }
+ expPart := currentFI.Parts[partIdx]
+
+ // ensure that part ETag is canonicalized to strip off extraneous quotes
+ part.ETag = canonicalizeETag(part.ETag)
+ expETag := tryDecryptETag(objectEncryptionKey, expPart.ETag, kind == crypto.S3)
+ if expETag != part.ETag {
+ invp := InvalidPart{
+ PartNumber: part.PartNumber,
+ ExpETag: expETag,
+ GotETag: part.ETag,
+ }
+ return oi, invp
+ }
+
+ if checksumType.IsSet() {
+ crc := expPart.Checksums[checksumType.String()]
+ if crc == "" {
+ return oi, InvalidPart{
+ PartNumber: part.PartNumber,
+ }
+ }
+ wantCS := map[string]string{
+ hash.ChecksumCRC32.String(): part.ChecksumCRC32,
+ hash.ChecksumCRC32C.String(): part.ChecksumCRC32C,
+ hash.ChecksumSHA1.String(): part.ChecksumSHA1,
+ hash.ChecksumSHA256.String(): part.ChecksumSHA256,
+ hash.ChecksumCRC64NVME.String(): part.ChecksumCRC64NVME,
+ }
+ if wantCS[checksumType.String()] != crc {
+ return oi, InvalidPart{
+ PartNumber: part.PartNumber,
+ ExpETag: wantCS[checksumType.String()],
+ GotETag: crc,
+ }
+ }
+ cs := hash.NewChecksumString(checksumType.String(), crc)
+ if !cs.Valid() {
+ return oi, InvalidPart{
+ PartNumber: part.PartNumber,
+ }
+ }
+ if checksumType.FullObjectRequested() {
+ if err := checksum.AddPart(*cs, expPart.ActualSize); err != nil {
+ return oi, InvalidPart{
+ PartNumber: part.PartNumber,
+ ExpETag: "",
+ GotETag: err.Error(),
+ }
+ }
+ }
+ checksumCombined = append(checksumCombined, cs.Raw...)
+ }
+
+ // All parts except the last part has to be at least 5MB.
+ if (i < len(parts)-1) && !isMinAllowedPartSize(currentFI.Parts[partIdx].ActualSize) {
+ return oi, PartTooSmall{
+ PartNumber: part.PartNumber,
+ PartSize: expPart.ActualSize,
+ PartETag: part.ETag,
+ }
+ }
+
+ // Save for total object size.
+ objectSize += expPart.Size
+
+ // Save the consolidated actual size.
+ objectActualSize += expPart.ActualSize
+
+ // Add incoming parts.
+ fi.Parts[i] = ObjectPartInfo{
+ Number: part.PartNumber,
+ Size: expPart.Size,
+ ActualSize: expPart.ActualSize,
+ ModTime: expPart.ModTime,
+ Index: expPart.Index,
+ Checksums: nil, // Not transferred since we do not need it.
+ }
+ }
+
+ if opts.WantChecksum != nil {
+ if checksumType.FullObjectRequested() {
+ if opts.WantChecksum.Encoded != checksum.Encoded {
+ 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
+ }
+ }
+ }
+
+ // Accept encrypted checksum from incoming request.
+ if opts.UserDefined[ReplicationSsecChecksumHeader] != "" {
+ if v, err := base64.StdEncoding.DecodeString(opts.UserDefined[ReplicationSsecChecksumHeader]); err == nil {
+ fi.Checksum = v
+ }
+ delete(opts.UserDefined, ReplicationSsecChecksumHeader)
+ }
+
+ if checksumType.IsSet() {
+ checksumType |= hash.ChecksumMultipart | hash.ChecksumIncludesMultipart
+ checksum.Type = checksumType
+ if !checksumType.FullObjectRequested() {
+ checksum = *hash.NewChecksumFromData(checksumType, checksumCombined)
+ }
+ fi.Checksum = checksum.AppendTo(nil, checksumCombined)
+ if opts.EncryptFn != nil {
+ fi.Checksum = opts.EncryptFn("object-checksum", fi.Checksum)
+ }
+ }
+ // Remove superfluous internal headers.
+ delete(fi.Metadata, hash.MinIOMultipartChecksum)
+ delete(fi.Metadata, hash.MinIOMultipartChecksumType)
+
+ // Save the final object size and modtime.
+ fi.Size = objectSize
+ fi.ModTime = opts.MTime
+ if opts.MTime.IsZero() {
+ fi.ModTime = UTCNow()
+ }
+
+ // Save successfully calculated md5sum.
+ // for replica, newMultipartUpload would have already sent the replication ETag
+ if fi.Metadata["etag"] == "" {
+ if opts.UserDefined["etag"] != "" {
+ fi.Metadata["etag"] = opts.UserDefined["etag"]
+ } else { // fallback if not already calculated in handler.
+ fi.Metadata["etag"] = getCompleteMultipartMD5(parts)
+ }
+ }
+
+ // Save the consolidated actual size.
+ if opts.ReplicationRequest {
+ if v := opts.UserDefined[ReservedMetadataPrefix+"Actual-Object-Size"]; v != "" {
+ fi.Metadata[ReservedMetadataPrefix+"actual-size"] = v
+ }
+ } else {
+ fi.Metadata[ReservedMetadataPrefix+"actual-size"] = strconv.FormatInt(objectActualSize, 10)
+ }
+
+ if opts.DataMovement {
+ fi.SetDataMov()
+ }
+
+ // Update all erasure metadata, make sure to not modify fields like
+ // checksum which are different on each disks.
+ for index := range partsMetadata {
+ if partsMetadata[index].IsValid() {
+ partsMetadata[index].Size = fi.Size
+ partsMetadata[index].ModTime = fi.ModTime
+ partsMetadata[index].Metadata = fi.Metadata
+ partsMetadata[index].Parts = fi.Parts
+ partsMetadata[index].Checksum = fi.Checksum
+ partsMetadata[index].Versioned = opts.Versioned || opts.VersionSuspended
+ }
+ }
+
+ paths := make([]string, 0, len(currentFI.Parts))
+ // Remove parts that weren't present in CompleteMultipartUpload request.
+ for _, curpart := range currentFI.Parts {
+ paths = append(paths, pathJoin(uploadIDPath, currentFI.DataDir, fmt.Sprintf("part.%d.meta", curpart.Number)))
+
+ if objectPartIndex(fi.Parts, curpart.Number) == -1 {
+ // Delete the missing part files. e.g,
+ // Request 1: NewMultipart
+ // Request 2: PutObjectPart 1
+ // Request 3: PutObjectPart 2
+ // Request 4: CompleteMultipartUpload --part 2
+ // N.B. 1st part is not present. This part should be removed from the storage.
+ paths = append(paths, pathJoin(uploadIDPath, currentFI.DataDir, fmt.Sprintf("part.%d", curpart.Number)))
+ }
+ }
+
+ if !opts.NoLock {
+ lk := er.NewNSLock(bucket, object)
+ lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ return ObjectInfo{}, err
+ }
+ ctx = lkctx.Context()
+ defer lk.Unlock(lkctx)
+ }
+
+ er.cleanupMultipartPath(ctx, paths...) // cleanup all part.N.meta, and skipped part.N's before final rename().
+
+ defer func() {
+ if err == nil {
+ er.deleteAll(context.Background(), minioMetaMultipartBucket, uploadIDPath)
+ }
+ }()
+
+ // Rename the multipart object to final location.
+ onlineDisks, versions, oldDataDir, err := renameData(ctx, onlineDisks, minioMetaMultipartBucket, uploadIDPath,
+ partsMetadata, bucket, object, writeQuorum)
+ if err != nil {
+ return oi, toObjectErr(err, bucket, object, uploadID)
+ }
+
+ if err = er.commitRenameDataDir(ctx, bucket, object, oldDataDir, onlineDisks, writeQuorum); err != nil {
+ return ObjectInfo{}, toObjectErr(err, bucket, object, uploadID)
+ }
+
+ if !opts.Speedtest && len(versions) > 0 {
+ globalMRFState.addPartialOp(PartialOperation{
+ Bucket: bucket,
+ Object: object,
+ Queued: time.Now(),
+ Versions: versions,
+ SetIndex: er.setIndex,
+ PoolIndex: er.poolIndex,
+ })
+ }
+
+ if !opts.Speedtest && len(versions) == 0 {
+ // Check if there is any offline disk and add it to the MRF list
+ for _, disk := range onlineDisks {
+ if disk != nil && disk.IsOnline() {
+ continue
+ }
+ er.addPartial(bucket, object, fi.VersionID)
+ break
+ }
+ }
+
+ for i := range len(onlineDisks) {
+ if onlineDisks[i] != nil && onlineDisks[i].IsOnline() {
+ // Object info is the same in all disks, so we can pick
+ // the first meta from online disk
+ fi = partsMetadata[i]
+ break
+ }
+ }
+
+ // we are adding a new version to this object under the namespace lock, so this is the latest version.
+ fi.IsLatest = true
+
+ // Success, return object info.
+ return fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended), nil
+}
+
+// AbortMultipartUpload - aborts an ongoing multipart operation
+// signified by the input uploadID. This is an atomic operation
+// doesn't require clients to initiate multiple such requests.
+//
+// All parts are purged from all disks and reference to the uploadID
+// would be removed from the system, rollback is not possible on this
+// operation.
+func (er erasureObjects) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) (err error) {
+ if !opts.NoAuditLog {
+ auditObjectErasureSet(ctx, "AbortMultipartUpload", object, &er)
+ }
+
+ // Cleanup all uploaded parts.
+ defer er.deleteAll(ctx, minioMetaMultipartBucket, er.getUploadIDDir(bucket, object, uploadID))
+
+ // Validates if upload ID exists.
+ _, _, err = er.checkUploadIDExists(ctx, bucket, object, uploadID, false)
+ return toObjectErr(err, bucket, object, uploadID)
+}
diff --git a/cmd/erasure-object.go b/cmd/erasure-object.go
new file mode 100644
index 00000000..33fd3dc1
--- /dev/null
+++ b/cmd/erasure-object.go
@@ -0,0 +1,2599 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "io"
+ "maps"
+ "net/http"
+ "path"
+ "runtime"
+ "slices"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/klauspost/readahead"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio-go/v7/pkg/tags"
+ "github.com/minio/minio/internal/bucket/lifecycle"
+ "github.com/minio/minio/internal/bucket/object/lock"
+ "github.com/minio/minio/internal/bucket/replication"
+ "github.com/minio/minio/internal/config/storageclass"
+ "github.com/minio/minio/internal/crypto"
+ "github.com/minio/minio/internal/event"
+ "github.com/minio/minio/internal/grid"
+ "github.com/minio/minio/internal/hash"
+ xhttp "github.com/minio/minio/internal/http"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/mimedb"
+ "github.com/minio/pkg/v3/sync/errgroup"
+ "github.com/minio/sio"
+)
+
+// list all errors which can be ignored in object operations.
+var objectOpIgnoredErrs = append(baseIgnoredErrs, errDiskAccessDenied, errUnformattedDisk, errDiskOngoingReq)
+
+// Object Operations
+
+func countOnlineDisks(onlineDisks []StorageAPI) (online int) {
+ for _, onlineDisk := range onlineDisks {
+ if onlineDisk != nil && onlineDisk.IsOnline() {
+ online++
+ }
+ }
+ return online
+}
+
+// CopyObject - copy object source object to destination object.
+// if source object and destination object are same we only
+// update metadata.
+func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (oi ObjectInfo, err error) {
+ if !dstOpts.NoAuditLog {
+ auditObjectErasureSet(ctx, "CopyObject", dstObject, &er)
+ }
+
+ // This call shouldn't be used for anything other than metadata updates or adding self referential versions.
+ if !srcInfo.metadataOnly {
+ return oi, NotImplemented{}
+ }
+
+ if !dstOpts.NoLock {
+ lk := er.NewNSLock(dstBucket, dstObject)
+ lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ return oi, err
+ }
+ ctx = lkctx.Context()
+ defer lk.Unlock(lkctx)
+ }
+ // Read metadata associated with the object from all disks.
+ storageDisks := er.getDisks()
+
+ var metaArr []FileInfo
+ var errs []error
+
+ // Read metadata associated with the object from all disks.
+ if srcOpts.VersionID != "" {
+ metaArr, errs = readAllFileInfo(ctx, storageDisks, "", srcBucket, srcObject, srcOpts.VersionID, true, false)
+ } else {
+ metaArr, errs = readAllXL(ctx, storageDisks, srcBucket, srcObject, true, false)
+ }
+
+ readQuorum, writeQuorum, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
+ if err != nil {
+ if shouldCheckForDangling(err, errs, srcBucket) {
+ _, derr := er.deleteIfDangling(context.Background(), srcBucket, srcObject, metaArr, errs, nil, srcOpts)
+ if derr == nil {
+ if srcOpts.VersionID != "" {
+ err = errFileVersionNotFound
+ } else {
+ err = errFileNotFound
+ }
+ }
+ }
+ return ObjectInfo{}, toObjectErr(err, srcBucket, srcObject)
+ }
+
+ // List all online disks.
+ onlineDisks, modTime, etag := listOnlineDisks(storageDisks, metaArr, errs, readQuorum)
+
+ // Pick latest valid metadata.
+ fi, err := pickValidFileInfo(ctx, metaArr, modTime, etag, readQuorum)
+ if err != nil {
+ return oi, toObjectErr(err, srcBucket, srcObject)
+ }
+ if fi.Deleted {
+ if srcOpts.VersionID == "" {
+ return oi, toObjectErr(errFileNotFound, srcBucket, srcObject)
+ }
+ return fi.ToObjectInfo(srcBucket, srcObject, srcOpts.Versioned || srcOpts.VersionSuspended), toObjectErr(errMethodNotAllowed, srcBucket, srcObject)
+ }
+
+ filterOnlineDisksInplace(fi, metaArr, onlineDisks)
+
+ versionID := srcInfo.VersionID
+ if srcInfo.versionOnly {
+ versionID = dstOpts.VersionID
+ // preserve destination versionId if specified.
+ if versionID == "" {
+ versionID = mustGetUUID()
+ fi.IsLatest = true // we are creating a new version so this is latest.
+ }
+ }
+
+ modTime = UTCNow() // We only preserve modTime if dstOpts.MTime is true.
+ // in all other cases mtime is latest.
+
+ fi.VersionID = versionID // set any new versionID we might have created
+ fi.ModTime = modTime // set modTime for the new versionID
+ if !dstOpts.MTime.IsZero() {
+ modTime = dstOpts.MTime
+ fi.ModTime = dstOpts.MTime
+ }
+ // check inline before overwriting metadata.
+ inlineData := fi.InlineData()
+
+ fi.Metadata = srcInfo.UserDefined
+ srcInfo.UserDefined["etag"] = srcInfo.ETag
+
+ freeVersionID := fi.TierFreeVersionID()
+ freeVersionMarker := fi.TierFreeVersion()
+
+ // Update `xl.meta` content on each disks.
+ for index := range metaArr {
+ if metaArr[index].IsValid() {
+ metaArr[index].ModTime = modTime
+ metaArr[index].VersionID = versionID
+ if !metaArr[index].InlineData() {
+ // If the data is not inlined, we may end up incorrectly
+ // inlining the data here, that leads to an inconsistent
+ // situation where some objects are were not inlined
+ // were now inlined, make sure to `nil` the Data such
+ // that xl.meta is written as expected.
+ metaArr[index].Data = nil
+ }
+ metaArr[index].Metadata = srcInfo.UserDefined
+ // Preserve existing values
+ if inlineData {
+ metaArr[index].SetInlineData()
+ }
+ if freeVersionID != "" {
+ metaArr[index].SetTierFreeVersionID(freeVersionID)
+ }
+ if freeVersionMarker {
+ metaArr[index].SetTierFreeVersion()
+ }
+ }
+ }
+
+ // Write unique `xl.meta` for each disk.
+ if _, err = writeUniqueFileInfo(ctx, onlineDisks, "", srcBucket, srcObject, metaArr, writeQuorum); err != nil {
+ return oi, toObjectErr(err, srcBucket, srcObject)
+ }
+
+ return fi.ToObjectInfo(srcBucket, srcObject, srcOpts.Versioned || srcOpts.VersionSuspended), nil
+}
+
+// GetObjectNInfo - returns object info and an object
+// Read(Closer). When err != nil, the returned reader is always nil.
+func (er erasureObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions) (gr *GetObjectReader, err error) {
+ if !opts.NoAuditLog {
+ auditObjectErasureSet(ctx, "GetObject", object, &er)
+ }
+
+ var unlockOnDefer bool
+ nsUnlocker := func() {}
+ defer func() {
+ if unlockOnDefer {
+ nsUnlocker()
+ }
+ }()
+
+ // Acquire lock
+ if !opts.NoLock {
+ lock := er.NewNSLock(bucket, object)
+ lkctx, err := lock.GetRLock(ctx, globalOperationTimeout)
+ if err != nil {
+ return nil, err
+ }
+ ctx = lkctx.Context()
+
+ // Release lock when the metadata is verified, and reader
+ // is ready to be read.
+ //
+ // This is possible to be lock free because
+ // - xl.meta for inlined objects has already read the data
+ // into memory, any mutation on xl.meta subsequently is
+ // inconsequential to the overall read operation.
+ // - xl.meta metadata is still verified for quorum under lock()
+ // however writing the response doesn't need to serialize
+ // concurrent writers
+ unlockOnDefer = true
+ nsUnlocker = func() { lock.RUnlock(lkctx) }
+ }
+
+ fi, metaArr, onlineDisks, err := er.getObjectFileInfo(ctx, bucket, object, opts, true)
+ if err != nil {
+ return nil, toObjectErr(err, bucket, object)
+ }
+
+ objInfo := fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)
+ if objInfo.DeleteMarker {
+ if opts.VersionID == "" {
+ return &GetObjectReader{
+ ObjInfo: objInfo,
+ }, toObjectErr(errFileNotFound, bucket, object)
+ }
+ // Make sure to return object info to provide extra information.
+ return &GetObjectReader{
+ ObjInfo: objInfo,
+ }, toObjectErr(errMethodNotAllowed, bucket, object)
+ }
+
+ // Set NoDecryption for SSE-C objects and if replication request
+ if crypto.SSEC.IsEncrypted(objInfo.UserDefined) && opts.ReplicationRequest {
+ opts.NoDecryption = true
+ }
+
+ if objInfo.Size == 0 {
+ if _, _, err := rs.GetOffsetLength(objInfo.Size); err != nil {
+ // Make sure to return object info to provide extra information.
+ return &GetObjectReader{
+ ObjInfo: objInfo,
+ }, err
+ }
+
+ // Zero byte objects don't even need to further initialize pipes etc.
+ return NewGetObjectReaderFromReader(bytes.NewReader(nil), objInfo, opts)
+ }
+
+ if objInfo.IsRemote() {
+ gr, err := getTransitionedObjectReader(ctx, bucket, object, rs, h, objInfo, opts)
+ if err != nil {
+ return nil, err
+ }
+ unlockOnDefer = false
+ return gr.WithCleanupFuncs(nsUnlocker), nil
+ }
+
+ fn, off, length, err := NewGetObjectReader(rs, objInfo, opts, h)
+ if err != nil {
+ return nil, err
+ }
+
+ if unlockOnDefer {
+ unlockOnDefer = fi.InlineData() || len(fi.Data) > 0
+ }
+
+ pr, pw := xioutil.WaitPipe()
+ go func() {
+ pw.CloseWithError(er.getObjectWithFileInfo(ctx, bucket, object, off, length, pw, fi, metaArr, onlineDisks))
+ }()
+
+ // Cleanup function to cause the go routine above to exit, in
+ // case of incomplete read.
+ pipeCloser := func() {
+ pr.CloseWithError(nil)
+ }
+
+ if !unlockOnDefer {
+ return fn(pr, h, pipeCloser, nsUnlocker)
+ }
+
+ return fn(pr, h, pipeCloser)
+}
+
+func (er erasureObjects) getObjectWithFileInfo(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, fi FileInfo, metaArr []FileInfo, onlineDisks []StorageAPI) error {
+ // Reorder online disks based on erasure distribution order.
+ // Reorder parts metadata based on erasure distribution order.
+ onlineDisks, metaArr = shuffleDisksAndPartsMetadataByIndex(onlineDisks, metaArr, fi)
+
+ // For negative length read everything.
+ if length < 0 {
+ length = fi.Size - startOffset
+ }
+
+ // Reply back invalid range if the input offset and length fall out of range.
+ if startOffset > fi.Size || startOffset+length > fi.Size {
+ return InvalidRange{startOffset, length, fi.Size}
+ }
+
+ // Get start part index and offset.
+ partIndex, partOffset, err := fi.ObjectToPartOffset(ctx, startOffset)
+ if err != nil {
+ return InvalidRange{startOffset, length, fi.Size}
+ }
+
+ // Calculate endOffset according to length
+ endOffset := startOffset
+ if length > 0 {
+ endOffset += length - 1
+ }
+
+ // Get last part index to read given length.
+ lastPartIndex, _, err := fi.ObjectToPartOffset(ctx, endOffset)
+ if err != nil {
+ return InvalidRange{startOffset, length, fi.Size}
+ }
+
+ var totalBytesRead int64
+ erasure, err := NewErasure(ctx, fi.Erasure.DataBlocks, fi.Erasure.ParityBlocks, fi.Erasure.BlockSize)
+ if err != nil {
+ return toObjectErr(err, bucket, object)
+ }
+
+ var healOnce sync.Once
+
+ for ; partIndex <= lastPartIndex; partIndex++ {
+ if length == totalBytesRead {
+ break
+ }
+
+ partNumber := fi.Parts[partIndex].Number
+
+ // Save the current part name and size.
+ partSize := fi.Parts[partIndex].Size
+
+ partLength := partSize - partOffset
+ // partLength should be adjusted so that we don't write more data than what was requested.
+ if partLength > (length - totalBytesRead) {
+ partLength = length - totalBytesRead
+ }
+
+ tillOffset := erasure.ShardFileOffset(partOffset, partLength, partSize)
+ // Get the checksums of the current part.
+ readers := make([]io.ReaderAt, len(onlineDisks))
+ prefer := make([]bool, len(onlineDisks))
+ for index, disk := range onlineDisks {
+ if disk == OfflineDisk {
+ continue
+ }
+ if !metaArr[index].IsValid() {
+ continue
+ }
+ if !metaArr[index].Erasure.Equal(fi.Erasure) {
+ continue
+ }
+ checksumInfo := metaArr[index].Erasure.GetChecksumInfo(partNumber)
+ partPath := pathJoin(object, metaArr[index].DataDir, fmt.Sprintf("part.%d", partNumber))
+ readers[index] = newBitrotReader(disk, metaArr[index].Data, bucket, partPath, tillOffset,
+ checksumInfo.Algorithm, checksumInfo.Hash, erasure.ShardSize())
+
+ // Prefer local disks
+ prefer[index] = disk.Hostname() == ""
+ }
+
+ written, err := erasure.Decode(ctx, writer, readers, partOffset, partLength, partSize, prefer)
+ // Note: we should not be defer'ing the following closeBitrotReaders() call as
+ // we are inside a for loop i.e if we use defer, we would accumulate a lot of open files by the time
+ // we return from this function.
+ closeBitrotReaders(readers)
+ if err != nil {
+ // If we have successfully written all the content that was asked
+ // by the client, but we still see an error - this would mean
+ // that we have some parts or data blocks missing or corrupted
+ // - attempt a heal to successfully heal them for future calls.
+ if written == partLength {
+ if errors.Is(err, errFileNotFound) || errors.Is(err, errFileCorrupt) {
+ healOnce.Do(func() {
+ globalMRFState.addPartialOp(PartialOperation{
+ Bucket: bucket,
+ Object: object,
+ VersionID: fi.VersionID,
+ Queued: time.Now(),
+ SetIndex: er.setIndex,
+ PoolIndex: er.poolIndex,
+ BitrotScan: errors.Is(err, errFileCorrupt),
+ })
+ })
+ // Healing is triggered and we have written
+ // successfully the content to client for
+ // the specific part, we should `nil` this error
+ // and proceed forward, instead of throwing errors.
+ err = nil
+ }
+ }
+ if err != nil {
+ return toObjectErr(err, bucket, object)
+ }
+ }
+ // Track total bytes read from disk and written to the client.
+ totalBytesRead += partLength
+ // partOffset will be valid only for the first part, hence reset it to 0 for
+ // the remaining parts.
+ partOffset = 0
+ } // End of read all parts loop.
+ // Return success.
+ return nil
+}
+
+// GetObjectInfo - reads object metadata and replies back ObjectInfo.
+func (er erasureObjects) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (info ObjectInfo, err error) {
+ if !opts.NoAuditLog {
+ auditObjectErasureSet(ctx, "GetObjectInfo", object, &er)
+ }
+
+ if !opts.NoLock {
+ // Lock the object before reading.
+ lk := er.NewNSLock(bucket, object)
+ lkctx, err := lk.GetRLock(ctx, globalOperationTimeout)
+ if err != nil {
+ return ObjectInfo{}, err
+ }
+ ctx = lkctx.Context()
+ defer lk.RUnlock(lkctx)
+ }
+
+ return er.getObjectInfo(ctx, bucket, object, opts)
+}
+
+func auditDanglingObjectDeletion(ctx context.Context, bucket, object, versionID string, tags map[string]string) {
+ if len(logger.AuditTargets()) == 0 {
+ return
+ }
+
+ opts := AuditLogOptions{
+ Event: "DeleteDanglingObject",
+ Bucket: bucket,
+ Object: object,
+ VersionID: versionID,
+ Tags: tags,
+ }
+
+ auditLogInternal(ctx, opts)
+}
+
+func joinErrs(errs []error) string {
+ var s string
+ for i := range s {
+ if s != "" {
+ s += ","
+ }
+ if errs[i] == nil {
+ s += ""
+ } else {
+ s += errs[i].Error()
+ }
+ }
+ return s
+}
+
+func (er erasureObjects) deleteIfDangling(ctx context.Context, bucket, object string, metaArr []FileInfo, errs []error, dataErrsByPart map[int][]int, opts ObjectOptions) (FileInfo, error) {
+ m, ok := isObjectDangling(metaArr, errs, dataErrsByPart)
+ if !ok {
+ // We only come here if we cannot figure out if the object
+ // can be deleted safely, in such a scenario return ReadQuorum error.
+ return FileInfo{}, errErasureReadQuorum
+ }
+ tags := make(map[string]string, 16)
+ tags["set"] = strconv.Itoa(er.setIndex)
+ tags["pool"] = strconv.Itoa(er.poolIndex)
+ tags["merrs"] = joinErrs(errs)
+ tags["derrs"] = fmt.Sprintf("%v", dataErrsByPart)
+ if m.IsValid() {
+ tags["sz"] = strconv.FormatInt(m.Size, 10)
+ tags["mt"] = m.ModTime.Format(iso8601Format)
+ tags["d:p"] = fmt.Sprintf("%d:%d", m.Erasure.DataBlocks, m.Erasure.ParityBlocks)
+ } else {
+ tags["invalid"] = "1"
+ tags["d:p"] = fmt.Sprintf("%d:%d", er.setDriveCount-er.defaultParityCount, er.defaultParityCount)
+ }
+
+ // count the number of offline disks
+ offline := 0
+ for i := range len(errs) {
+ var found bool
+ switch {
+ case errors.Is(errs[i], errDiskNotFound):
+ found = true
+ default:
+ for p := range dataErrsByPart {
+ if dataErrsByPart[p][i] == checkPartDiskNotFound {
+ found = true
+ break
+ }
+ }
+ }
+ if found {
+ offline++
+ }
+ }
+ if offline > 0 {
+ tags["offline"] = strconv.Itoa(offline)
+ }
+
+ _, file, line, cok := runtime.Caller(1)
+ if cok {
+ tags["caller"] = fmt.Sprintf("%s:%d", file, line)
+ }
+
+ defer auditDanglingObjectDeletion(ctx, bucket, object, m.VersionID, tags)
+
+ fi := FileInfo{
+ VersionID: m.VersionID,
+ }
+ if opts.VersionID != "" {
+ fi.VersionID = opts.VersionID
+ }
+ fi.SetTierFreeVersionID(mustGetUUID())
+ disks := er.getDisks()
+ g := errgroup.WithNErrs(len(disks))
+ for index := range disks {
+ g.Go(func() error {
+ if disks[index] == nil {
+ return errDiskNotFound
+ }
+ return disks[index].DeleteVersion(ctx, bucket, object, fi, false, DeleteOptions{})
+ }, index)
+ }
+
+ for index, err := range g.Wait() {
+ var errStr string
+ if err != nil {
+ errStr = err.Error()
+ } else {
+ errStr = ""
+ }
+ tags[fmt.Sprintf("ddisk-%d", index)] = errStr
+ }
+
+ return m, nil
+}
+
+func fileInfoFromRaw(ri RawFileInfo, bucket, object string, readData, inclFreeVers bool) (FileInfo, error) {
+ return getFileInfo(ri.Buf, bucket, object, "", fileInfoOpts{
+ Data: readData,
+ InclFreeVersions: inclFreeVers,
+ })
+}
+
+func readAllRawFileInfo(ctx context.Context, disks []StorageAPI, bucket, object string, readData bool) ([]RawFileInfo, []error) {
+ rawFileInfos := make([]RawFileInfo, len(disks))
+ g := errgroup.WithNErrs(len(disks))
+ for index := range disks {
+ g.Go(func() (err error) {
+ if disks[index] == nil {
+ return errDiskNotFound
+ }
+ rf, err := disks[index].ReadXL(ctx, bucket, object, readData)
+ if err != nil {
+ return err
+ }
+ rawFileInfos[index] = rf
+ return nil
+ }, index)
+ }
+
+ return rawFileInfos, g.Wait()
+}
+
+func pickLatestQuorumFilesInfo(ctx context.Context, rawFileInfos []RawFileInfo, errs []error, bucket, object string, readData, inclFreeVers bool) ([]FileInfo, []error) {
+ metadataArray := make([]*xlMetaV2, len(rawFileInfos))
+ metaFileInfos := make([]FileInfo, len(rawFileInfos))
+ metadataShallowVersions := make([][]xlMetaV2ShallowVersion, len(rawFileInfos))
+ var v2bufs [][]byte
+ if !readData {
+ v2bufs = make([][]byte, len(rawFileInfos))
+ }
+
+ // Read `xl.meta` in parallel across disks.
+ for index := range rawFileInfos {
+ rf := rawFileInfos[index]
+ if rf.Buf == nil {
+ continue
+ }
+ if !readData {
+ // Save the buffer so we can reuse it.
+ v2bufs[index] = rf.Buf
+ }
+
+ var xl xlMetaV2
+ if err := xl.LoadOrConvert(rf.Buf); err != nil {
+ errs[index] = err
+ continue
+ }
+ metadataArray[index] = &xl
+ metaFileInfos[index] = FileInfo{}
+ }
+
+ for index := range metadataArray {
+ if metadataArray[index] != nil {
+ metadataShallowVersions[index] = metadataArray[index].versions
+ }
+ }
+
+ readQuorum := (len(rawFileInfos) + 1) / 2
+ meta := &xlMetaV2{versions: mergeXLV2Versions(readQuorum, false, 1, metadataShallowVersions...)}
+ lfi, err := meta.ToFileInfo(bucket, object, "", inclFreeVers, true)
+ if err != nil {
+ for i := range errs {
+ if errs[i] == nil {
+ errs[i] = err
+ }
+ }
+ return metaFileInfos, errs
+ }
+ if !lfi.IsValid() {
+ for i := range errs {
+ if errs[i] == nil {
+ errs[i] = errFileCorrupt
+ }
+ }
+ return metaFileInfos, errs
+ }
+
+ versionID := lfi.VersionID
+ if versionID == "" {
+ versionID = nullVersionID
+ }
+
+ for index := range metadataArray {
+ if metadataArray[index] == nil {
+ continue
+ }
+
+ // make sure to preserve this for diskmtime based healing bugfix.
+ metaFileInfos[index], errs[index] = metadataArray[index].ToFileInfo(bucket, object, versionID, inclFreeVers, true)
+ if errs[index] != nil {
+ continue
+ }
+
+ if readData {
+ metaFileInfos[index].Data = metadataArray[index].data.find(versionID)
+ }
+ }
+ if !readData {
+ for i := range v2bufs {
+ metaDataPoolPut(v2bufs[i])
+ }
+ }
+
+ // Return all the metadata.
+ return metaFileInfos, errs
+}
+
+// Checking if an object is dangling costs some IOPS; hence implementing this function
+// which decides which condition it is useful to check if an object is dangling
+//
+// errs: errors from reading xl.meta in all disks
+// err: reduced errs
+// bucket: the object name in question
+func shouldCheckForDangling(err error, errs []error, bucket string) bool {
+ // Avoid data in .minio.sys for now
+ if bucket == minioMetaBucket {
+ return false
+ }
+ switch {
+ // Check if we have a read quorum issue
+ case errors.Is(err, errErasureReadQuorum):
+ return true
+ // Check if the object is non-existent on most disks but not all of them
+ case (errors.Is(err, errFileNotFound) || errors.Is(err, errFileVersionNotFound)) && (countErrs(errs, nil) > 0):
+ return true
+ }
+ return false
+}
+
+func readAllXL(ctx context.Context, disks []StorageAPI, bucket, object string, readData, inclFreeVers bool) ([]FileInfo, []error) {
+ rawFileInfos, errs := readAllRawFileInfo(ctx, disks, bucket, object, readData)
+ return pickLatestQuorumFilesInfo(ctx, rawFileInfos, errs, bucket, object, readData, inclFreeVers)
+}
+
+func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object string, opts ObjectOptions, readData bool) (FileInfo, []FileInfo, []StorageAPI, error) {
+ rawArr := make([]RawFileInfo, er.setDriveCount)
+ metaArr := make([]FileInfo, er.setDriveCount)
+ errs := make([]error, er.setDriveCount)
+ for i := range errs {
+ errs[i] = errDiskOngoingReq
+ }
+
+ done := make(chan bool, er.setDriveCount)
+ disks := er.getDisks()
+
+ ropts := ReadOptions{
+ ReadData: readData,
+ InclFreeVersions: opts.InclFreeVersions,
+ Healing: false,
+ }
+
+ mrfCheck := make(chan FileInfo)
+ defer xioutil.SafeClose(mrfCheck)
+
+ var rw sync.Mutex
+
+ // Ask for all disks first;
+ go func() {
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ wg := sync.WaitGroup{}
+ for i, disk := range disks {
+ if disk == nil {
+ done <- false
+ continue
+ }
+ if !disk.IsOnline() {
+ done <- false
+ continue
+ }
+ wg.Add(1)
+ go func(i int, disk StorageAPI) {
+ defer wg.Done()
+
+ var (
+ fi FileInfo
+ rfi RawFileInfo
+ err error
+ )
+
+ if opts.VersionID != "" {
+ // Read a specific version ID
+ fi, err = disk.ReadVersion(ctx, "", bucket, object, opts.VersionID, ropts)
+ } else {
+ // Read the latest version
+ rfi, err = disk.ReadXL(ctx, bucket, object, readData)
+ if err == nil {
+ fi, err = fileInfoFromRaw(rfi, bucket, object, readData, opts.InclFreeVersions)
+ }
+ }
+
+ rw.Lock()
+ rawArr[i] = rfi
+ metaArr[i], errs[i] = fi, err
+ rw.Unlock()
+
+ done <- err == nil
+ }(i, disk)
+ }
+
+ wg.Wait()
+ xioutil.SafeClose(done)
+
+ fi, ok := <-mrfCheck
+ if !ok {
+ return
+ }
+
+ if fi.Deleted {
+ return
+ }
+
+ // if one of the disk is offline, return right here no need
+ // to attempt a heal on the object.
+ if countErrs(errs, errDiskNotFound) > 0 {
+ return
+ }
+
+ var missingBlocks int
+ for i := range errs {
+ if IsErr(errs[i],
+ errFileNotFound,
+ errFileVersionNotFound,
+ errFileCorrupt,
+ ) {
+ missingBlocks++
+ }
+ }
+
+ // if missing metadata can be reconstructed, attempt to reconstruct.
+ // additionally do not heal delete markers inline, let them be
+ // healed upon regular heal process.
+ if missingBlocks > 0 && missingBlocks < fi.Erasure.DataBlocks {
+ globalMRFState.addPartialOp(PartialOperation{
+ Bucket: fi.Volume,
+ Object: fi.Name,
+ VersionID: fi.VersionID,
+ Queued: time.Now(),
+ SetIndex: er.setIndex,
+ PoolIndex: er.poolIndex,
+ })
+ }
+ }()
+
+ validResp := 0
+ totalResp := 0
+
+ // minDisks value is only to reduce the number of calls
+ // to the disks; this value is not accurate because we do
+ // not know the storage class of the object yet
+ minDisks := 0
+ if p := globalStorageClass.GetParityForSC(""); p > -1 {
+ minDisks = er.setDriveCount - p
+ } else {
+ minDisks = er.setDriveCount - er.defaultParityCount
+ }
+
+ if minDisks == er.setDriveCount/2 {
+ // when data and parity are same we must atleast
+ // wait for response from 1 extra drive to avoid
+ // split-brain.
+ minDisks++
+ }
+
+ calcQuorum := func(metaArr []FileInfo, errs []error) (FileInfo, []FileInfo, []StorageAPI, time.Time, string, error) {
+ readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
+ if err != nil {
+ return FileInfo{}, nil, nil, time.Time{}, "", err
+ }
+ if err := reduceReadQuorumErrs(ctx, errs, objectOpIgnoredErrs, readQuorum); err != nil {
+ return FileInfo{}, nil, nil, time.Time{}, "", err
+ }
+ onlineDisks, modTime, etag := listOnlineDisks(disks, metaArr, errs, readQuorum)
+ fi, err := pickValidFileInfo(ctx, metaArr, modTime, etag, readQuorum)
+ if err != nil {
+ return FileInfo{}, nil, nil, time.Time{}, "", err
+ }
+
+ onlineMeta := make([]FileInfo, len(metaArr))
+ for i, disk := range onlineDisks {
+ if disk != nil {
+ onlineMeta[i] = metaArr[i]
+ }
+ }
+
+ return fi, onlineMeta, onlineDisks, modTime, etag, nil
+ }
+
+ var (
+ modTime time.Time
+ etag string
+ fi FileInfo
+ onlineMeta []FileInfo
+ onlineDisks []StorageAPI
+ err error
+ )
+
+ for success := range done {
+ totalResp++
+ if success {
+ validResp++
+ }
+
+ if totalResp >= minDisks && opts.FastGetObjInfo {
+ rw.Lock()
+ ok := countErrs(errs, errFileNotFound) >= minDisks || countErrs(errs, errFileVersionNotFound) >= minDisks
+ rw.Unlock()
+ if ok {
+ err = errFileNotFound
+ if opts.VersionID != "" {
+ err = errFileVersionNotFound
+ }
+ break
+ }
+ }
+
+ if totalResp < er.setDriveCount {
+ if !opts.FastGetObjInfo {
+ continue
+ }
+ if validResp < minDisks {
+ continue
+ }
+ }
+
+ rw.Lock()
+ // when its a versioned bucket and empty versionID - at totalResp == setDriveCount
+ // we must use rawFileInfo to resolve versions to figure out the latest version.
+ if opts.VersionID == "" && totalResp == er.setDriveCount {
+ fi, onlineMeta, onlineDisks, modTime, etag, err = calcQuorum(pickLatestQuorumFilesInfo(ctx,
+ rawArr, errs, bucket, object, readData, opts.InclFreeVersions))
+ } else {
+ fi, onlineMeta, onlineDisks, modTime, etag, err = calcQuorum(metaArr, errs)
+ }
+ rw.Unlock()
+ if err == nil && (fi.InlineData() || len(fi.Data) > 0) {
+ break
+ }
+ }
+
+ if err != nil {
+ // We can only look for dangling if we received all the responses, if we did
+ // not we simply ignore it, since we can't tell for sure if its dangling object.
+ if totalResp == er.setDriveCount && shouldCheckForDangling(err, errs, bucket) {
+ _, derr := er.deleteIfDangling(context.Background(), bucket, object, metaArr, errs, nil, opts)
+ if derr == nil {
+ if opts.VersionID != "" {
+ err = errFileVersionNotFound
+ } else {
+ err = errFileNotFound
+ }
+ }
+ }
+ // 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)
+ }
+
+ if !fi.Deleted && len(fi.Erasure.Distribution) != len(onlineDisks) {
+ err := fmt.Errorf("unexpected file distribution (%v) from online disks (%v), looks like backend disks have been manually modified refusing to heal %s/%s(%s)",
+ fi.Erasure.Distribution, onlineDisks, bucket, object, opts.VersionID)
+ storageLogOnceIf(ctx, err, "get-object-file-info-manually-modified")
+ return fi, nil, nil, toObjectErr(err, bucket, object, opts.VersionID)
+ }
+
+ filterOnlineDisksInplace(fi, onlineMeta, onlineDisks)
+ for i := range onlineMeta {
+ // verify metadata is valid, it has similar erasure info
+ // as well as common modtime, if modtime is not possible
+ // verify if it has common "etag" at least.
+ if onlineMeta[i].IsValid() && onlineMeta[i].Erasure.Equal(fi.Erasure) {
+ ok := onlineMeta[i].ModTime.Equal(modTime)
+ if modTime.IsZero() || modTime.Equal(timeSentinel) {
+ ok = etag != "" && etag == fi.Metadata["etag"]
+ }
+ if ok {
+ continue
+ }
+ } // in all other cases metadata is corrupt, do not read from it.
+
+ onlineMeta[i] = FileInfo{}
+ onlineDisks[i] = nil
+ }
+
+ select {
+ case mrfCheck <- fi.ShallowCopy():
+ case <-ctx.Done():
+ return fi, onlineMeta, onlineDisks, toObjectErr(ctx.Err(), bucket, object)
+ }
+
+ return fi, onlineMeta, onlineDisks, nil
+}
+
+// getObjectInfo - wrapper for reading object metadata and constructs ObjectInfo.
+func (er erasureObjects) getObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
+ fi, _, _, err := er.getObjectFileInfo(ctx, bucket, object, opts, false)
+ if err != nil {
+ return objInfo, toObjectErr(err, bucket, object)
+ }
+ objInfo = fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)
+ if fi.Deleted {
+ if opts.VersionID == "" || opts.DeleteMarker {
+ return objInfo, toObjectErr(errFileNotFound, bucket, object)
+ }
+ // Make sure to return object info to provide extra information.
+ return objInfo, toObjectErr(errMethodNotAllowed, bucket, object)
+ }
+
+ return objInfo, nil
+}
+
+// getObjectInfoAndQuorum - wrapper for reading object metadata and constructs ObjectInfo, additionally returns write quorum for the object.
+func (er erasureObjects) getObjectInfoAndQuorum(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, wquorum int, err error) {
+ fi, _, _, err := er.getObjectFileInfo(ctx, bucket, object, opts, false)
+ if err != nil {
+ return objInfo, er.defaultWQuorum(), toObjectErr(err, bucket, object)
+ }
+
+ wquorum = fi.WriteQuorum(er.defaultWQuorum())
+
+ objInfo = fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)
+ if !fi.VersionPurgeStatus().Empty() && opts.VersionID != "" {
+ // Make sure to return object info to provide extra information.
+ return objInfo, wquorum, toObjectErr(errMethodNotAllowed, bucket, object)
+ }
+
+ if fi.Deleted {
+ if opts.VersionID == "" || opts.DeleteMarker {
+ return objInfo, wquorum, toObjectErr(errFileNotFound, bucket, object)
+ }
+ // Make sure to return object info to provide extra information.
+ return objInfo, wquorum, toObjectErr(errMethodNotAllowed, bucket, object)
+ }
+
+ return objInfo, wquorum, nil
+}
+
+// Similar to rename but renames data from srcEntry to dstEntry at dataDir
+func renameData(ctx context.Context, disks []StorageAPI, srcBucket, srcEntry string, metadata []FileInfo, dstBucket, dstEntry string, writeQuorum int) ([]StorageAPI, []byte, string, error) {
+ g := errgroup.WithNErrs(len(disks))
+
+ fvID := mustGetUUID()
+ for index := range disks {
+ metadata[index].SetTierFreeVersionID(fvID)
+ }
+
+ diskVersions := make([][]byte, len(disks))
+ dataDirs := make([]string, len(disks))
+ // Rename file on all underlying storage disks.
+ for index := range disks {
+ g.Go(func() error {
+ if disks[index] == nil {
+ return errDiskNotFound
+ }
+
+ // Pick one FileInfo for a disk at index.
+ fi := metadata[index]
+ // Assign index when index is initialized
+ if fi.Erasure.Index == 0 {
+ fi.Erasure.Index = index + 1
+ }
+
+ if !fi.IsValid() {
+ return errFileCorrupt
+ }
+ resp, err := disks[index].RenameData(ctx, srcBucket, srcEntry, fi, dstBucket, dstEntry, RenameOptions{})
+ if err != nil {
+ return err
+ }
+ diskVersions[index] = resp.Sign
+ dataDirs[index] = resp.OldDataDir
+ return nil
+ }, index)
+ }
+
+ // Wait for all renames to finish.
+ errs := g.Wait()
+
+ err := reduceWriteQuorumErrs(ctx, errs, objectOpIgnoredErrs, writeQuorum)
+ if err != nil {
+ dg := errgroup.WithNErrs(len(disks))
+ for index, nerr := range errs {
+ if nerr != nil {
+ continue
+ }
+ index := index
+ // When we are going to return error, attempt to delete success
+ // on some of the drives, if we cannot we do not have to notify
+ // caller this dangling object will be now scheduled to be removed
+ // via active healing.
+ dg.Go(func() error {
+ return disks[index].DeleteVersion(context.Background(), dstBucket, dstEntry, metadata[index], false, DeleteOptions{
+ UndoWrite: true,
+ OldDataDir: dataDirs[index],
+ })
+ }, index)
+ }
+ dg.Wait()
+ }
+ var dataDir string
+ var versions []byte
+ if err == nil {
+ versions = reduceCommonVersions(diskVersions, writeQuorum)
+ for index, dversions := range diskVersions {
+ if errs[index] != nil {
+ continue
+ }
+ if !bytes.Equal(dversions, versions) {
+ if len(dversions) > len(versions) {
+ versions = dversions
+ }
+ break
+ }
+ }
+ dataDir = reduceCommonDataDir(dataDirs, writeQuorum)
+ }
+
+ // We can safely allow RenameData errors up to len(er.getDisks()) - writeQuorum
+ // otherwise return failure.
+ return evalDisks(disks, errs), versions, dataDir, err
+}
+
+func (er erasureObjects) putMetacacheObject(ctx context.Context, key string, r *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) {
+ data := r.Reader
+
+ // No metadata is set, allocate a new one.
+ if opts.UserDefined == nil {
+ opts.UserDefined = make(map[string]string)
+ }
+
+ storageDisks := er.getDisks()
+ // Get parity and data drive count based on storage class metadata
+ parityDrives := globalStorageClass.GetParityForSC(opts.UserDefined[xhttp.AmzStorageClass])
+ if parityDrives < 0 {
+ parityDrives = er.defaultParityCount
+ }
+ dataDrives := len(storageDisks) - parityDrives
+
+ // we now know the number of blocks this object needs for data and parity.
+ // writeQuorum is dataBlocks + 1
+ writeQuorum := dataDrives
+ if dataDrives == parityDrives {
+ writeQuorum++
+ }
+
+ // Validate input data size and it can never be less than zero.
+ if data.Size() < -1 {
+ bugLogIf(ctx, errInvalidArgument, logger.ErrorKind)
+ return ObjectInfo{}, toObjectErr(errInvalidArgument)
+ }
+
+ // Initialize parts metadata
+ partsMetadata := make([]FileInfo, len(storageDisks))
+
+ fi := newFileInfo(pathJoin(minioMetaBucket, key), dataDrives, parityDrives)
+ fi.DataDir = mustGetUUID()
+
+ // Initialize erasure metadata.
+ for index := range partsMetadata {
+ partsMetadata[index] = fi
+ }
+
+ // Order disks according to erasure distribution
+ var onlineDisks []StorageAPI
+ onlineDisks, partsMetadata = shuffleDisksAndPartsMetadata(storageDisks, partsMetadata, fi)
+
+ erasure, err := NewErasure(ctx, fi.Erasure.DataBlocks, fi.Erasure.ParityBlocks, fi.Erasure.BlockSize)
+ if err != nil {
+ return ObjectInfo{}, toObjectErr(err, minioMetaBucket, key)
+ }
+
+ // Fetch buffer for I/O, returns from the pool if not allocates a new one and returns.
+ var buffer []byte
+ switch size := data.Size(); {
+ case size == 0:
+ buffer = make([]byte, 1) // Allocate at least a byte to reach EOF
+ case size >= fi.Erasure.BlockSize:
+ buffer = globalBytePoolCap.Load().Get()
+ defer globalBytePoolCap.Load().Put(buffer)
+ case size < fi.Erasure.BlockSize:
+ // No need to allocate fully blockSizeV1 buffer if the incoming data is smaller.
+ buffer = make([]byte, size, 2*size+int64(fi.Erasure.ParityBlocks+fi.Erasure.DataBlocks-1))
+ }
+
+ if len(buffer) > int(fi.Erasure.BlockSize) {
+ buffer = buffer[:fi.Erasure.BlockSize]
+ }
+
+ writers := make([]io.Writer, len(onlineDisks))
+ inlineBuffers := make([]*bytes.Buffer, len(onlineDisks))
+ for i, disk := range onlineDisks {
+ if disk == nil {
+ continue
+ }
+ if disk.IsOnline() {
+ buf := grid.GetByteBufferCap(int(erasure.ShardFileSize(data.Size())) + 64)
+ inlineBuffers[i] = bytes.NewBuffer(buf[:0])
+ defer grid.PutByteBuffer(buf)
+ writers[i] = newStreamingBitrotWriterBuffer(inlineBuffers[i], DefaultBitrotAlgorithm, erasure.ShardSize())
+ }
+ }
+
+ n, erasureErr := erasure.Encode(ctx, data, writers, buffer, writeQuorum)
+ closeErrs := closeBitrotWriters(writers)
+ if erasureErr != nil {
+ 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
+ // than specified in request header.
+ if n < data.Size() {
+ return ObjectInfo{}, IncompleteBody{Bucket: minioMetaBucket, Object: key}
+ }
+ var index []byte
+ if opts.IndexCB != nil {
+ index = opts.IndexCB()
+ }
+
+ modTime := UTCNow()
+
+ for i, w := range writers {
+ if w == nil {
+ // Make sure to avoid writing to disks which we couldn't complete in erasure.Encode()
+ onlineDisks[i] = nil
+ continue
+ }
+ partsMetadata[i].Data = inlineBuffers[i].Bytes()
+ partsMetadata[i].AddObjectPart(1, "", n, data.ActualSize(), modTime, index, nil)
+ }
+
+ // Fill all the necessary metadata.
+ // Update `xl.meta` content on each disks.
+ for index := range partsMetadata {
+ partsMetadata[index].Size = n
+ partsMetadata[index].Fresh = true
+ partsMetadata[index].ModTime = modTime
+ partsMetadata[index].Metadata = opts.UserDefined
+ }
+
+ // Set an additional header when data is inlined.
+ for index := range partsMetadata {
+ partsMetadata[index].SetInlineData()
+ }
+
+ for i := range len(onlineDisks) {
+ if onlineDisks[i] != nil && onlineDisks[i].IsOnline() {
+ // Object info is the same in all disks, so we can pick
+ // the first meta from online disk
+ fi = partsMetadata[i]
+ break
+ }
+ }
+
+ if _, err = writeUniqueFileInfo(ctx, onlineDisks, "", minioMetaBucket, key, partsMetadata, writeQuorum); err != nil {
+ return ObjectInfo{}, toObjectErr(err, minioMetaBucket, key)
+ }
+
+ return fi.ToObjectInfo(minioMetaBucket, key, opts.Versioned || opts.VersionSuspended), nil
+}
+
+// PutObject - creates an object upon reading from the input stream
+// until EOF, erasure codes the data across all disk and additionally
+// writes `xl.meta` which carries the necessary metadata for future
+// object operations.
+func (er erasureObjects) PutObject(ctx context.Context, bucket string, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) {
+ return er.putObject(ctx, bucket, object, data, opts)
+}
+
+// putObject wrapper for erasureObjects PutObject
+func (er erasureObjects) putObject(ctx context.Context, bucket string, object string, r *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) {
+ if !opts.NoAuditLog {
+ auditObjectErasureSet(ctx, "PutObject", object, &er)
+ }
+
+ data := r.Reader
+
+ if opts.CheckPrecondFn != nil {
+ if !opts.NoLock {
+ ns := er.NewNSLock(bucket, object)
+ lkctx, err := ns.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ return ObjectInfo{}, err
+ }
+ ctx = lkctx.Context()
+ defer ns.Unlock(lkctx)
+ opts.NoLock = true
+ }
+
+ obj, err := er.getObjectInfo(ctx, bucket, object, opts)
+ if err == nil && opts.CheckPrecondFn(obj) {
+ return objInfo, PreConditionFailed{}
+ }
+ if err != nil && !isErrVersionNotFound(err) && !isErrObjectNotFound(err) && !isErrReadQuorum(err) {
+ return objInfo, err
+ }
+
+ // if object doesn't exist return error for If-Match conditional requests
+ // If-None-Match should be allowed to proceed for non-existent objects
+ if err != nil && opts.HasIfMatch && (isErrObjectNotFound(err) || isErrVersionNotFound(err)) {
+ return objInfo, err
+ }
+ }
+
+ // Validate input data size and it can never be less than -1.
+ if data.Size() < -1 {
+ bugLogIf(ctx, errInvalidArgument, logger.ErrorKind)
+ return ObjectInfo{}, toObjectErr(errInvalidArgument)
+ }
+
+ userDefined := cloneMSS(opts.UserDefined)
+
+ storageDisks := er.getDisks()
+
+ // Get parity and data drive count based on storage class metadata
+ parityDrives := globalStorageClass.GetParityForSC(userDefined[xhttp.AmzStorageClass])
+ if parityDrives < 0 {
+ parityDrives = er.defaultParityCount
+ }
+ if opts.MaxParity {
+ parityDrives = len(storageDisks) / 2
+ }
+ if !opts.MaxParity && globalStorageClass.AvailabilityOptimized() {
+ // If we have offline disks upgrade the number of erasure codes for this object.
+ parityOrig := parityDrives
+
+ var offlineDrives int
+ for _, disk := range storageDisks {
+ if disk == nil || !disk.IsOnline() {
+ parityDrives++
+ offlineDrives++
+ continue
+ }
+ }
+
+ if offlineDrives >= (len(storageDisks)+1)/2 {
+ // if offline drives are more than 50% of the drives
+ // we have no quorum, we shouldn't proceed just
+ // fail at that point.
+ return ObjectInfo{}, toObjectErr(errErasureWriteQuorum, bucket, object)
+ }
+
+ if parityDrives >= len(storageDisks)/2 {
+ parityDrives = len(storageDisks) / 2
+ }
+
+ if parityOrig != parityDrives {
+ userDefined[minIOErasureUpgraded] = strconv.Itoa(parityOrig) + "->" + strconv.Itoa(parityDrives)
+ }
+ }
+ dataDrives := len(storageDisks) - parityDrives
+
+ // we now know the number of blocks this object needs for data and parity.
+ // writeQuorum is dataBlocks + 1
+ writeQuorum := dataDrives
+ if dataDrives == parityDrives {
+ writeQuorum++
+ }
+
+ // Initialize parts metadata
+ partsMetadata := make([]FileInfo, len(storageDisks))
+
+ fi := newFileInfo(pathJoin(bucket, object), dataDrives, parityDrives)
+ fi.VersionID = opts.VersionID
+ if opts.Versioned && fi.VersionID == "" {
+ fi.VersionID = mustGetUUID()
+ }
+
+ fi.DataDir = mustGetUUID()
+ if ckSum := userDefined[ReplicationSsecChecksumHeader]; ckSum != "" {
+ if v, err := base64.StdEncoding.DecodeString(ckSum); err == nil {
+ fi.Checksum = v
+ }
+ delete(userDefined, ReplicationSsecChecksumHeader)
+ }
+ uniqueID := mustGetUUID()
+ tempObj := uniqueID
+
+ // Initialize erasure metadata.
+ for index := range partsMetadata {
+ partsMetadata[index] = fi
+ }
+
+ // Order disks according to erasure distribution
+ var onlineDisks []StorageAPI
+ onlineDisks, partsMetadata = shuffleDisksAndPartsMetadata(storageDisks, partsMetadata, fi)
+
+ erasure, err := NewErasure(ctx, fi.Erasure.DataBlocks, fi.Erasure.ParityBlocks, fi.Erasure.BlockSize)
+ if err != nil {
+ return ObjectInfo{}, toObjectErr(err, bucket, object)
+ }
+
+ // Fetch buffer for I/O, returns from the pool if not allocates a new one and returns.
+ var buffer []byte
+ switch size := data.Size(); {
+ case size == 0:
+ buffer = make([]byte, 1) // Allocate at least a byte to reach EOF
+ case size >= fi.Erasure.BlockSize || size == -1:
+ buffer = globalBytePoolCap.Load().Get()
+ defer globalBytePoolCap.Load().Put(buffer)
+ case size < fi.Erasure.BlockSize:
+ // No need to allocate fully blockSizeV1 buffer if the incoming data is smaller.
+ buffer = make([]byte, size, 2*size+int64(fi.Erasure.ParityBlocks+fi.Erasure.DataBlocks-1))
+ }
+
+ if len(buffer) > int(fi.Erasure.BlockSize) {
+ buffer = buffer[:fi.Erasure.BlockSize]
+ }
+
+ partName := "part.1"
+ tempErasureObj := pathJoin(uniqueID, fi.DataDir, partName)
+
+ defer er.deleteAll(context.Background(), minioMetaTmpBucket, tempObj)
+
+ var inlineBuffers []*bytes.Buffer
+ if globalStorageClass.ShouldInline(erasure.ShardFileSize(data.ActualSize()), opts.Versioned) {
+ inlineBuffers = make([]*bytes.Buffer, len(onlineDisks))
+ }
+
+ shardFileSize := erasure.ShardFileSize(data.Size())
+ writers := make([]io.Writer, len(onlineDisks))
+ for i, disk := range onlineDisks {
+ if disk == nil {
+ continue
+ }
+
+ if !disk.IsOnline() {
+ continue
+ }
+
+ if len(inlineBuffers) > 0 {
+ buf := grid.GetByteBufferCap(int(shardFileSize) + 64)
+ inlineBuffers[i] = bytes.NewBuffer(buf[:0])
+ defer grid.PutByteBuffer(buf)
+ writers[i] = newStreamingBitrotWriterBuffer(inlineBuffers[i], DefaultBitrotAlgorithm, erasure.ShardSize())
+ continue
+ }
+
+ writers[i] = newBitrotWriter(disk, bucket, minioMetaTmpBucket, tempErasureObj, shardFileSize, DefaultBitrotAlgorithm, erasure.ShardSize())
+ }
+
+ toEncode := io.Reader(data)
+ if data.Size() >= bigFileThreshold {
+ // We use 2 buffers, so we always have a full buffer of input.
+ pool := globalBytePoolCap.Load()
+ bufA := pool.Get()
+ bufB := pool.Get()
+ defer pool.Put(bufA)
+ defer pool.Put(bufB)
+ ra, err := readahead.NewReaderBuffer(data, [][]byte{bufA[:fi.Erasure.BlockSize], bufB[:fi.Erasure.BlockSize]})
+ if err == nil {
+ toEncode = ra
+ defer ra.Close()
+ }
+ bugLogIf(ctx, err)
+ }
+ n, erasureErr := erasure.Encode(ctx, toEncode, writers, buffer, writeQuorum)
+ closeErrs := closeBitrotWriters(writers)
+ if erasureErr != nil {
+ 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
+ // than specified in request header.
+ if n < data.Size() {
+ return ObjectInfo{}, IncompleteBody{Bucket: bucket, Object: object}
+ }
+
+ var compIndex []byte
+ if opts.IndexCB != nil {
+ compIndex = opts.IndexCB()
+ }
+
+ modTime := opts.MTime
+ if opts.MTime.IsZero() {
+ modTime = UTCNow()
+ }
+
+ kind, encrypted := crypto.IsEncrypted(userDefined)
+ actualSize := data.ActualSize()
+ if actualSize < 0 {
+ compressed := fi.IsCompressed()
+ switch {
+ case compressed:
+ // ... nothing changes for compressed stream.
+ // if actualSize is -1 we have no known way to
+ // determine what is the actualSize.
+ case encrypted:
+ decSize, err := sio.DecryptedSize(uint64(n))
+ if err == nil {
+ actualSize = int64(decSize)
+ }
+ default:
+ actualSize = n
+ }
+ }
+ // If ServerSideChecksum is wanted for this object, it takes precedence
+ // over opts.WantChecksum.
+ if opts.WantServerSideChecksumType.IsSet() {
+ serverSideChecksum := r.RawServerSideChecksumResult()
+ if serverSideChecksum != nil {
+ fi.Checksum = serverSideChecksum.AppendTo(nil, nil)
+ if opts.EncryptFn != nil {
+ fi.Checksum = opts.EncryptFn("object-checksum", fi.Checksum)
+ }
+ }
+ } else if fi.Checksum == nil && opts.WantChecksum != nil {
+ // Trailing headers checksums should now be filled.
+ fi.Checksum = opts.WantChecksum.AppendTo(nil, nil)
+ if opts.EncryptFn != nil {
+ fi.Checksum = opts.EncryptFn("object-checksum", fi.Checksum)
+ }
+ }
+ for i, w := range writers {
+ if w == nil {
+ onlineDisks[i] = nil
+ continue
+ }
+ if len(inlineBuffers) > 0 && inlineBuffers[i] != nil {
+ partsMetadata[i].Data = inlineBuffers[i].Bytes()
+ } else {
+ partsMetadata[i].Data = nil
+ }
+ // No need to add checksum to part. We already have it on the object.
+ partsMetadata[i].AddObjectPart(1, "", n, actualSize, modTime, compIndex, nil)
+ partsMetadata[i].Versioned = opts.Versioned || opts.VersionSuspended
+ partsMetadata[i].Checksum = fi.Checksum
+ }
+
+ userDefined["etag"] = r.MD5CurrentHexString()
+ if opts.PreserveETag != "" {
+ if !opts.ReplicationRequest {
+ userDefined["etag"] = opts.PreserveETag
+ } else if kind != crypto.S3 {
+ // if we have a replication request
+ // and SSE-S3 is specified do not preserve
+ // the incoming etag.
+ userDefined["etag"] = opts.PreserveETag
+ }
+ }
+
+ // Guess content-type from the extension if possible.
+ if userDefined["content-type"] == "" {
+ userDefined["content-type"] = mimedb.TypeByExtension(path.Ext(object))
+ }
+
+ // if storageClass is standard no need to save it as part of metadata.
+ if userDefined[xhttp.AmzStorageClass] == storageclass.STANDARD {
+ delete(userDefined, xhttp.AmzStorageClass)
+ }
+
+ // Fill all the necessary metadata.
+ // Update `xl.meta` content on each disks.
+ for index := range partsMetadata {
+ partsMetadata[index].Metadata = userDefined
+ partsMetadata[index].Size = n
+ partsMetadata[index].ModTime = modTime
+ if len(inlineBuffers) > 0 {
+ partsMetadata[index].SetInlineData()
+ }
+ if opts.DataMovement {
+ partsMetadata[index].SetDataMov()
+ }
+ }
+
+ if !opts.NoLock {
+ lk := er.NewNSLock(bucket, object)
+ lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ return ObjectInfo{}, err
+ }
+ ctx = lkctx.Context()
+ defer lk.Unlock(lkctx)
+ }
+
+ // Rename the successfully written temporary object to final location.
+ onlineDisks, versions, oldDataDir, err := renameData(ctx, onlineDisks, minioMetaTmpBucket, tempObj, partsMetadata, bucket, object, writeQuorum)
+ if err != nil {
+ if errors.Is(err, errFileNotFound) {
+ // An in-quorum errFileNotFound means that client stream
+ // prematurely closed and we do not find any xl.meta or
+ // part.1's - in such a scenario we must return as if client
+ // disconnected. This means that erasure.Encode() CreateFile()
+ // did not do anything.
+ return ObjectInfo{}, IncompleteBody{Bucket: bucket, Object: object}
+ }
+ return ObjectInfo{}, toObjectErr(err, bucket, object)
+ }
+
+ if err = er.commitRenameDataDir(ctx, bucket, object, oldDataDir, onlineDisks, writeQuorum); err != nil {
+ return ObjectInfo{}, toObjectErr(err, bucket, object)
+ }
+
+ for i := range len(onlineDisks) {
+ if onlineDisks[i] != nil && onlineDisks[i].IsOnline() {
+ // Object info is the same in all disks, so we can pick
+ // the first meta from online disk
+ fi = partsMetadata[i]
+ break
+ }
+ }
+
+ // For speedtest objects do not attempt to heal them.
+ if !opts.Speedtest {
+ // When there is versions disparity we are healing
+ // the content implicitly for all versions, we can
+ // avoid triggering another MRF heal for offline drives.
+ if len(versions) == 0 {
+ // Whether a disk was initially or becomes offline
+ // during this upload, send it to the MRF list.
+ for i := range len(onlineDisks) {
+ if onlineDisks[i] != nil && onlineDisks[i].IsOnline() {
+ continue
+ }
+
+ er.addPartial(bucket, object, fi.VersionID)
+ break
+ }
+ } else {
+ globalMRFState.addPartialOp(PartialOperation{
+ Bucket: bucket,
+ Object: object,
+ Queued: time.Now(),
+ Versions: versions,
+ SetIndex: er.setIndex,
+ PoolIndex: er.poolIndex,
+ })
+ }
+ }
+
+ fi.ReplicationState = opts.PutReplicationState()
+
+ // we are adding a new version to this object under the namespace lock, so this is the latest version.
+ fi.IsLatest = true
+
+ return fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended), nil
+}
+
+func (er erasureObjects) deleteObjectVersion(ctx context.Context, bucket, object string, fi FileInfo, forceDelMarker bool) error {
+ disks := er.getDisks()
+ // Assume (N/2 + 1) quorum for Delete()
+ // this is a theoretical assumption such that
+ // for delete's we do not need to honor storage
+ // class for objects that have reduced quorum
+ // due to storage class - this only needs to be honored
+ // for Read() requests alone that we already do.
+ writeQuorum := len(disks)/2 + 1
+
+ g := errgroup.WithNErrs(len(disks))
+ for index := range disks {
+ g.Go(func() error {
+ if disks[index] == nil {
+ return errDiskNotFound
+ }
+ return disks[index].DeleteVersion(ctx, bucket, object, fi, forceDelMarker, DeleteOptions{})
+ }, index)
+ }
+ // return errors if any during deletion
+ return reduceWriteQuorumErrs(ctx, g.Wait(), objectOpIgnoredErrs, writeQuorum)
+}
+
+// DeleteObjects deletes objects/versions in bulk, this function will still automatically split objects list
+// into smaller bulks if some object names are found to be duplicated in the delete list, splitting
+// into smaller bulks will avoid holding twice the write lock of the duplicated object names.
+func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objects []ObjectToDelete, opts ObjectOptions) ([]DeletedObject, []error) {
+ if !opts.NoAuditLog {
+ for _, obj := range objects {
+ auditObjectErasureSet(ctx, "DeleteObjects", obj.ObjectName, &er)
+ }
+ }
+
+ errs := make([]error, len(objects))
+ dobjects := make([]DeletedObject, len(objects))
+ writeQuorums := make([]int, len(objects))
+
+ storageDisks := er.getDisks()
+
+ for i := range objects {
+ // Assume (N/2 + 1) quorums for all objects
+ // this is a theoretical assumption such that
+ // for delete's we do not need to honor storage
+ // class for objects which have reduced quorum
+ // storage class only needs to be honored for
+ // Read() requests alone which we already do.
+ writeQuorums[i] = len(storageDisks)/2 + 1
+ }
+
+ versionsMap := make(map[string]FileInfoVersions, len(objects))
+ for i := range objects {
+ // Construct the FileInfo data that needs to be preserved on the disk.
+ vr := FileInfo{
+ Name: objects[i].ObjectName,
+ VersionID: objects[i].VersionID,
+ ReplicationState: objects[i].ReplicationState(),
+ // save the index to set correct error at this index.
+ Idx: i,
+ }
+ vr.SetTierFreeVersionID(mustGetUUID())
+ // VersionID is not set means delete is not specific about
+ // any version, look for if the bucket is versioned or not.
+ if objects[i].VersionID == "" {
+ // MinIO extension to bucket version configuration
+ suspended := opts.VersionSuspended
+ versioned := opts.Versioned
+ if opts.PrefixEnabledFn != nil {
+ versioned = opts.PrefixEnabledFn(objects[i].ObjectName)
+ }
+ if versioned || suspended {
+ // Bucket is versioned and no version was explicitly
+ // mentioned for deletes, create a delete marker instead.
+ vr.ModTime = UTCNow()
+ vr.Deleted = true
+ // Versioning suspended means that we add a `null` version
+ // delete marker, if not add a new version for this delete
+ // marker.
+ if versioned {
+ vr.VersionID = mustGetUUID()
+ }
+ }
+ }
+ // De-dup same object name to collect multiple versions for same object.
+ v, ok := versionsMap[objects[i].ObjectName]
+ if ok {
+ v.Versions = append(v.Versions, vr)
+ } else {
+ v = FileInfoVersions{
+ Name: vr.Name,
+ Versions: []FileInfo{vr},
+ }
+ }
+ if vr.Deleted {
+ dobjects[i] = DeletedObject{
+ DeleteMarker: vr.Deleted,
+ DeleteMarkerVersionID: vr.VersionID,
+ DeleteMarkerMTime: DeleteMarkerMTime{vr.ModTime},
+ ObjectName: vr.Name,
+ ReplicationState: vr.ReplicationState,
+ }
+ } else {
+ dobjects[i] = DeletedObject{
+ ObjectName: vr.Name,
+ VersionID: vr.VersionID,
+ ReplicationState: vr.ReplicationState,
+ }
+ }
+ versionsMap[objects[i].ObjectName] = v
+ }
+
+ dedupVersions := make([]FileInfoVersions, 0, len(versionsMap))
+ for _, fivs := range versionsMap {
+ // 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.
+ delObjErrs := make([][]error, len(storageDisks))
+
+ var wg sync.WaitGroup
+ // Remove versions in bulk for each disk
+ for index, disk := range storageDisks {
+ wg.Add(1)
+ go func(index int, disk StorageAPI) {
+ defer wg.Done()
+ delObjErrs[index] = make([]error, len(objects))
+ if disk == nil {
+ for i := range objects {
+ delObjErrs[index][i] = errDiskNotFound
+ }
+ return
+ }
+ errs := disk.DeleteVersions(ctx, bucket, dedupVersions, DeleteOptions{})
+ for i, err := range errs {
+ if err == nil {
+ continue
+ }
+ for _, v := range dedupVersions[i].Versions {
+ delObjErrs[index][v.Idx] = err
+ }
+ }
+ }(index, disk)
+ }
+ wg.Wait()
+
+ // Reduce errors for each object
+ for objIndex := range objects {
+ diskErrs := make([]error, len(storageDisks))
+ // Iterate over disks to fetch the error
+ // of deleting of the current object
+ for i := range delObjErrs {
+ // delObjErrs[i] is not nil when disks[i] is also not nil
+ if delObjErrs[i] != nil {
+ diskErrs[i] = delObjErrs[i][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 != "" {
+ errs[objIndex] = toObjectErr(err, bucket, objects[objIndex].ObjectName, objects[objIndex].VersionID)
+ } else {
+ errs[objIndex] = toObjectErr(err, bucket, objects[objIndex].ObjectName)
+ }
+ }
+
+ // Check failed deletes across multiple objects
+ for i, dobj := range dobjects {
+ // This object errored, we should attempt a heal just in case.
+ if errs[i] != nil && !isErrVersionNotFound(errs[i]) && !isErrObjectNotFound(errs[i]) {
+ // all other direct versionId references we should
+ // ensure no dangling file is left over.
+ er.addPartial(bucket, dobj.ObjectName, dobj.VersionID)
+ continue
+ }
+
+ // Check if there is any offline disk and add it to the MRF list
+ for _, disk := range storageDisks {
+ if disk != nil && disk.IsOnline() {
+ // Skip attempted heal on online disks.
+ continue
+ }
+
+ // all other direct versionId references we should
+ // ensure no dangling file is left over.
+ er.addPartial(bucket, dobj.ObjectName, dobj.VersionID)
+ break
+ }
+ }
+
+ return dobjects, errs
+}
+
+func (er erasureObjects) commitRenameDataDir(ctx context.Context, bucket, object, dataDir string, onlineDisks []StorageAPI, writeQuorum int) error {
+ if dataDir == "" {
+ return nil
+ }
+ g := errgroup.WithNErrs(len(onlineDisks))
+ for index := range onlineDisks {
+ g.Go(func() error {
+ if onlineDisks[index] == nil {
+ return nil
+ }
+ return onlineDisks[index].Delete(ctx, bucket, pathJoin(object, dataDir), DeleteOptions{
+ Recursive: true,
+ })
+ }, index)
+ }
+
+ return reduceWriteQuorumErrs(ctx, g.Wait(), objectOpIgnoredErrs, writeQuorum)
+}
+
+func (er erasureObjects) deletePrefix(ctx context.Context, bucket, prefix string) error {
+ disks := er.getDisks()
+ // Assume (N/2 + 1) quorum for Delete()
+ // this is a theoretical assumption such that
+ // for delete's we do not need to honor storage
+ // class for objects that have reduced quorum
+ // due to storage class - this only needs to be honored
+ // for Read() requests alone that we already do.
+ writeQuorum := len(disks)/2 + 1
+
+ g := errgroup.WithNErrs(len(disks))
+ for index := range disks {
+ g.Go(func() error {
+ if disks[index] == nil {
+ return nil
+ }
+ return disks[index].Delete(ctx, bucket, prefix, DeleteOptions{
+ Recursive: true,
+ Immediate: true,
+ })
+ }, index)
+ }
+
+ // return errors if any during deletion
+ return reduceWriteQuorumErrs(ctx, g.Wait(), objectOpIgnoredErrs, writeQuorum)
+}
+
+// DeleteObject - deletes an object, this call doesn't necessary reply
+// any error as it is not necessary for the handler to reply back a
+// response to the client request.
+func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
+ if !opts.NoAuditLog {
+ auditObjectErasureSet(ctx, "DeleteObject", object, &er)
+ }
+
+ var lc *lifecycle.Lifecycle
+ var rcfg lock.Retention
+ var replcfg *replication.Config
+ if opts.Expiration.Expire {
+ // Check if the current bucket has a configured lifecycle policy
+ lc, err = globalLifecycleSys.Get(bucket)
+ if err != nil && !errors.Is(err, BucketLifecycleNotFound{Bucket: bucket}) {
+ return objInfo, err
+ }
+ rcfg, err = globalBucketObjectLockSys.Get(bucket)
+ if err != nil {
+ return objInfo, err
+ }
+ replcfg, err = getReplicationConfig(ctx, bucket)
+ if err != nil {
+ return objInfo, err
+ }
+ }
+
+ // expiration attempted on a bucket with no lifecycle
+ // rules shall be rejected.
+ if lc == nil && opts.Expiration.Expire {
+ if opts.VersionID != "" {
+ return objInfo, VersionNotFound{
+ Bucket: bucket,
+ Object: object,
+ VersionID: opts.VersionID,
+ }
+ }
+ return objInfo, ObjectNotFound{
+ Bucket: bucket,
+ Object: object,
+ }
+ }
+
+ if opts.DeletePrefix {
+ if opts.Expiration.Expire {
+ // Expire all versions expiration must still verify the state() on disk
+ // via a getObjectInfo() call as follows, any read quorum issues we
+ // must not proceed further for safety reasons. attempt a MRF heal
+ // while we see such quorum errors.
+ goi, _, gerr := er.getObjectInfoAndQuorum(ctx, bucket, object, opts)
+ if gerr != nil && goi.Name == "" {
+ if _, ok := gerr.(InsufficientReadQuorum); ok {
+ // Add an MRF heal for next time.
+ er.addPartial(bucket, object, opts.VersionID)
+
+ return objInfo, InsufficientWriteQuorum{}
+ }
+ return objInfo, gerr
+ }
+
+ // Add protection and re-verify the ILM rules for qualification
+ // based on the latest objectInfo and see if the object still
+ // qualifies for deletion.
+ if gerr == nil {
+ var isErr bool
+ evt := evalActionFromLifecycle(ctx, *lc, rcfg, replcfg, goi)
+ switch evt.Action {
+ case lifecycle.DeleteAllVersionsAction, lifecycle.DelMarkerDeleteAllVersionsAction:
+ // opts.DeletePrefix is used only in the above lifecycle Expiration actions.
+ default:
+ // object has been modified since lifecycle action was previously evaluated
+ isErr = true
+ }
+ if isErr {
+ if goi.VersionID != "" {
+ return goi, VersionNotFound{
+ Bucket: bucket,
+ Object: object,
+ VersionID: goi.VersionID,
+ }
+ }
+ return goi, ObjectNotFound{
+ Bucket: bucket,
+ Object: object,
+ }
+ }
+ }
+ } // Delete marker and any latest that qualifies shall be expired permanently.
+
+ return ObjectInfo{}, toObjectErr(er.deletePrefix(ctx, bucket, object), bucket, object)
+ }
+
+ storageDisks := er.getDisks()
+ versionFound := true
+ objInfo = ObjectInfo{VersionID: opts.VersionID} // version id needed in Delete API response.
+ goi, _, gerr := er.getObjectInfoAndQuorum(ctx, bucket, object, opts)
+ tryDel := false
+ if gerr != nil && goi.Name == "" {
+ if _, ok := gerr.(InsufficientReadQuorum); ok {
+ if opts.Versioned || opts.VersionSuspended || countOnlineDisks(storageDisks) < len(storageDisks)/2+1 {
+ // Add an MRF heal for next time.
+ er.addPartial(bucket, object, opts.VersionID)
+ return objInfo, InsufficientWriteQuorum{}
+ }
+ tryDel = true // only for unversioned objects if there is write quorum
+ }
+ // For delete marker replication, versionID being replicated will not exist on disk
+ if opts.DeleteMarker {
+ versionFound = false
+ } else if !tryDel {
+ return objInfo, gerr
+ }
+ }
+
+ if opts.EvalMetadataFn != nil {
+ dsc, err := opts.EvalMetadataFn(&goi, gerr)
+ if err != nil {
+ return ObjectInfo{}, err
+ }
+ if dsc.ReplicateAny() {
+ opts.SetDeleteReplicationState(dsc, opts.VersionID)
+ goi.replicationDecision = opts.DeleteReplication.ReplicateDecisionStr
+ }
+ }
+
+ if opts.EvalRetentionBypassFn != nil {
+ if err := opts.EvalRetentionBypassFn(goi, gerr); err != nil {
+ return ObjectInfo{}, err
+ }
+ }
+
+ if opts.Expiration.Expire {
+ if gerr == nil {
+ evt := evalActionFromLifecycle(ctx, *lc, rcfg, replcfg, goi)
+ var isErr bool
+ switch evt.Action {
+ case lifecycle.NoneAction:
+ isErr = true
+ case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
+ isErr = true
+ }
+ if isErr {
+ if goi.VersionID != "" {
+ return goi, VersionNotFound{
+ Bucket: bucket,
+ Object: object,
+ VersionID: goi.VersionID,
+ }
+ }
+ return goi, ObjectNotFound{
+ Bucket: bucket,
+ Object: object,
+ }
+ }
+ }
+ }
+
+ // Determine whether to mark object deleted for replication
+ markDelete := goi.VersionID != ""
+
+ // Default deleteMarker to true if object is under versioning
+ deleteMarker := opts.Versioned
+
+ if opts.VersionID != "" {
+ // case where replica version needs to be deleted on target cluster
+ if versionFound && opts.DeleteMarkerReplicationStatus() == replication.Replica {
+ markDelete = false
+ }
+ if opts.VersionPurgeStatus().Empty() && opts.DeleteMarkerReplicationStatus().Empty() {
+ markDelete = false
+ }
+ if opts.VersionPurgeStatus() == replication.VersionPurgeComplete {
+ markDelete = false
+ }
+ // now, since VersionPurgeStatus() is already set, we can let the
+ // lower layers decide this. This fixes a regression that was introduced
+ // in PR #14555 where !VersionPurgeStatus.Empty() is automatically
+ // considered as Delete marker true to avoid listing such objects by
+ // regular ListObjects() calls. However for delete replication this
+ // ends up being a problem because "upon" a successful delete this
+ // ends up creating a new delete marker that is spurious and unnecessary.
+ //
+ // Regression introduced by #14555 was reintroduced in #15564
+ if versionFound {
+ if !goi.VersionPurgeStatus.Empty() {
+ deleteMarker = false
+ } else if !goi.DeleteMarker { // implies a versioned delete of object
+ deleteMarker = false
+ }
+ }
+ }
+
+ modTime := opts.MTime
+ if opts.MTime.IsZero() {
+ modTime = UTCNow()
+ }
+ fvID := mustGetUUID()
+
+ defer func() {
+ // attempt a heal before returning if there are offline disks
+ // for both del marker and permanent delete situations.
+ for _, disk := range storageDisks {
+ if disk != nil && disk.IsOnline() {
+ continue
+ }
+ er.addPartial(bucket, object, opts.VersionID)
+ break
+ }
+ }()
+
+ if markDelete && (opts.Versioned || opts.VersionSuspended) {
+ if !deleteMarker {
+ // versioning suspended means we add `null` version as
+ // delete marker, if its not decided already.
+ deleteMarker = opts.VersionSuspended && opts.VersionID == ""
+ }
+ fi := FileInfo{
+ Name: object,
+ Deleted: deleteMarker,
+ MarkDeleted: markDelete,
+ ModTime: modTime,
+ ReplicationState: opts.DeleteReplication,
+ TransitionStatus: opts.Transition.Status,
+ ExpireRestored: opts.Transition.ExpireRestored,
+ }
+ fi.SetTierFreeVersionID(fvID)
+ if opts.SkipFreeVersion {
+ fi.SetSkipTierFreeVersion()
+ }
+ if opts.VersionID != "" {
+ fi.VersionID = opts.VersionID
+ } else if opts.Versioned {
+ fi.VersionID = mustGetUUID()
+ }
+ // versioning suspended means we add `null` version as
+ // delete marker. Add delete marker, since we don't have
+ // any version specified explicitly. Or if a particular
+ // version id needs to be replicated.
+ if err = er.deleteObjectVersion(ctx, bucket, object, fi, opts.DeleteMarker); err != nil {
+ return objInfo, toObjectErr(err, bucket, object)
+ }
+ oi := fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)
+ oi.replicationDecision = goi.replicationDecision
+ return oi, nil
+ }
+
+ // Delete the object version on all disks.
+ dfi := FileInfo{
+ Name: object,
+ VersionID: opts.VersionID,
+ MarkDeleted: markDelete,
+ Deleted: deleteMarker,
+ ModTime: modTime,
+ ReplicationState: opts.DeleteReplication,
+ TransitionStatus: opts.Transition.Status,
+ ExpireRestored: opts.Transition.ExpireRestored,
+ }
+ dfi.SetTierFreeVersionID(fvID)
+ if opts.SkipFreeVersion {
+ dfi.SetSkipTierFreeVersion()
+ }
+ if err = er.deleteObjectVersion(ctx, bucket, object, dfi, opts.DeleteMarker); err != nil {
+ return objInfo, toObjectErr(err, bucket, object)
+ }
+
+ return dfi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended), nil
+}
+
+// Send the successful but partial upload/delete, however ignore
+// if the channel is blocked by other items.
+func (er erasureObjects) addPartial(bucket, object, versionID string) {
+ globalMRFState.addPartialOp(PartialOperation{
+ Bucket: bucket,
+ Object: object,
+ VersionID: versionID,
+ Queued: time.Now(),
+ })
+}
+
+func (er erasureObjects) PutObjectMetadata(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
+ if !opts.NoLock {
+ // Lock the object before updating metadata.
+ lk := er.NewNSLock(bucket, object)
+ lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ return ObjectInfo{}, err
+ }
+ ctx = lkctx.Context()
+ defer lk.Unlock(lkctx)
+ }
+
+ disks := er.getDisks()
+
+ var metaArr []FileInfo
+ var errs []error
+
+ // Read metadata associated with the object from all disks.
+ if opts.VersionID != "" {
+ metaArr, errs = readAllFileInfo(ctx, disks, "", bucket, object, opts.VersionID, false, false)
+ } else {
+ metaArr, errs = readAllXL(ctx, disks, bucket, object, false, false)
+ }
+
+ readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
+ if err != nil {
+ if shouldCheckForDangling(err, errs, bucket) {
+ _, derr := er.deleteIfDangling(context.Background(), bucket, object, metaArr, errs, nil, opts)
+ if derr == nil {
+ if opts.VersionID != "" {
+ err = errFileVersionNotFound
+ } else {
+ err = errFileNotFound
+ }
+ }
+ }
+ return ObjectInfo{}, toObjectErr(err, bucket, object)
+ }
+
+ // List all online disks.
+ onlineDisks, modTime, etag := listOnlineDisks(disks, metaArr, errs, readQuorum)
+
+ // Pick latest valid metadata.
+ fi, err := pickValidFileInfo(ctx, metaArr, modTime, etag, readQuorum)
+ if err != nil {
+ return ObjectInfo{}, toObjectErr(err, bucket, object)
+ }
+
+ if fi.Deleted {
+ return ObjectInfo{}, toObjectErr(errMethodNotAllowed, bucket, object)
+ }
+
+ filterOnlineDisksInplace(fi, metaArr, onlineDisks)
+
+ // if version-id is not specified retention is supposed to be set on the latest object.
+ if opts.VersionID == "" {
+ opts.VersionID = fi.VersionID
+ }
+
+ objInfo := fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)
+ if opts.EvalMetadataFn != nil {
+ if _, err := opts.EvalMetadataFn(&objInfo, err); err != nil {
+ return ObjectInfo{}, err
+ }
+ }
+ maps.Copy(fi.Metadata, objInfo.UserDefined)
+ fi.ModTime = opts.MTime
+ fi.VersionID = opts.VersionID
+
+ if err = er.updateObjectMeta(ctx, bucket, object, fi, onlineDisks); err != nil {
+ return ObjectInfo{}, toObjectErr(err, bucket, object)
+ }
+
+ return fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended), nil
+}
+
+// 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) {
+ if !opts.NoLock {
+ // Lock the object before updating tags.
+ lk := er.NewNSLock(bucket, object)
+ lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ return ObjectInfo{}, err
+ }
+ ctx = lkctx.Context()
+ defer lk.Unlock(lkctx)
+ }
+
+ disks := er.getDisks()
+
+ var metaArr []FileInfo
+ var errs []error
+
+ // Read metadata associated with the object from all disks.
+ if opts.VersionID != "" {
+ metaArr, errs = readAllFileInfo(ctx, disks, "", bucket, object, opts.VersionID, false, false)
+ } else {
+ metaArr, errs = readAllXL(ctx, disks, bucket, object, false, false)
+ }
+
+ readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
+ if err != nil {
+ if shouldCheckForDangling(err, errs, bucket) {
+ _, derr := er.deleteIfDangling(context.Background(), bucket, object, metaArr, errs, nil, opts)
+ if derr == nil {
+ if opts.VersionID != "" {
+ err = errFileVersionNotFound
+ } else {
+ err = errFileNotFound
+ }
+ }
+ }
+ return ObjectInfo{}, toObjectErr(err, bucket, object)
+ }
+
+ // List all online disks.
+ onlineDisks, modTime, etag := listOnlineDisks(disks, metaArr, errs, readQuorum)
+
+ // Pick latest valid metadata.
+ fi, err := pickValidFileInfo(ctx, metaArr, modTime, etag, readQuorum)
+ if err != nil {
+ return ObjectInfo{}, toObjectErr(err, bucket, object)
+ }
+ if fi.Deleted {
+ if opts.VersionID == "" {
+ return ObjectInfo{}, toObjectErr(errFileNotFound, bucket, object)
+ }
+ return ObjectInfo{}, toObjectErr(errMethodNotAllowed, bucket, object)
+ }
+
+ filterOnlineDisksInplace(fi, metaArr, onlineDisks)
+
+ fi.Metadata[xhttp.AmzObjectTagging] = tags
+ fi.ReplicationState = opts.PutReplicationState()
+ maps.Copy(fi.Metadata, opts.UserDefined)
+
+ if err = er.updateObjectMeta(ctx, bucket, object, fi, onlineDisks); err != nil {
+ return ObjectInfo{}, toObjectErr(err, bucket, object)
+ }
+
+ return fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended), nil
+}
+
+func (er erasureObjects) updateObjectMetaWithOpts(ctx context.Context, bucket, object string, fi FileInfo, onlineDisks []StorageAPI, opts UpdateMetadataOpts) error {
+ if len(fi.Metadata) == 0 {
+ return nil
+ }
+
+ g := errgroup.WithNErrs(len(onlineDisks))
+
+ // Start writing `xl.meta` to all disks in parallel.
+ for index := range onlineDisks {
+ g.Go(func() error {
+ if onlineDisks[index] == nil {
+ return errDiskNotFound
+ }
+ return onlineDisks[index].UpdateMetadata(ctx, bucket, object, fi, opts)
+ }, index)
+ }
+
+ // Wait for all the routines.
+ mErrs := g.Wait()
+
+ return reduceWriteQuorumErrs(ctx, mErrs, objectOpIgnoredErrs, fi.WriteQuorum(er.defaultWQuorum()))
+}
+
+// updateObjectMeta will update the metadata of a file.
+func (er erasureObjects) updateObjectMeta(ctx context.Context, bucket, object string, fi FileInfo, onlineDisks []StorageAPI) error {
+ return er.updateObjectMetaWithOpts(ctx, bucket, object, fi, onlineDisks, UpdateMetadataOpts{})
+}
+
+// DeleteObjectTags - delete object tags from an existing object
+func (er erasureObjects) DeleteObjectTags(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
+ return er.PutObjectTags(ctx, bucket, object, "", opts)
+}
+
+// GetObjectTags - get object tags from an existing object
+func (er erasureObjects) GetObjectTags(ctx context.Context, bucket, object string, opts ObjectOptions) (*tags.Tags, error) {
+ // GetObjectInfo will return tag value as well
+ oi, err := er.GetObjectInfo(ctx, bucket, object, opts)
+ if err != nil {
+ return nil, err
+ }
+
+ return tags.ParseObjectTags(oi.UserTags)
+}
+
+// TransitionObject - transition object content to target tier.
+func (er erasureObjects) TransitionObject(ctx context.Context, bucket, object string, opts ObjectOptions) error {
+ tgtClient, err := globalTierConfigMgr.getDriver(ctx, opts.Transition.Tier)
+ if err != nil {
+ return err
+ }
+
+ if !opts.NoLock {
+ // Acquire write lock before starting to transition the object.
+ lk := er.NewNSLock(bucket, object)
+ lkctx, err := lk.GetLock(ctx, globalDeleteOperationTimeout)
+ if err != nil {
+ return err
+ }
+ ctx = lkctx.Context()
+ defer lk.Unlock(lkctx)
+ }
+
+ fi, metaArr, onlineDisks, err := er.getObjectFileInfo(ctx, bucket, object, opts, true)
+ if err != nil {
+ return toObjectErr(err, bucket, object)
+ }
+ if fi.Deleted {
+ if opts.VersionID == "" {
+ return toObjectErr(errFileNotFound, bucket, object)
+ }
+ // Make sure to return object info to provide extra information.
+ return toObjectErr(errMethodNotAllowed, bucket, object)
+ }
+ // verify that the object queued for transition is identical to that on disk.
+ if !opts.MTime.Equal(fi.ModTime) || !strings.EqualFold(opts.Transition.ETag, extractETag(fi.Metadata)) {
+ return toObjectErr(errFileNotFound, bucket, object)
+ }
+ // if object already transitioned, return
+ if fi.TransitionStatus == lifecycle.TransitionComplete {
+ return nil
+ }
+
+ if fi.XLV1 {
+ if _, err = er.HealObject(ctx, bucket, object, "", madmin.HealOpts{NoLock: true}); err != nil {
+ return err
+ }
+ // Fetch FileInfo again. HealObject migrates object the latest
+ // format. Among other things this changes fi.DataDir and
+ // possibly fi.Data (if data is inlined).
+ fi, metaArr, onlineDisks, err = er.getObjectFileInfo(ctx, bucket, object, opts, true)
+ if err != nil {
+ return toObjectErr(err, bucket, object)
+ }
+ }
+ traceFn := globalLifecycleSys.trace(fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended))
+
+ destObj, err := genTransitionObjName(bucket)
+ if err != nil {
+ traceFn(ILMTransition, nil, err)
+ return err
+ }
+
+ pr, pw := xioutil.WaitPipe()
+ go func() {
+ err := er.getObjectWithFileInfo(ctx, bucket, object, 0, fi.Size, pw, fi, metaArr, onlineDisks)
+ pw.CloseWithError(err)
+ }()
+
+ var rv remoteVersionID
+ 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)
+ if err != nil {
+ traceFn(ILMTransition, nil, err)
+ return err
+ }
+ fi.TransitionStatus = lifecycle.TransitionComplete
+ fi.TransitionedObjName = destObj
+ fi.TransitionTier = opts.Transition.Tier
+ fi.TransitionVersionID = string(rv)
+ eventName := event.ObjectTransitionComplete
+
+ storageDisks := er.getDisks()
+
+ if err = er.deleteObjectVersion(ctx, bucket, object, fi, false); err != nil {
+ eventName = event.ObjectTransitionFailed
+ }
+
+ for _, disk := range storageDisks {
+ if disk != nil && disk.IsOnline() {
+ continue
+ }
+ er.addPartial(bucket, object, opts.VersionID)
+ break
+ }
+
+ objInfo := fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)
+ sendEvent(eventArgs{
+ EventName: eventName,
+ BucketName: bucket,
+ Object: objInfo,
+ UserAgent: "Internal: [ILM-Transition]",
+ Host: globalLocalNodeName,
+ })
+ tags := opts.LifecycleAuditEvent.Tags()
+ auditLogLifecycle(ctx, objInfo, ILMTransition, tags, traceFn)
+ return err
+}
+
+// RestoreTransitionedObject - restore transitioned object content locally on this cluster.
+// This is similar to PostObjectRestore from AWS GLACIER
+// storage class. When PostObjectRestore API is called, a temporary copy of the object
+// is restored locally to the bucket on source cluster until the restore expiry date.
+// The copy that was transitioned continues to reside in the transitioned tier.
+func (er erasureObjects) RestoreTransitionedObject(ctx context.Context, bucket, object string, opts ObjectOptions) error {
+ return er.restoreTransitionedObject(ctx, bucket, object, opts)
+}
+
+// update restore status header in the metadata
+func (er erasureObjects) updateRestoreMetadata(ctx context.Context, bucket, object string, objInfo ObjectInfo, opts ObjectOptions) error {
+ oi := objInfo.Clone()
+ oi.metadataOnly = true // Perform only metadata updates.
+
+ // allow retry in the case of failure to restore
+ delete(oi.UserDefined, xhttp.AmzRestore)
+
+ if _, err := er.CopyObject(ctx, bucket, object, bucket, object, oi, ObjectOptions{
+ VersionID: oi.VersionID,
+ }, ObjectOptions{
+ VersionID: oi.VersionID,
+ }); err != nil {
+ storageLogIf(ctx, fmt.Errorf("Unable to update transition restore metadata for %s/%s(%s): %s", bucket, object, oi.VersionID, err))
+ return err
+ }
+ return nil
+}
+
+// restoreTransitionedObject for multipart object chunks the file stream from remote tier into the same number of parts
+// as in the xl.meta for this version and rehydrates the part.n into the fi.DataDir for this version as in the xl.meta
+func (er erasureObjects) restoreTransitionedObject(ctx context.Context, bucket string, object string, opts ObjectOptions) error {
+ setRestoreHeaderFn := func(oi ObjectInfo, rerr error) error {
+ if rerr == nil {
+ return nil // nothing to do; restore object was successful
+ }
+ er.updateRestoreMetadata(ctx, bucket, object, oi, opts)
+ return rerr
+ }
+ var oi ObjectInfo
+ // get the file info on disk for transitioned object
+ actualfi, _, _, err := er.getObjectFileInfo(ctx, bucket, object, opts, false)
+ if err != nil {
+ return setRestoreHeaderFn(oi, toObjectErr(err, bucket, object))
+ }
+
+ oi = actualfi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)
+ ropts := putRestoreOpts(bucket, object, opts.Transition.RestoreRequest, oi)
+ if len(oi.Parts) == 1 {
+ var rs *HTTPRangeSpec
+ gr, err := getTransitionedObjectReader(ctx, bucket, object, rs, http.Header{}, oi, opts)
+ if err != nil {
+ return setRestoreHeaderFn(oi, toObjectErr(err, bucket, object))
+ }
+ defer gr.Close()
+ hashReader, err := hash.NewReader(ctx, gr, gr.ObjInfo.Size, "", "", gr.ObjInfo.Size)
+ if err != nil {
+ return setRestoreHeaderFn(oi, toObjectErr(err, bucket, object))
+ }
+ pReader := NewPutObjReader(hashReader)
+ _, err = er.PutObject(ctx, bucket, object, pReader, ropts)
+ return setRestoreHeaderFn(oi, toObjectErr(err, bucket, object))
+ }
+
+ res, err := er.NewMultipartUpload(ctx, bucket, object, ropts)
+ if err != nil {
+ return setRestoreHeaderFn(oi, err)
+ }
+
+ var uploadedParts []CompletePart
+ var rs *HTTPRangeSpec
+ // get reader from the warm backend - note that even in the case of encrypted objects, this stream is still encrypted.
+ gr, err := getTransitionedObjectReader(ctx, bucket, object, rs, http.Header{}, oi, opts)
+ if err != nil {
+ return setRestoreHeaderFn(oi, err)
+ }
+ defer gr.Close()
+
+ // rehydrate the parts back on disk as per the original xl.meta prior to transition
+ for _, partInfo := range oi.Parts {
+ hr, err := hash.NewReader(ctx, io.LimitReader(gr, partInfo.Size), partInfo.Size, "", "", partInfo.Size)
+ if err != nil {
+ return setRestoreHeaderFn(oi, err)
+ }
+ pInfo, err := er.PutObjectPart(ctx, bucket, object, res.UploadID, partInfo.Number, NewPutObjReader(hr), ObjectOptions{})
+ if err != nil {
+ return setRestoreHeaderFn(oi, err)
+ }
+ if pInfo.Size != partInfo.Size {
+ return setRestoreHeaderFn(oi, InvalidObjectState{Bucket: bucket, Object: object})
+ }
+ uploadedParts = append(uploadedParts, CompletePart{
+ PartNumber: pInfo.PartNumber,
+ ETag: pInfo.ETag,
+ })
+ }
+ _, err = er.CompleteMultipartUpload(ctx, bucket, object, res.UploadID, uploadedParts, ObjectOptions{
+ MTime: oi.ModTime,
+ })
+ return setRestoreHeaderFn(oi, err)
+}
+
+// DecomTieredObject - moves tiered object to another pool during decommissioning.
+func (er erasureObjects) DecomTieredObject(ctx context.Context, bucket, object string, fi FileInfo, opts ObjectOptions) error {
+ if opts.UserDefined == nil {
+ opts.UserDefined = make(map[string]string)
+ }
+ // overlay Erasure info for this set of disks
+ storageDisks := er.getDisks()
+ // Get parity and data drive count based on storage class metadata
+ parityDrives := globalStorageClass.GetParityForSC(opts.UserDefined[xhttp.AmzStorageClass])
+ if parityDrives < 0 {
+ parityDrives = er.defaultParityCount
+ }
+ dataDrives := len(storageDisks) - parityDrives
+
+ // we now know the number of blocks this object needs for data and parity.
+ // writeQuorum is dataBlocks + 1
+ writeQuorum := dataDrives
+ if dataDrives == parityDrives {
+ writeQuorum++
+ }
+
+ // Initialize parts metadata
+ partsMetadata := make([]FileInfo, len(storageDisks))
+
+ fi2 := newFileInfo(pathJoin(bucket, object), dataDrives, parityDrives)
+ fi.Erasure = fi2.Erasure
+ // Initialize erasure metadata.
+ for index := range partsMetadata {
+ partsMetadata[index] = fi
+ partsMetadata[index].Erasure.Index = index + 1
+ }
+
+ // Order disks according to erasure distribution
+ var onlineDisks []StorageAPI
+ onlineDisks, partsMetadata = shuffleDisksAndPartsMetadata(storageDisks, partsMetadata, fi)
+
+ if _, err := writeUniqueFileInfo(ctx, onlineDisks, "", bucket, object, partsMetadata, writeQuorum); err != nil {
+ return toObjectErr(err, bucket, object)
+ }
+
+ return nil
+}
diff --git a/cmd/erasure-object_test.go b/cmd/erasure-object_test.go
new file mode 100644
index 00000000..03f452f5
--- /dev/null
+++ b/cmd/erasure-object_test.go
@@ -0,0 +1,1283 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "crypto/md5"
+ crand "crypto/rand"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "testing"
+
+ "github.com/dustin/go-humanize"
+ "github.com/minio/minio/internal/config/storageclass"
+)
+
+func TestRepeatPutObjectPart(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ var objLayer ObjectLayer
+ var disks []string
+ var err error
+ var opts ObjectOptions
+
+ objLayer, disks, err = prepareErasure16(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // cleaning up of temporary test directories
+ defer objLayer.Shutdown(t.Context())
+ defer removeRoots(disks)
+
+ err = objLayer.MakeBucket(ctx, "bucket1", MakeBucketOptions{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ res, err := objLayer.NewMultipartUpload(ctx, "bucket1", "mpartObj1", opts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ fiveMBBytes := bytes.Repeat([]byte("a"), 5*humanize.MiByte)
+ md5Hex := getMD5Hash(fiveMBBytes)
+ _, err = objLayer.PutObjectPart(ctx, "bucket1", "mpartObj1", res.UploadID, 1, mustGetPutObjReader(t, bytes.NewReader(fiveMBBytes), 5*humanize.MiByte, md5Hex, ""), opts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // PutObjectPart should succeed even if part already exists. ref: https://github.com/minio/minio/issues/1930
+ _, err = objLayer.PutObjectPart(ctx, "bucket1", "mpartObj1", res.UploadID, 1, mustGetPutObjReader(t, bytes.NewReader(fiveMBBytes), 5*humanize.MiByte, md5Hex, ""), opts)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestErasureDeleteObjectBasic(t *testing.T) {
+ testCases := []struct {
+ bucket string
+ object string
+ expectedErr error
+ }{
+ {".test", "dir/obj", BucketNameInvalid{Bucket: ".test"}},
+ {"----", "dir/obj", BucketNameInvalid{Bucket: "----"}},
+ {"bucket", "", ObjectNameInvalid{Bucket: "bucket", Object: ""}},
+ {"bucket", "doesnotexist", ObjectNotFound{Bucket: "bucket", Object: "doesnotexist"}},
+ {"bucket", "dir/doesnotexist", ObjectNotFound{Bucket: "bucket", Object: "dir/doesnotexist"}},
+ {"bucket", "dir", ObjectNotFound{Bucket: "bucket", Object: "dir"}},
+ {"bucket", "dir/", ObjectNotFound{Bucket: "bucket", Object: "dir/"}},
+ {"bucket", "dir/obj", nil},
+ }
+
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ // Create an instance of xl backend
+ xl, fsDirs, err := prepareErasure16(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer xl.Shutdown(t.Context())
+
+ err = xl.MakeBucket(ctx, "bucket", MakeBucketOptions{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Create object "dir/obj" under bucket "bucket" for Test 7 to pass
+ _, err = xl.PutObject(ctx, "bucket", "dir/obj", mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), ObjectOptions{})
+ if err != nil {
+ t.Fatalf("Erasure Object upload failed: %s", err)
+ }
+ for _, test := range testCases {
+ t.Run("", func(t *testing.T) {
+ _, err := xl.GetObjectInfo(ctx, "bucket", "dir/obj", ObjectOptions{})
+ if err != nil {
+ t.Fatal("dir/obj not found before last test")
+ }
+ _, actualErr := xl.DeleteObject(ctx, test.bucket, test.object, ObjectOptions{})
+ if test.expectedErr != nil && actualErr != test.expectedErr {
+ t.Errorf("Expected to fail with %s, but failed with %s", test.expectedErr, actualErr)
+ }
+ if test.expectedErr == nil && actualErr != nil {
+ t.Errorf("Expected to pass, but failed with %s", actualErr)
+ }
+ })
+ }
+ // Cleanup backend directories
+ removeRoots(fsDirs)
+}
+
+func TestDeleteObjectsVersionedTwoPools(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ 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: %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) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ obj, fsDirs, err := prepareErasure(ctx, 16)
+ if err != nil {
+ t.Fatal("Unable to initialize 'Erasure' object layer.", err)
+ }
+ // Remove all dirs.
+ for _, dir := range fsDirs {
+ defer os.RemoveAll(dir)
+ }
+
+ type testCaseType struct {
+ bucket string
+ object string
+ }
+
+ bucketName := "bucket"
+ testCases := []testCaseType{
+ {bucketName, "dir/obj1"},
+ {bucketName, "dir/obj1"},
+ }
+
+ err = obj.MakeBucket(ctx, bucketName, MakeBucketOptions{
+ VersioningEnabled: true,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ names := make([]ObjectToDelete, len(testCases))
+ for i, testCase := range testCases {
+ objInfo, err := obj.PutObject(ctx, testCase.bucket, testCase.object,
+ mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), ObjectOptions{
+ Versioned: true,
+ })
+ if err != nil {
+ t.Fatalf("Erasure Object upload failed: %s", err)
+ }
+ names[i] = ObjectToDelete{
+ ObjectV: ObjectV{
+ ObjectName: objInfo.Name,
+ VersionID: objInfo.VersionID,
+ },
+ }
+ }
+ names = append(names, ObjectToDelete{
+ ObjectV: ObjectV{
+ ObjectName: "dir/obj1",
+ VersionID: mustGetUUID(), // add a non-existent UUID.
+ },
+ })
+
+ _, delErrs := obj.DeleteObjects(ctx, bucketName, names, ObjectOptions{
+ Versioned: true,
+ })
+ for i := range delErrs {
+ if delErrs[i] != nil {
+ t.Errorf("Failed to remove object `%v` with the error: `%v`", names[i], delErrs[i])
+ }
+ }
+
+ for i, test := range testCases {
+ _, statErr := obj.GetObjectInfo(ctx, test.bucket, test.object, ObjectOptions{
+ VersionID: names[i].VersionID,
+ })
+ switch statErr.(type) {
+ case VersionNotFound:
+ default:
+ t.Fatalf("Object %s is not removed", test.bucket+SlashSeparator+test.object)
+ }
+ }
+
+ if _, err = os.ReadFile(pathJoin(fsDirs[0], bucketName, "dir/obj1", "xl.meta")); err == nil {
+ t.Fatalf("xl.meta still present after removal")
+ }
+}
+
+func TestErasureDeleteObjectsErasureSet(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ obj, fsDirs, err := prepareErasureSets32(ctx)
+ if err != nil {
+ t.Fatal("Unable to initialize 'Erasure' object layer.", err)
+ }
+
+ setObjectLayer(obj)
+ initConfigSubsystem(ctx, obj)
+
+ // Remove all dirs.
+ for _, dir := range fsDirs {
+ defer os.RemoveAll(dir)
+ }
+
+ type testCaseType struct {
+ bucket string
+ object string
+ }
+
+ bucketName := "bucket"
+ testCases := []testCaseType{
+ {bucketName, "dir/obj1"},
+ {bucketName, "dir/obj2"},
+ {bucketName, "obj3"},
+ {bucketName, "obj_4"},
+ }
+
+ if err = obj.MakeBucket(ctx, bucketName, MakeBucketOptions{}); err != nil {
+ t.Fatal(err)
+ }
+
+ for _, testCase := range testCases {
+ _, err = obj.PutObject(ctx, testCase.bucket, testCase.object,
+ mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), ObjectOptions{})
+ if err != nil {
+ t.Fatalf("Erasure Object upload failed: %s", err)
+ }
+ }
+
+ toObjectNames := func(testCases []testCaseType) []ObjectToDelete {
+ names := make([]ObjectToDelete, len(testCases))
+ for i := range testCases {
+ names[i] = ObjectToDelete{
+ ObjectV: ObjectV{
+ ObjectName: testCases[i].object,
+ },
+ }
+ }
+ return names
+ }
+
+ objectNames := toObjectNames(testCases)
+ _, delErrs := obj.DeleteObjects(ctx, bucketName, objectNames, ObjectOptions{})
+
+ for i := range delErrs {
+ if delErrs[i] != nil {
+ t.Errorf("Failed to remove object `%v` with the error: `%v`", objectNames[i], delErrs[i])
+ }
+ }
+
+ for _, test := range testCases {
+ _, statErr := obj.GetObjectInfo(ctx, test.bucket, test.object, ObjectOptions{})
+ switch statErr.(type) {
+ case ObjectNotFound:
+ default:
+ t.Fatalf("Object %s is not removed", test.bucket+SlashSeparator+test.object)
+ }
+ }
+}
+
+func TestErasureDeleteObjectDiskNotFound(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ // Create an instance of xl backend.
+ obj, fsDirs, err := prepareErasure16(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Cleanup backend directories
+ defer obj.Shutdown(t.Context())
+ defer removeRoots(fsDirs)
+
+ z := obj.(*erasureServerPools)
+ xl := z.serverPools[0].sets[0]
+
+ // Create "bucket"
+ err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ bucket := "bucket"
+ object := "object"
+ opts := ObjectOptions{}
+ // Create object "obj" under bucket "bucket".
+ _, err = obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), opts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ erasureDisks := xl.getDisks()
+ z.serverPools[0].erasureDisksMu.Lock()
+ xl.getDisks = func() []StorageAPI {
+ for i := range erasureDisks[:6] {
+ erasureDisks[i] = newNaughtyDisk(erasureDisks[i], nil, errFaultyDisk)
+ }
+ return erasureDisks
+ }
+
+ z.serverPools[0].erasureDisksMu.Unlock()
+ _, err = obj.DeleteObject(ctx, bucket, object, ObjectOptions{})
+ if !errors.Is(err, errErasureWriteQuorum) {
+ t.Fatal(err)
+ }
+
+ // Create "obj" under "bucket".
+ _, err = obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), opts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Remove one more disk to 'lose' quorum, by taking 2 more drives offline.
+ erasureDisks = xl.getDisks()
+ z.serverPools[0].erasureDisksMu.Lock()
+ xl.getDisks = func() []StorageAPI {
+ erasureDisks[7] = nil
+ erasureDisks[8] = nil
+ return erasureDisks
+ }
+
+ z.serverPools[0].erasureDisksMu.Unlock()
+ _, err = obj.DeleteObject(ctx, bucket, object, ObjectOptions{})
+ // since majority of disks are not available, metaquorum is not achieved and hence errErasureWriteQuorum error
+ if !errors.Is(err, errErasureWriteQuorum) {
+ t.Errorf("Expected deleteObject to fail with %v, but failed with %v", toObjectErr(errErasureWriteQuorum, bucket, object), err)
+ }
+}
+
+func TestErasureDeleteObjectDiskNotFoundErasure4(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ // Create an instance of xl backend.
+ obj, fsDirs, err := prepareErasure16(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Cleanup backend directories
+ defer obj.Shutdown(t.Context())
+ defer removeRoots(fsDirs)
+
+ z := obj.(*erasureServerPools)
+ xl := z.serverPools[0].sets[0]
+
+ // Create "bucket"
+ err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ bucket := "bucket"
+ object := "object"
+ opts := ObjectOptions{}
+ // Create object "obj" under bucket "bucket".
+ _, err = obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), opts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Upload a good object
+ _, err = obj.DeleteObject(ctx, bucket, object, ObjectOptions{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Create "obj" under "bucket".
+ _, err = obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), opts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Remove disks to 'lose' quorum for object, by setting 5 to nil.
+ erasureDisks := xl.getDisks()
+ z.serverPools[0].erasureDisksMu.Lock()
+ xl.getDisks = func() []StorageAPI {
+ for i := range erasureDisks[:5] {
+ erasureDisks[i] = newNaughtyDisk(erasureDisks[i], nil, errFaultyDisk)
+ }
+ return erasureDisks
+ }
+
+ z.serverPools[0].erasureDisksMu.Unlock()
+ _, err = obj.DeleteObject(ctx, bucket, object, ObjectOptions{})
+ // since majority of disks are not available, metaquorum is not achieved and hence errErasureWriteQuorum error
+ if !errors.Is(err, errErasureWriteQuorum) {
+ t.Errorf("Expected deleteObject to fail with %v, but failed with %v", toObjectErr(errErasureWriteQuorum, bucket, object), err)
+ }
+}
+
+func TestErasureDeleteObjectDiskNotFoundErr(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ // Create an instance of xl backend.
+ obj, fsDirs, err := prepareErasure16(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Cleanup backend directories
+ defer obj.Shutdown(t.Context())
+ defer removeRoots(fsDirs)
+
+ z := obj.(*erasureServerPools)
+ xl := z.serverPools[0].sets[0]
+
+ // Create "bucket"
+ err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ bucket := "bucket"
+ object := "object"
+ opts := ObjectOptions{}
+ // Create object "obj" under bucket "bucket".
+ _, err = obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), opts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // for a 16 disk setup, EC is 4, but will be upgraded up to 8.
+ // Remove 4 disks.
+ erasureDisks := xl.getDisks()
+ z.serverPools[0].erasureDisksMu.Lock()
+ xl.getDisks = func() []StorageAPI {
+ for i := range erasureDisks[:4] {
+ erasureDisks[i] = newNaughtyDisk(erasureDisks[i], nil, errFaultyDisk)
+ }
+ return erasureDisks
+ }
+
+ z.serverPools[0].erasureDisksMu.Unlock()
+ _, err = obj.DeleteObject(ctx, bucket, object, ObjectOptions{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Create "obj" under "bucket".
+ _, err = obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), opts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Object was uploaded with 4 known bad drives, so we should still be able to lose 3 drives and still write to the object.
+ erasureDisks = xl.getDisks()
+ z.serverPools[0].erasureDisksMu.Lock()
+ xl.getDisks = func() []StorageAPI {
+ erasureDisks[7] = nil
+ erasureDisks[8] = nil
+ erasureDisks[9] = nil
+ return erasureDisks
+ }
+
+ z.serverPools[0].erasureDisksMu.Unlock()
+ _, err = obj.DeleteObject(ctx, bucket, object, ObjectOptions{})
+ // since majority of disks are available, metaquorum achieved.
+ if err != nil {
+ t.Errorf("Expected deleteObject to not fail, but failed with %v", err)
+ }
+}
+
+func TestGetObjectNoQuorum(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ // Create an instance of xl backend.
+ obj, fsDirs, err := prepareErasure16(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Cleanup backend directories.
+ defer obj.Shutdown(t.Context())
+ defer removeRoots(fsDirs)
+
+ z := obj.(*erasureServerPools)
+ xl := z.serverPools[0].sets[0]
+
+ // Create "bucket"
+ err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ bucket := "bucket"
+ object := "object"
+ opts := ObjectOptions{}
+ buf := make([]byte, smallFileThreshold*16)
+ if _, err = io.ReadFull(crand.Reader, buf); err != nil {
+ t.Fatal(err)
+ }
+
+ // Test use case 1: All disks are online, xl.meta are present, but data are missing
+ _, err = obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader(buf), int64(len(buf)), "", ""), opts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, disk := range xl.getDisks() {
+ files, _ := disk.ListDir(ctx, "", bucket, object, -1)
+ for _, file := range files {
+ if file != "xl.meta" {
+ disk.Delete(ctx, bucket, pathJoin(object, file), DeleteOptions{
+ Recursive: true,
+ Immediate: false,
+ })
+ }
+ }
+ }
+
+ gr, err := xl.GetObjectNInfo(ctx, bucket, object, nil, nil, opts)
+ if err != nil {
+ if err != toObjectErr(errErasureReadQuorum, bucket, object) {
+ t.Errorf("Expected GetObject to fail with %v, but failed with %v", toObjectErr(errErasureReadQuorum, bucket, object), err)
+ }
+ }
+ if gr != nil {
+ _, err = io.Copy(io.Discard, gr)
+ if err != toObjectErr(errErasureReadQuorum, bucket, object) {
+ t.Errorf("Expected GetObject to fail with %v, but failed with %v", toObjectErr(errErasureReadQuorum, bucket, object), err)
+ }
+ gr.Close()
+ }
+
+ // Test use case 2: Make 9 disks offline, which leaves less than quorum number of disks
+ // in a 16 disk Erasure setup. The original disks are 'replaced' with
+ // naughtyDisks that fail after 'f' successful StorageAPI method
+ // invocations, where f - [0,2)
+
+ // Create "object" under "bucket".
+ _, err = obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader(buf), int64(len(buf)), "", ""), opts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for f := range 2 {
+ diskErrors := make(map[int]error)
+ for i := 0; i <= f; i++ {
+ diskErrors[i] = nil
+ }
+ erasureDisks := xl.getDisks()
+ for i := range erasureDisks[:9] {
+ switch diskType := erasureDisks[i].(type) {
+ case *naughtyDisk:
+ erasureDisks[i] = newNaughtyDisk(diskType.disk, diskErrors, errFaultyDisk)
+ default:
+ erasureDisks[i] = newNaughtyDisk(erasureDisks[i], diskErrors, errFaultyDisk)
+ }
+ }
+ z.serverPools[0].erasureDisksMu.Lock()
+ xl.getDisks = func() []StorageAPI {
+ return erasureDisks
+ }
+ z.serverPools[0].erasureDisksMu.Unlock()
+ // Fetch object from store.
+ gr, err := xl.GetObjectNInfo(ctx, bucket, object, nil, nil, opts)
+ if err != nil {
+ if err != toObjectErr(errErasureReadQuorum, bucket, object) {
+ t.Errorf("Expected GetObject to fail with %v, but failed with %v", toObjectErr(errErasureReadQuorum, bucket, object), err)
+ }
+ }
+ if gr != nil {
+ _, err = io.Copy(io.Discard, gr)
+ if err != toObjectErr(errErasureReadQuorum, bucket, object) {
+ t.Errorf("Expected GetObject to fail with %v, but failed with %v", toObjectErr(errErasureReadQuorum, bucket, object), err)
+ }
+ gr.Close()
+ }
+ }
+}
+
+func TestHeadObjectNoQuorum(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ // Create an instance of xl backend.
+ obj, fsDirs, err := prepareErasure16(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Cleanup backend directories.
+ defer obj.Shutdown(t.Context())
+ defer removeRoots(fsDirs)
+
+ z := obj.(*erasureServerPools)
+ xl := z.serverPools[0].sets[0]
+
+ // Create "bucket"
+ err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ bucket := "bucket"
+ object := "object"
+ opts := ObjectOptions{}
+
+ // Test use case 1: All disks are online, xl.meta are present, but data are missing
+ _, err = obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), opts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, disk := range xl.getDisks() {
+ files, _ := disk.ListDir(ctx, "", bucket, object, -1)
+ for _, file := range files {
+ if file != "xl.meta" {
+ disk.Delete(ctx, bucket, pathJoin(object, file), DeleteOptions{
+ Recursive: true,
+ Immediate: false,
+ })
+ }
+ }
+ }
+
+ _, err = xl.GetObjectInfo(ctx, bucket, object, opts)
+ if err != nil {
+ t.Errorf("Expected StatObject to succeed if data dir are not found, but failed with %v", err)
+ }
+
+ // Test use case 2: Make 9 disks offline, which leaves less than quorum number of disks
+ // in a 16 disk Erasure setup. The original disks are 'replaced' with
+ // naughtyDisks that fail after 'f' successful StorageAPI method
+ // invocations, where f - [0,2)
+
+ // Create "object" under "bucket".
+ _, err = obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), opts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ erasureDisks := xl.getDisks()
+ for i := range erasureDisks[:10] {
+ erasureDisks[i] = nil
+ }
+
+ z.serverPools[0].erasureDisksMu.Lock()
+ xl.getDisks = func() []StorageAPI {
+ return erasureDisks
+ }
+ z.serverPools[0].erasureDisksMu.Unlock()
+
+ // Fetch object from store.
+ _, err = xl.GetObjectInfo(ctx, bucket, object, opts)
+ if err != toObjectErr(errErasureReadQuorum, bucket, object) {
+ t.Errorf("Expected getObjectInfo to fail with %v, but failed with %v", toObjectErr(errErasureWriteQuorum, bucket, object), err)
+ }
+}
+
+func TestPutObjectNoQuorum(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ // Create an instance of xl backend.
+ obj, fsDirs, err := prepareErasure16(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Cleanup backend directories.
+ defer obj.Shutdown(t.Context())
+ defer removeRoots(fsDirs)
+
+ z := obj.(*erasureServerPools)
+ xl := z.serverPools[0].sets[0]
+
+ // Create "bucket"
+ err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ bucket := "bucket"
+ object := "object"
+ opts := ObjectOptions{}
+ // Create "object" under "bucket".
+ _, err = obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader(bytes.Repeat([]byte{'a'}, smallFileThreshold*16)), smallFileThreshold*16, "", ""), opts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Make 9 disks offline, which leaves less than quorum number of disks
+ // in a 16 disk Erasure setup. The original disks are 'replaced' with
+ // naughtyDisks that fail after 'f' successful StorageAPI method
+ // invocations, where f - [0,4)
+ for f := range 2 {
+ diskErrors := make(map[int]error)
+ for i := 0; i <= f; i++ {
+ diskErrors[i] = nil
+ }
+ erasureDisks := xl.getDisks()
+ for i := range erasureDisks[:9] {
+ switch diskType := erasureDisks[i].(type) {
+ case *naughtyDisk:
+ erasureDisks[i] = newNaughtyDisk(diskType.disk, diskErrors, errFaultyDisk)
+ default:
+ erasureDisks[i] = newNaughtyDisk(erasureDisks[i], diskErrors, errFaultyDisk)
+ }
+ }
+ z.serverPools[0].erasureDisksMu.Lock()
+ xl.getDisks = func() []StorageAPI {
+ return erasureDisks
+ }
+ z.serverPools[0].erasureDisksMu.Unlock()
+ // Upload new content to same object "object"
+ _, err = obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader(bytes.Repeat([]byte{byte(f)}, smallFileThreshold*16)), smallFileThreshold*16, "", ""), opts)
+ if !errors.Is(err, errErasureWriteQuorum) {
+ t.Errorf("Expected putObject to fail with %v, but failed with %v", toObjectErr(errErasureWriteQuorum, bucket, object), err)
+ }
+ }
+}
+
+func TestPutObjectNoQuorumSmall(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ // Create an instance of xl backend.
+ obj, fsDirs, err := prepareErasure16(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Cleanup backend directories.
+ defer obj.Shutdown(t.Context())
+ defer removeRoots(fsDirs)
+
+ z := obj.(*erasureServerPools)
+ xl := z.serverPools[0].sets[0]
+
+ // Create "bucket"
+ err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ bucket := "bucket"
+ object := "object"
+ opts := ObjectOptions{}
+ // Create "object" under "bucket".
+ _, err = obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader(bytes.Repeat([]byte{'a'}, smallFileThreshold/2)), smallFileThreshold/2, "", ""), opts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Make 9 disks offline, which leaves less than quorum number of disks
+ // in a 16 disk Erasure setup. The original disks are 'replaced' with
+ // naughtyDisks that fail after 'f' successful StorageAPI method
+ // invocations, where f - [0,2)
+ for f := range 2 {
+ t.Run("exec-"+strconv.Itoa(f), func(t *testing.T) {
+ diskErrors := make(map[int]error)
+ for i := 0; i <= f; i++ {
+ diskErrors[i] = nil
+ }
+ erasureDisks := xl.getDisks()
+ for i := range erasureDisks[:9] {
+ switch diskType := erasureDisks[i].(type) {
+ case *naughtyDisk:
+ erasureDisks[i] = newNaughtyDisk(diskType.disk, diskErrors, errFaultyDisk)
+ default:
+ erasureDisks[i] = newNaughtyDisk(erasureDisks[i], diskErrors, errFaultyDisk)
+ }
+ }
+ z.serverPools[0].erasureDisksMu.Lock()
+ xl.getDisks = func() []StorageAPI {
+ return erasureDisks
+ }
+ z.serverPools[0].erasureDisksMu.Unlock()
+ // Upload new content to same object "object"
+ _, err = obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader(bytes.Repeat([]byte{byte(f)}, smallFileThreshold/2)), smallFileThreshold/2, "", ""), opts)
+ if !errors.Is(err, errErasureWriteQuorum) {
+ t.Errorf("Expected putObject to fail with %v, but failed with %v", toObjectErr(errErasureWriteQuorum, bucket, object), err)
+ }
+ })
+ }
+}
+
+// Test PutObject twice, one small and another bigger
+// than small data threshold and checks reading them again
+func TestPutObjectSmallInlineData(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ const numberOfDisks = 4
+
+ // Create an instance of xl backend.
+ obj, fsDirs, err := prepareErasure(ctx, numberOfDisks)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Cleanup backend directories.
+ defer obj.Shutdown(t.Context())
+ defer removeRoots(fsDirs)
+
+ bucket := "bucket"
+ object := "object"
+
+ // Create "bucket"
+ err = obj.MakeBucket(ctx, bucket, MakeBucketOptions{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Test: Upload a small file and read it.
+ smallData := []byte{'a'}
+ _, err = obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader(smallData), int64(len(smallData)), "", ""), ObjectOptions{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ gr, err := obj.GetObjectNInfo(ctx, bucket, object, nil, nil, ObjectOptions{})
+ if err != nil {
+ t.Fatalf("Expected GetObject to succeed, but failed with %v", err)
+ }
+ output := bytes.NewBuffer([]byte{})
+ _, err = io.Copy(output, gr)
+ if err != nil {
+ t.Fatalf("Expected GetObject reading data to succeed, but failed with %v", err)
+ }
+ gr.Close()
+ if !bytes.Equal(output.Bytes(), smallData) {
+ t.Fatalf("Corrupted data is found")
+ }
+
+ // Test: Upload a file bigger than the small file threshold
+ // under the same bucket & key name and try to read it again.
+
+ output.Reset()
+ bigData := bytes.Repeat([]byte{'b'}, smallFileThreshold*numberOfDisks/2)
+
+ _, err = obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader(bigData), int64(len(bigData)), "", ""), ObjectOptions{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ gr, err = obj.GetObjectNInfo(ctx, bucket, object, nil, nil, ObjectOptions{})
+ if err != nil {
+ t.Fatalf("Expected GetObject to succeed, but failed with %v", err)
+ }
+ _, err = io.Copy(output, gr)
+ if err != nil {
+ t.Fatalf("Expected GetObject reading data to succeed, but failed with %v", err)
+ }
+ gr.Close()
+ if !bytes.Equal(output.Bytes(), bigData) {
+ t.Fatalf("Corrupted data found")
+ }
+}
+
+func TestObjectQuorumFromMeta(t *testing.T) {
+ ExecObjectLayerTestWithDirs(t, testObjectQuorumFromMeta)
+}
+
+func testObjectQuorumFromMeta(obj ObjectLayer, instanceType string, dirs []string, t TestErrHandler) {
+ bucket := getRandomBucketName()
+
+ var opts ObjectOptions
+ // make data with more than one part
+ partCount := 3
+ data := bytes.Repeat([]byte("a"), 6*1024*1024*partCount)
+
+ z := obj.(*erasureServerPools)
+ xl := z.serverPools[0].sets[0]
+ erasureDisks := xl.getDisks()
+
+ ctx, cancel := context.WithCancel(GlobalContext)
+ defer cancel()
+
+ err := obj.MakeBucket(ctx, bucket, MakeBucketOptions{})
+ if err != nil {
+ t.Fatalf("Failed to make a bucket %v", err)
+ }
+
+ // Object for test case 1 - No StorageClass defined, no MetaData in PutObject
+ object1 := "object1"
+ globalStorageClass.Update(storageclass.Config{
+ RRS: storageclass.StorageClass{
+ Parity: 2,
+ },
+ Standard: storageclass.StorageClass{
+ Parity: 4,
+ },
+ })
+ _, err = obj.PutObject(ctx, bucket, object1, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), opts)
+ if err != nil {
+ t.Fatalf("Failed to putObject %v", err)
+ }
+
+ parts1, errs1 := readAllFileInfo(ctx, erasureDisks, "", bucket, object1, "", false, false)
+ parts1SC := globalStorageClass
+
+ // Object for test case 2 - No StorageClass defined, MetaData in PutObject requesting RRS Class
+ object2 := "object2"
+ metadata2 := make(map[string]string)
+ metadata2["x-amz-storage-class"] = storageclass.RRS
+ _, err = obj.PutObject(ctx, bucket, object2, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), ObjectOptions{UserDefined: metadata2})
+ if err != nil {
+ t.Fatalf("Failed to putObject %v", err)
+ }
+
+ parts2, errs2 := readAllFileInfo(ctx, erasureDisks, "", bucket, object2, "", false, false)
+ parts2SC := globalStorageClass
+
+ // Object for test case 3 - No StorageClass defined, MetaData in PutObject requesting Standard Storage Class
+ object3 := "object3"
+ metadata3 := make(map[string]string)
+ metadata3["x-amz-storage-class"] = storageclass.STANDARD
+ _, err = obj.PutObject(ctx, bucket, object3, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), ObjectOptions{UserDefined: metadata3})
+ if err != nil {
+ t.Fatalf("Failed to putObject %v", err)
+ }
+
+ parts3, errs3 := readAllFileInfo(ctx, erasureDisks, "", bucket, object3, "", false, false)
+ parts3SC := globalStorageClass
+
+ // Object for test case 4 - Standard StorageClass defined as Parity 6, MetaData in PutObject requesting Standard Storage Class
+ object4 := "object4"
+ metadata4 := make(map[string]string)
+ metadata4["x-amz-storage-class"] = storageclass.STANDARD
+ globalStorageClass.Update(storageclass.Config{
+ Standard: storageclass.StorageClass{
+ Parity: 6,
+ },
+ })
+
+ _, err = obj.PutObject(ctx, bucket, object4, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), ObjectOptions{UserDefined: metadata4})
+ if err != nil {
+ t.Fatalf("Failed to putObject %v", err)
+ }
+
+ parts4, errs4 := readAllFileInfo(ctx, erasureDisks, "", bucket, object4, "", false, false)
+ parts4SC := storageclass.Config{
+ Standard: storageclass.StorageClass{
+ Parity: 6,
+ },
+ }
+
+ // Object for test case 5 - RRS StorageClass defined as Parity 2, MetaData in PutObject requesting RRS Class
+ // Reset global storage class flags
+ object5 := "object5"
+ metadata5 := make(map[string]string)
+ metadata5["x-amz-storage-class"] = storageclass.RRS
+ globalStorageClass.Update(storageclass.Config{
+ RRS: storageclass.StorageClass{
+ Parity: 2,
+ },
+ })
+
+ _, err = obj.PutObject(ctx, bucket, object5, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), ObjectOptions{UserDefined: metadata5})
+ if err != nil {
+ t.Fatalf("Failed to putObject %v", err)
+ }
+
+ parts5, errs5 := readAllFileInfo(ctx, erasureDisks, "", bucket, object5, "", false, false)
+ parts5SC := globalStorageClass
+
+ // Object for test case 6 - RRS StorageClass defined as Parity 2, MetaData in PutObject requesting Standard Storage Class
+ object6 := "object6"
+ metadata6 := make(map[string]string)
+ metadata6["x-amz-storage-class"] = storageclass.STANDARD
+ globalStorageClass.Update(storageclass.Config{
+ Standard: storageclass.StorageClass{
+ Parity: 4,
+ },
+ RRS: storageclass.StorageClass{
+ Parity: 2,
+ },
+ })
+
+ _, err = obj.PutObject(ctx, bucket, object6, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), ObjectOptions{UserDefined: metadata6})
+ if err != nil {
+ t.Fatalf("Failed to putObject %v", err)
+ }
+
+ parts6, errs6 := readAllFileInfo(ctx, erasureDisks, "", bucket, object6, "", false, false)
+ parts6SC := storageclass.Config{
+ RRS: storageclass.StorageClass{
+ Parity: 2,
+ },
+ }
+
+ // Object for test case 7 - Standard StorageClass defined as Parity 5, MetaData in PutObject requesting RRS Class
+ // Reset global storage class flags
+ object7 := "object7"
+ metadata7 := make(map[string]string)
+ metadata7["x-amz-storage-class"] = storageclass.STANDARD
+ globalStorageClass.Update(storageclass.Config{
+ Standard: storageclass.StorageClass{
+ Parity: 5,
+ },
+ })
+
+ _, err = obj.PutObject(ctx, bucket, object7, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), ObjectOptions{UserDefined: metadata7})
+ if err != nil {
+ t.Fatalf("Failed to putObject %v", err)
+ }
+
+ parts7, errs7 := readAllFileInfo(ctx, erasureDisks, "", bucket, object7, "", false, false)
+ parts7SC := storageclass.Config{
+ Standard: storageclass.StorageClass{
+ Parity: 5,
+ },
+ }
+
+ tests := []struct {
+ parts []FileInfo
+ errs []error
+ expectedReadQuorum int
+ expectedWriteQuorum int
+ storageClassCfg storageclass.Config
+ expectedError error
+ }{
+ {parts1, errs1, 12, 12, parts1SC, nil},
+ {parts2, errs2, 14, 14, parts2SC, nil},
+ {parts3, errs3, 12, 12, parts3SC, nil},
+ {parts4, errs4, 10, 10, parts4SC, nil},
+ {parts5, errs5, 14, 14, parts5SC, nil},
+ {parts6, errs6, 12, 12, parts6SC, nil},
+ {parts7, errs7, 11, 11, parts7SC, nil},
+ }
+ for _, tt := range tests {
+ t.(*testing.T).Run("", func(t *testing.T) {
+ globalStorageClass.Update(tt.storageClassCfg)
+ actualReadQuorum, actualWriteQuorum, err := objectQuorumFromMeta(ctx, tt.parts, tt.errs, storageclass.DefaultParityBlocks(len(erasureDisks)))
+ if tt.expectedError != nil && err == nil {
+ t.Errorf("Expected %s, got %s", tt.expectedError, err)
+ }
+ if tt.expectedError == nil && err != nil {
+ t.Errorf("Expected %s, got %s", tt.expectedError, err)
+ }
+ if tt.expectedReadQuorum != actualReadQuorum {
+ t.Errorf("Expected Read Quorum %d, got %d", tt.expectedReadQuorum, actualReadQuorum)
+ }
+ if tt.expectedWriteQuorum != actualWriteQuorum {
+ t.Errorf("Expected Write Quorum %d, got %d", tt.expectedWriteQuorum, actualWriteQuorum)
+ }
+ })
+ }
+}
+
+// In some deployments, one object has data inlined in one disk and not inlined in other disks.
+func TestGetObjectInlineNotInline(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ // Create a backend with 4 disks named disk{1...4}, this name convention
+ // because we will unzip some object data from a sample archive.
+ const numDisks = 4
+ path := t.TempDir()
+
+ var fsDirs []string
+ for i := 1; i <= numDisks; i++ {
+ fsDirs = append(fsDirs, filepath.Join(path, fmt.Sprintf("disk%d", i)))
+ }
+
+ objLayer, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, fsDirs...))
+ if err != nil {
+ removeRoots(fsDirs)
+ t.Fatal(err)
+ }
+
+ // cleaning up of temporary test directories
+ defer objLayer.Shutdown(t.Context())
+ defer removeRoots(fsDirs)
+
+ // Create a testbucket
+ err = objLayer.MakeBucket(ctx, "testbucket", MakeBucketOptions{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Unzip sample object data to the existing disks
+ err = unzipArchive("testdata/xl-meta-inline-notinline.zip", path)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Try to read the object and check its md5sum
+ gr, err := objLayer.GetObjectNInfo(ctx, "testbucket", "file", nil, nil, ObjectOptions{})
+ if err != nil {
+ t.Fatalf("Expected GetObject to succeed, but failed with %v", err)
+ }
+
+ h := md5.New()
+ _, err = io.Copy(h, gr)
+ if err != nil {
+ t.Fatalf("Expected GetObject reading data to succeed, but failed with %v", err)
+ }
+ gr.Close()
+
+ const expectedHash = "fffb6377948ebea75ad2b8058e849ef5"
+ foundHash := fmt.Sprintf("%x", h.Sum(nil))
+ if foundHash != expectedHash {
+ t.Fatalf("Expected data to have md5sum = `%s`, found `%s`", expectedHash, foundHash)
+ }
+}
+
+// Test reading an object with some outdated data in some disks
+func TestGetObjectWithOutdatedDisks(t *testing.T) {
+ if runtime.GOOS == globalWindowsOSName {
+ t.Skip()
+ }
+
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ // Create an instance of xl backend.
+ obj, fsDirs, err := prepareErasure(ctx, 6)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Cleanup backend directories.
+ defer obj.Shutdown(t.Context())
+ defer removeRoots(fsDirs)
+
+ z := obj.(*erasureServerPools)
+ sets := z.serverPools[0]
+ xl := sets.sets[0]
+
+ origErasureDisks := xl.getDisks()
+
+ testCases := []struct {
+ bucket string
+ versioned bool
+ object string
+ content []byte
+ }{
+ {"bucket1", false, "object1", []byte("aaaaaaaaaaaaaaaa")},
+ {"bucket2", false, "object2", bytes.Repeat([]byte{'a'}, smallFileThreshold*2)},
+ {"bucket3", true, "version1", []byte("aaaaaaaaaaaaaaaa")},
+ {"bucket4", true, "version2", bytes.Repeat([]byte{'a'}, smallFileThreshold*2)},
+ }
+
+ for i, testCase := range testCases {
+ // Step 1: create a bucket
+ err = z.MakeBucket(ctx, testCase.bucket, MakeBucketOptions{VersioningEnabled: testCase.versioned})
+ if err != nil {
+ t.Fatalf("Test %d: Failed to create a bucket: %v", i+1, err)
+ }
+
+ // Step 2: Upload an object with a random content
+ initialData := bytes.Repeat([]byte{'b'}, len(testCase.content))
+ sets.erasureDisksMu.Lock()
+ xl.getDisks = func() []StorageAPI { return origErasureDisks }
+ sets.erasureDisksMu.Unlock()
+ _, err = z.PutObject(ctx, testCase.bucket, testCase.object, mustGetPutObjReader(t, bytes.NewReader(initialData), int64(len(initialData)), "", ""),
+ ObjectOptions{Versioned: testCase.versioned})
+ if err != nil {
+ t.Fatalf("Test %d: Failed to upload a random object: %v", i+1, err)
+ }
+
+ // Step 3: Upload the object with some disks offline
+ sets.erasureDisksMu.Lock()
+ xl.getDisks = func() []StorageAPI {
+ disks := make([]StorageAPI, len(origErasureDisks))
+ copy(disks, origErasureDisks)
+ disks[0] = nil
+ disks[1] = nil
+ return disks
+ }
+ sets.erasureDisksMu.Unlock()
+ got, err := z.PutObject(ctx, testCase.bucket, testCase.object, mustGetPutObjReader(t, bytes.NewReader(testCase.content), int64(len(testCase.content)), "", ""),
+ ObjectOptions{Versioned: testCase.versioned})
+ if err != nil {
+ t.Fatalf("Test %d: Failed to upload the final object: %v", i+1, err)
+ }
+
+ // Step 4: Try to read the object back and check its md5sum
+ sets.erasureDisksMu.Lock()
+ xl.getDisks = func() []StorageAPI { return origErasureDisks }
+ sets.erasureDisksMu.Unlock()
+ gr, err := z.GetObjectNInfo(ctx, testCase.bucket, testCase.object, nil, nil, ObjectOptions{VersionID: got.VersionID})
+ if err != nil {
+ t.Fatalf("Expected GetObject to succeed, but failed with %v", err)
+ }
+
+ h := md5.New()
+ h.Write(testCase.content)
+ expectedHash := h.Sum(nil)
+
+ h.Reset()
+ _, err = io.Copy(h, gr)
+ if err != nil {
+ t.Fatalf("Test %d: Failed to calculate md5sum of the object: %v", i+1, err)
+ }
+ gr.Close()
+ foundHash := h.Sum(nil)
+
+ if !bytes.Equal(foundHash, expectedHash) {
+ t.Fatalf("Test %d: Expected data to have md5sum = `%x`, found `%x`", i+1, expectedHash, foundHash)
+ }
+ }
+}
diff --git a/cmd/erasure-server-pool-decom.go b/cmd/erasure-server-pool-decom.go
new file mode 100644
index 00000000..834b9fb8
--- /dev/null
+++ b/cmd/erasure-server-pool-decom.go
@@ -0,0 +1,1544 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "math/rand"
+ "net/http"
+ "slices"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/dustin/go-humanize"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/bucket/lifecycle"
+ 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/hash"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/console"
+ "github.com/minio/pkg/v3/env"
+ "github.com/minio/pkg/v3/workers"
+)
+
+// PoolDecommissionInfo currently decommissioning information
+type PoolDecommissionInfo struct {
+ StartTime time.Time `json:"startTime" msg:"st"`
+ StartSize int64 `json:"startSize" msg:"ss"`
+ TotalSize int64 `json:"totalSize" msg:"ts"`
+ CurrentSize int64 `json:"currentSize" msg:"cs"`
+
+ Complete bool `json:"complete" msg:"cmp"`
+ Failed bool `json:"failed" msg:"fl"`
+ Canceled bool `json:"canceled" msg:"cnl"`
+
+ // Internal information.
+ QueuedBuckets []string `json:"-" msg:"bkts"`
+ DecommissionedBuckets []string `json:"-" msg:"dbkts"`
+
+ // Last bucket/object decommissioned.
+ Bucket string `json:"-" msg:"bkt"`
+ // Captures prefix that is currently being
+ // decommissioned inside the 'Bucket'
+ Prefix string `json:"-" msg:"pfx"`
+ Object string `json:"-" msg:"obj"`
+
+ // Verbose information
+ ItemsDecommissioned int64 `json:"objectsDecommissioned" msg:"id"`
+ ItemsDecommissionFailed int64 `json:"objectsDecommissionedFailed" msg:"idf"`
+ BytesDone int64 `json:"bytesDecommissioned" msg:"bd"`
+ BytesFailed int64 `json:"bytesDecommissionedFailed" msg:"bf"`
+}
+
+// Clone make a copy of PoolDecommissionInfo
+func (pd *PoolDecommissionInfo) Clone() *PoolDecommissionInfo {
+ if pd == nil {
+ return nil
+ }
+ return &PoolDecommissionInfo{
+ StartTime: pd.StartTime,
+ StartSize: pd.StartSize,
+ TotalSize: pd.TotalSize,
+ CurrentSize: pd.CurrentSize,
+ Complete: pd.Complete,
+ Failed: pd.Failed,
+ Canceled: pd.Canceled,
+ QueuedBuckets: pd.QueuedBuckets,
+ DecommissionedBuckets: pd.DecommissionedBuckets,
+ Bucket: pd.Bucket,
+ Prefix: pd.Prefix,
+ Object: pd.Object,
+ ItemsDecommissioned: pd.ItemsDecommissioned,
+ ItemsDecommissionFailed: pd.ItemsDecommissionFailed,
+ BytesDone: pd.BytesDone,
+ BytesFailed: pd.BytesFailed,
+ }
+}
+
+// bucketPop should be called when a bucket is done decommissioning.
+// Adds the bucket to the list of decommissioned buckets and updates resume numbers.
+func (pd *PoolDecommissionInfo) bucketPop(bucket string) bool {
+ pd.DecommissionedBuckets = append(pd.DecommissionedBuckets, bucket)
+ for i, b := range pd.QueuedBuckets {
+ if b == bucket {
+ // Bucket is done.
+ pd.QueuedBuckets = append(pd.QueuedBuckets[:i], pd.QueuedBuckets[i+1:]...)
+ // Clear tracker info.
+ if pd.Bucket == bucket {
+ pd.Bucket = "" // empty this out for next bucket
+ pd.Prefix = "" // empty this out for the next bucket
+ pd.Object = "" // empty this out for next object
+ }
+ return true
+ }
+ }
+ return false
+}
+
+func (pd *PoolDecommissionInfo) isBucketDecommissioned(bucket string) bool {
+ return slices.Contains(pd.DecommissionedBuckets, bucket)
+}
+
+func (pd *PoolDecommissionInfo) bucketPush(bucket decomBucketInfo) {
+ for _, b := range pd.QueuedBuckets {
+ if pd.isBucketDecommissioned(b) {
+ return
+ }
+ if b == bucket.String() {
+ return
+ }
+ }
+ pd.QueuedBuckets = append(pd.QueuedBuckets, bucket.String())
+ pd.Bucket = bucket.Name
+ pd.Prefix = bucket.Prefix
+}
+
+// PoolStatus captures current pool status
+type PoolStatus struct {
+ ID int `json:"id" msg:"id"`
+ CmdLine string `json:"cmdline" msg:"cl"`
+ LastUpdate time.Time `json:"lastUpdate" msg:"lu"`
+ Decommission *PoolDecommissionInfo `json:"decommissionInfo,omitempty" msg:"dec"`
+}
+
+// Clone returns a copy of PoolStatus
+func (ps PoolStatus) Clone() PoolStatus {
+ return PoolStatus{
+ ID: ps.ID,
+ CmdLine: ps.CmdLine,
+ LastUpdate: ps.LastUpdate,
+ Decommission: ps.Decommission.Clone(),
+ }
+}
+
+//go:generate msgp -file $GOFILE -unexported
+type poolMeta struct {
+ Version int `msg:"v"`
+ Pools []PoolStatus `msg:"pls"`
+
+ // Value should not be saved when we have not loaded anything yet.
+ dontSave bool `msg:"-"`
+}
+
+// A decommission resumable tells us if decommission is worth
+// resuming upon restart of a cluster.
+func (p *poolMeta) returnResumablePools() []PoolStatus {
+ var newPools []PoolStatus
+ for _, pool := range p.Pools {
+ if pool.Decommission == nil {
+ continue
+ }
+ if pool.Decommission.Complete || pool.Decommission.Canceled {
+ // Do not resume decommission upon startup for
+ // - decommission complete
+ // - decommission canceled
+ continue
+ } // In all other situations we need to resume
+ newPools = append(newPools, pool)
+ }
+ return newPools
+}
+
+func (p *poolMeta) DecommissionComplete(idx int) bool {
+ if p.Pools[idx].Decommission != nil && !p.Pools[idx].Decommission.Complete {
+ p.Pools[idx].LastUpdate = UTCNow()
+ p.Pools[idx].Decommission.Complete = true
+ p.Pools[idx].Decommission.Failed = false
+ p.Pools[idx].Decommission.Canceled = false
+ return true
+ }
+ return false
+}
+
+func (p *poolMeta) DecommissionFailed(idx int) bool {
+ if p.Pools[idx].Decommission != nil && !p.Pools[idx].Decommission.Failed {
+ p.Pools[idx].LastUpdate = UTCNow()
+ p.Pools[idx].Decommission.StartTime = time.Time{}
+ p.Pools[idx].Decommission.Complete = false
+ p.Pools[idx].Decommission.Failed = true
+ p.Pools[idx].Decommission.Canceled = false
+ return true
+ }
+ return false
+}
+
+func (p *poolMeta) DecommissionCancel(idx int) bool {
+ if p.Pools[idx].Decommission != nil && !p.Pools[idx].Decommission.Canceled {
+ p.Pools[idx].LastUpdate = UTCNow()
+ p.Pools[idx].Decommission.StartTime = time.Time{}
+ p.Pools[idx].Decommission.Complete = false
+ p.Pools[idx].Decommission.Failed = false
+ p.Pools[idx].Decommission.Canceled = true
+ return true
+ }
+ return false
+}
+
+func (p poolMeta) isBucketDecommissioned(idx int, bucket string) bool {
+ return p.Pools[idx].Decommission.isBucketDecommissioned(bucket)
+}
+
+func (p *poolMeta) BucketDone(idx int, bucket decomBucketInfo) bool {
+ if p.Pools[idx].Decommission == nil {
+ // Decommission not in progress.
+ return false
+ }
+ return p.Pools[idx].Decommission.bucketPop(bucket.String())
+}
+
+func (p poolMeta) ResumeBucketObject(idx int) (bucket, object string) {
+ if p.Pools[idx].Decommission != nil {
+ bucket = p.Pools[idx].Decommission.Bucket
+ object = p.Pools[idx].Decommission.Object
+ }
+ return bucket, object
+}
+
+func (p *poolMeta) TrackCurrentBucketObject(idx int, bucket string, object string) {
+ if p.Pools[idx].Decommission == nil {
+ // Decommission not in progress.
+ return
+ }
+ p.Pools[idx].Decommission.Bucket = bucket
+ p.Pools[idx].Decommission.Object = object
+}
+
+func (p *poolMeta) PendingBuckets(idx int) []decomBucketInfo {
+ if p.Pools[idx].Decommission == nil {
+ // Decommission not in progress.
+ return nil
+ }
+
+ decomBuckets := make([]decomBucketInfo, len(p.Pools[idx].Decommission.QueuedBuckets))
+ for i := range decomBuckets {
+ bucket, prefix := path2BucketObject(p.Pools[idx].Decommission.QueuedBuckets[i])
+ decomBuckets[i] = decomBucketInfo{
+ Name: bucket,
+ Prefix: prefix,
+ }
+ }
+
+ return decomBuckets
+}
+
+//msgp:ignore decomBucketInfo
+type decomBucketInfo struct {
+ Name string
+ Prefix string
+}
+
+func (db decomBucketInfo) String() string {
+ return pathJoin(db.Name, db.Prefix)
+}
+
+func (p *poolMeta) QueueBuckets(idx int, buckets []decomBucketInfo) {
+ // add new queued buckets
+ for _, bucket := range buckets {
+ p.Pools[idx].Decommission.bucketPush(bucket)
+ }
+}
+
+var (
+ errDecommissionAlreadyRunning = errors.New("decommission is already in progress")
+ errDecommissionComplete = errors.New("decommission is complete, please remove the servers from command-line")
+ errDecommissionNotStarted = errors.New("decommission is not in progress")
+)
+
+func (p *poolMeta) Decommission(idx int, pi poolSpaceInfo) error {
+ // Return an error when there is decommission on going - the user needs
+ // to explicitly cancel it first in order to restart decommissioning again.
+ if p.Pools[idx].Decommission != nil &&
+ !p.Pools[idx].Decommission.Complete &&
+ !p.Pools[idx].Decommission.Failed &&
+ !p.Pools[idx].Decommission.Canceled {
+ return errDecommissionAlreadyRunning
+ }
+
+ now := UTCNow()
+ p.Pools[idx].LastUpdate = now
+ p.Pools[idx].Decommission = &PoolDecommissionInfo{
+ StartTime: now,
+ StartSize: pi.Free,
+ CurrentSize: pi.Free,
+ TotalSize: pi.Total,
+ }
+ return nil
+}
+
+func (p poolMeta) IsSuspended(idx int) bool {
+ if idx >= len(p.Pools) {
+ // We don't really know if the pool is suspended or not, since it doesn't exist.
+ return false
+ }
+ return p.Pools[idx].Decommission != nil
+}
+
+func (p *poolMeta) validate(pools []*erasureSets) (bool, error) {
+ type poolInfo struct {
+ position int
+ completed bool
+ decomStarted bool // started but not finished yet
+ }
+
+ rememberedPools := make(map[string]poolInfo)
+ for idx, pool := range p.Pools {
+ complete := false
+ decomStarted := false
+ if pool.Decommission != nil {
+ if pool.Decommission.Complete {
+ complete = true
+ }
+ decomStarted = true
+ }
+ rememberedPools[pool.CmdLine] = poolInfo{
+ position: idx,
+ completed: complete,
+ decomStarted: decomStarted,
+ }
+ }
+
+ specifiedPools := make(map[string]int)
+ for idx, pool := range pools {
+ specifiedPools[pool.endpoints.CmdLine] = idx
+ }
+
+ var update bool
+ // Check if specified pools need to be removed from decommissioned pool.
+ for k := range specifiedPools {
+ pi, ok := rememberedPools[k]
+ if !ok {
+ // we do not have the pool anymore that we previously remembered, since all
+ // the CLI checks out we can allow updates since we are mostly adding a pool here.
+ update = true
+ }
+ if ok && pi.completed {
+ logger.LogIf(GlobalContext, "decommission", fmt.Errorf("pool(%s) = %s is decommissioned, please remove from server command line", humanize.Ordinal(pi.position+1), k))
+ }
+ }
+
+ if len(specifiedPools) == len(rememberedPools) {
+ for k, pi := range rememberedPools {
+ pos, ok := specifiedPools[k]
+ if ok && pos != pi.position {
+ update = true // pool order is changing, its okay to allow it.
+ }
+ }
+ }
+
+ if !update {
+ update = len(specifiedPools) != len(rememberedPools)
+ }
+
+ return update, nil
+}
+
+func (p *poolMeta) load(ctx context.Context, pool *erasureSets, pools []*erasureSets) error {
+ data, err := readConfig(ctx, pool, poolMetaName)
+ if err != nil {
+ if errors.Is(err, errConfigNotFound) || isErrObjectNotFound(err) {
+ return nil
+ }
+ return err
+ }
+ if len(data) == 0 {
+ // Seems to be empty create a new poolMeta object.
+ return nil
+ }
+ if len(data) <= 4 {
+ return fmt.Errorf("poolMeta: no data")
+ }
+ // Read header
+ switch binary.LittleEndian.Uint16(data[0:2]) {
+ case poolMetaFormat:
+ default:
+ return fmt.Errorf("poolMeta: unknown format: %d", binary.LittleEndian.Uint16(data[0:2]))
+ }
+ switch binary.LittleEndian.Uint16(data[2:4]) {
+ case poolMetaVersion:
+ default:
+ return fmt.Errorf("poolMeta: unknown version: %d", binary.LittleEndian.Uint16(data[2:4]))
+ }
+
+ // OK, parse data.
+ if _, err = p.UnmarshalMsg(data[4:]); err != nil {
+ return err
+ }
+
+ switch p.Version {
+ case poolMetaVersionV1:
+ default:
+ return fmt.Errorf("unexpected pool meta version: %d", p.Version)
+ }
+
+ return nil
+}
+
+func (p *poolMeta) CountItem(idx int, size int64, failed bool) {
+ pd := p.Pools[idx].Decommission
+ if pd == nil {
+ return
+ }
+ if failed {
+ pd.ItemsDecommissionFailed++
+ pd.BytesFailed += size
+ } else {
+ pd.ItemsDecommissioned++
+ pd.BytesDone += size
+ }
+ p.Pools[idx].Decommission = pd
+}
+
+func (p *poolMeta) updateAfter(ctx context.Context, idx int, pools []*erasureSets, duration time.Duration) (bool, error) {
+ if p.Pools[idx].Decommission == nil {
+ return false, errInvalidArgument
+ }
+ now := UTCNow()
+ if now.Sub(p.Pools[idx].LastUpdate) >= duration {
+ if serverDebugLog {
+ console.Debugf("decommission: persisting poolMeta on drive: threshold:%s, poolMeta:%#v\n", now.Sub(p.Pools[idx].LastUpdate), p.Pools[idx])
+ }
+ p.Pools[idx].LastUpdate = now
+ if err := p.save(ctx, pools); err != nil {
+ return false, err
+ }
+ return true, nil
+ }
+ return false, nil
+}
+
+func (p poolMeta) save(ctx context.Context, pools []*erasureSets) error {
+ if p.dontSave {
+ return nil
+ }
+ data := make([]byte, 4, p.Msgsize()+4)
+
+ // Initialize the header.
+ binary.LittleEndian.PutUint16(data[0:2], poolMetaFormat)
+ binary.LittleEndian.PutUint16(data[2:4], poolMetaVersion)
+
+ buf, err := p.MarshalMsg(data)
+ if err != nil {
+ return err
+ }
+
+ // Saves on all pools to make sure decommissioning of first pool is allowed.
+ for i, eset := range pools {
+ if err = saveConfig(ctx, eset, poolMetaName, buf); err != nil {
+ if !errors.Is(err, context.Canceled) {
+ storageLogIf(ctx, fmt.Errorf("saving pool.bin for pool index %d failed with: %v", i, err))
+ }
+ return err
+ }
+ }
+ return nil
+}
+
+const (
+ poolMetaName = "pool.bin"
+ poolMetaFormat = 1
+ poolMetaVersionV1 = 1
+ poolMetaVersion = poolMetaVersionV1
+)
+
+// Init() initializes pools and saves additional information about them
+// in 'pool.bin', this is eventually used for decommissioning the pool.
+func (z *erasureServerPools) Init(ctx context.Context) error {
+ // Load rebalance metadata if present
+ if err := z.loadRebalanceMeta(ctx); err == nil {
+ // Start rebalance routine if we can reload rebalance metadata.
+ z.StartRebalance()
+ }
+
+ meta := poolMeta{}
+ if err := meta.load(ctx, z.serverPools[0], z.serverPools); err != nil {
+ return err
+ }
+
+ update, err := meta.validate(z.serverPools)
+ if err != nil {
+ return err
+ }
+
+ // if no update is needed return right away.
+ if !update {
+ z.poolMetaMutex.Lock()
+ z.poolMeta = meta
+ z.poolMetaMutex.Unlock()
+ } else {
+ newMeta := newPoolMeta(z, meta)
+ if err = newMeta.save(ctx, z.serverPools); err != nil {
+ return err
+ }
+ z.poolMetaMutex.Lock()
+ z.poolMeta = newMeta
+ z.poolMetaMutex.Unlock()
+ }
+
+ pools := meta.returnResumablePools()
+ poolIndices := make([]int, 0, len(pools))
+ for _, pool := range pools {
+ idx := globalEndpoints.GetPoolIdx(pool.CmdLine)
+ if idx == -1 {
+ return fmt.Errorf("unexpected state present for decommission status pool(%s) not found", pool.CmdLine)
+ }
+ poolIndices = append(poolIndices, idx)
+ }
+
+ if len(poolIndices) > 0 && globalEndpoints[poolIndices[0]].Endpoints[0].IsLocal {
+ go func() {
+ // Resume decommissioning of pools, but wait 3 minutes for cluster to stabilize.
+ if err := sleepContext(ctx, 3*time.Minute); err != nil {
+ return
+ }
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+ for {
+ if err := z.Decommission(ctx, poolIndices...); err != nil {
+ if errors.Is(err, errDecommissionAlreadyRunning) {
+ // A previous decommission running found restart it.
+ for _, idx := range poolIndices {
+ z.doDecommissionInRoutine(ctx, idx)
+ }
+ return
+ }
+ if configRetriableErrors(err) {
+ decomLogIf(ctx, fmt.Errorf("Unable to resume decommission of pools %v: %w: retrying..", pools, err))
+ time.Sleep(time.Second + time.Duration(r.Float64()*float64(5*time.Second)))
+ continue
+ }
+ decomLogIf(ctx, fmt.Errorf("Unable to resume decommission of pool %v: %w", pools, err))
+ return
+ }
+ }
+ }()
+ }
+
+ return nil
+}
+
+func newPoolMeta(z *erasureServerPools, prevMeta poolMeta) poolMeta {
+ newMeta := poolMeta{} // to update write poolMeta fresh.
+ // looks like new pool was added we need to update,
+ // or this is a fresh installation (or an existing
+ // installation with pool removed)
+ newMeta.Version = poolMetaVersion
+ for idx, pool := range z.serverPools {
+ var skip bool
+ for _, currentPool := range prevMeta.Pools {
+ // Preserve any current pool status.
+ if currentPool.CmdLine == pool.endpoints.CmdLine {
+ currentPool.ID = idx
+ newMeta.Pools = append(newMeta.Pools, currentPool)
+ skip = true
+ break
+ }
+ }
+ if skip {
+ continue
+ }
+ newMeta.Pools = append(newMeta.Pools, PoolStatus{
+ CmdLine: pool.endpoints.CmdLine,
+ ID: idx,
+ LastUpdate: UTCNow(),
+ })
+ }
+ return newMeta
+}
+
+func (z *erasureServerPools) IsDecommissionRunning() bool {
+ z.poolMetaMutex.RLock()
+ defer z.poolMetaMutex.RUnlock()
+ meta := z.poolMeta
+ for _, pool := range meta.Pools {
+ if pool.Decommission != nil &&
+ !pool.Decommission.Complete &&
+ !pool.Decommission.Failed &&
+ !pool.Decommission.Canceled {
+ return true
+ }
+ }
+
+ return false
+}
+
+func (z *erasureServerPools) decommissionObject(ctx context.Context, idx int, bucket string, gr *GetObjectReader) (err error) {
+ objInfo := gr.ObjInfo
+
+ defer func() {
+ gr.Close()
+ auditLogDecom(ctx, "DecomCopyData", objInfo.Bucket, objInfo.Name, objInfo.VersionID, err)
+ }()
+
+ actualSize, err := objInfo.GetActualSize()
+ if err != nil {
+ return err
+ }
+
+ if objInfo.isMultipart() {
+ res, err := z.NewMultipartUpload(ctx, bucket, objInfo.Name, ObjectOptions{
+ VersionID: objInfo.VersionID,
+ UserDefined: objInfo.UserDefined,
+ NoAuditLog: true,
+ SrcPoolIdx: idx,
+ DataMovement: true,
+ })
+ if err != nil {
+ return fmt.Errorf("decommissionObject: NewMultipartUpload() %w", err)
+ }
+ defer z.AbortMultipartUpload(ctx, bucket, objInfo.Name, res.UploadID, ObjectOptions{NoAuditLog: true})
+ parts := make([]CompletePart, len(objInfo.Parts))
+ for i, part := range objInfo.Parts {
+ hr, err := hash.NewReader(ctx, io.LimitReader(gr, part.Size), part.Size, "", "", part.ActualSize)
+ if err != nil {
+ return fmt.Errorf("decommissionObject: hash.NewReader() %w", err)
+ }
+ pi, err := z.PutObjectPart(ctx, bucket, objInfo.Name, res.UploadID,
+ part.Number,
+ NewPutObjReader(hr),
+ ObjectOptions{
+ PreserveETag: part.ETag, // Preserve original ETag to ensure same metadata.
+ IndexCB: func() []byte {
+ return part.Index // Preserve part Index to ensure decompression works.
+ },
+ NoAuditLog: true,
+ })
+ if err != nil {
+ return fmt.Errorf("decommissionObject: PutObjectPart() %w", err)
+ }
+ parts[i] = CompletePart{
+ ETag: pi.ETag,
+ PartNumber: pi.PartNumber,
+ ChecksumCRC32: pi.ChecksumCRC32,
+ ChecksumCRC32C: pi.ChecksumCRC32C,
+ ChecksumSHA256: pi.ChecksumSHA256,
+ ChecksumSHA1: pi.ChecksumSHA1,
+ }
+ }
+ _, err = z.CompleteMultipartUpload(ctx, bucket, objInfo.Name, res.UploadID, parts, ObjectOptions{
+ SrcPoolIdx: idx,
+ DataMovement: true,
+ MTime: objInfo.ModTime,
+ NoAuditLog: true,
+ })
+ if err != nil {
+ err = fmt.Errorf("decommissionObject: CompleteMultipartUpload() %w", err)
+ }
+ return err
+ }
+
+ hr, err := hash.NewReader(ctx, io.LimitReader(gr, objInfo.Size), objInfo.Size, "", "", actualSize)
+ if err != nil {
+ return fmt.Errorf("decommissionObject: hash.NewReader() %w", err)
+ }
+
+ _, err = z.PutObject(ctx,
+ bucket,
+ objInfo.Name,
+ NewPutObjReader(hr),
+ ObjectOptions{
+ DataMovement: true,
+ SrcPoolIdx: idx,
+ VersionID: objInfo.VersionID,
+ MTime: objInfo.ModTime,
+ UserDefined: objInfo.UserDefined,
+ PreserveETag: objInfo.ETag, // Preserve original ETag to ensure same metadata.
+ IndexCB: func() []byte {
+ return objInfo.Parts[0].Index // Preserve part Index to ensure decompression works.
+ },
+ NoAuditLog: true,
+ })
+ if err != nil {
+ err = fmt.Errorf("decommissionObject: PutObject() %w", err)
+ }
+ return err
+}
+
+// versionsSorter sorts FileInfo slices by version.
+//
+//msgp:ignore versionsSorter
+type versionsSorter []FileInfo
+
+func (v versionsSorter) reverse() {
+ sort.Slice(v, func(i, j int) bool {
+ return v[i].ModTime.Before(v[j].ModTime)
+ })
+}
+
+func (set *erasureObjects) listObjectsToDecommission(ctx context.Context, bi decomBucketInfo, fn func(entry metaCacheEntry)) error {
+ disks, _ := set.getOnlineDisksWithHealing(false)
+ if len(disks) == 0 {
+ return fmt.Errorf("no online drives found for set with endpoints %s", set.getEndpoints())
+ }
+
+ // However many we ask, versions must exist on ~50%
+ listingQuorum := (set.setDriveCount + 1) / 2
+
+ // How to resolve partial results.
+ resolver := metadataResolutionParams{
+ dirQuorum: listingQuorum, // make sure to capture all quorum ratios
+ objQuorum: listingQuorum, // make sure to capture all quorum ratios
+ bucket: bi.Name,
+ }
+
+ err := listPathRaw(ctx, listPathRawOptions{
+ disks: disks,
+ bucket: bi.Name,
+ path: bi.Prefix,
+ recursive: true,
+ forwardTo: "",
+ minDisks: listingQuorum,
+ reportNotFound: false,
+ agreed: fn,
+ partial: func(entries metaCacheEntries, _ []error) {
+ entry, ok := entries.resolve(&resolver)
+ if ok {
+ fn(*entry)
+ }
+ },
+ finished: nil,
+ })
+ return err
+}
+
+func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool *erasureSets, bi decomBucketInfo) error {
+ ctx = logger.SetReqInfo(ctx, &logger.ReqInfo{})
+
+ const envDecomWorkers = "_MINIO_DECOMMISSION_WORKERS"
+ workerSize, err := env.GetInt(envDecomWorkers, len(pool.sets))
+ if err != nil {
+ decomLogIf(ctx, fmt.Errorf("invalid workers value err: %v, defaulting to %d", err, len(pool.sets)))
+ workerSize = len(pool.sets)
+ }
+
+ // Each decom worker needs one List() goroutine/worker
+ // add that many extra workers.
+ workerSize += len(pool.sets)
+
+ wk, err := workers.New(workerSize)
+ if err != nil {
+ return err
+ }
+
+ var vc *versioning.Versioning
+ var lc *lifecycle.Lifecycle
+ var lr objectlock.Retention
+ var rcfg *replication.Config
+ if bi.Name != minioMetaBucket {
+ vc, err = globalBucketVersioningSys.Get(bi.Name)
+ if err != nil {
+ return err
+ }
+
+ // Check if the current bucket has a configured lifecycle policy
+ lc, err = globalLifecycleSys.Get(bi.Name)
+ if err != nil && !errors.Is(err, BucketLifecycleNotFound{Bucket: bi.Name}) {
+ return err
+ }
+
+ // Check if bucket is object locked.
+ lr, err = globalBucketObjectLockSys.Get(bi.Name)
+ if err != nil {
+ return err
+ }
+
+ rcfg, err = getReplicationConfig(ctx, bi.Name)
+ if err != nil {
+ return err
+ }
+ }
+
+ for setIdx, set := range pool.sets {
+ filterLifecycle := func(bucket, object string, fi FileInfo) bool {
+ if lc == nil {
+ return false
+ }
+ versioned := vc != nil && vc.Versioned(object)
+ objInfo := fi.ToObjectInfo(bucket, object, versioned)
+
+ evt := evalActionFromLifecycle(ctx, *lc, lr, rcfg, objInfo)
+ switch {
+ case evt.Action.DeleteRestored(): // if restored copy has expired, delete it synchronously
+ applyExpiryOnTransitionedObject(ctx, z, objInfo, evt, lcEventSrc_Decom)
+ return false
+ case evt.Action.Delete():
+ globalExpiryState.enqueueByDays(objInfo, evt, lcEventSrc_Decom)
+ return true
+ default:
+ return false
+ }
+ }
+
+ decommissionEntry := func(entry metaCacheEntry) {
+ defer wk.Give()
+
+ if entry.isDir() {
+ return
+ }
+
+ fivs, err := entry.fileInfoVersions(bi.Name)
+ if err != nil {
+ return
+ }
+
+ // We need a reversed order for decommissioning,
+ // to create the appropriate stack.
+ versionsSorter(fivs.Versions).reverse()
+
+ var decommissioned, expired int
+ for _, version := range fivs.Versions {
+ stopFn := globalDecommissionMetrics.log(decomMetricDecommissionObject, idx, bi.Name, version.Name, version.VersionID)
+ // Apply lifecycle rules on the objects that are expired.
+ if filterLifecycle(bi.Name, version.Name, version) {
+ expired++
+ decommissioned++
+ stopFn(version.Size, errors.New("ILM expired object/version will be skipped"))
+ continue
+ }
+
+ // any object with only single DEL marker we don't need
+ // to decommission, just skip it, this also includes
+ // any other versions that have already expired.
+ remainingVersions := len(fivs.Versions) - expired
+ if version.Deleted && remainingVersions == 1 && rcfg == nil {
+ decommissioned++
+ stopFn(version.Size, errors.New("DELETE marked object with no other non-current versions will be skipped"))
+ continue
+ }
+
+ versionID := version.VersionID
+ if versionID == "" {
+ versionID = nullVersionID
+ }
+
+ var failure, ignore bool
+ if version.Deleted {
+ _, err := z.DeleteObject(ctx,
+ bi.Name,
+ version.Name,
+ ObjectOptions{
+ // Since we are preserving a delete marker, we have to make sure this is always true.
+ // regardless of the current configuration of the bucket we must preserve all versions
+ // on the pool being decommissioned.
+ Versioned: true,
+ VersionID: versionID,
+ MTime: version.ModTime,
+ DeleteReplication: version.ReplicationState,
+ SrcPoolIdx: idx,
+ DataMovement: true,
+ DeleteMarker: true, // make sure we create a delete marker
+ SkipDecommissioned: true, // make sure we skip the decommissioned pool
+ NoAuditLog: true,
+ })
+ if err != nil {
+ // This can happen when rebalance stop races with ongoing rebalance workers.
+ // These rebalance failures can be ignored.
+ if isErrObjectNotFound(err) || isErrVersionNotFound(err) || isDataMovementOverWriteErr(err) {
+ ignore = true
+ stopFn(0, nil)
+ continue
+ }
+ }
+ stopFn(version.Size, err)
+ if err != nil {
+ decomLogIf(ctx, err)
+ failure = true
+ }
+ z.poolMetaMutex.Lock()
+ z.poolMeta.CountItem(idx, 0, failure)
+ z.poolMetaMutex.Unlock()
+ if !failure {
+ // Success keep a count.
+ decommissioned++
+ }
+ auditLogDecom(ctx, "DecomCopyDeleteMarker", bi.Name, version.Name, versionID, err)
+ continue
+ }
+
+ // gr.Close() is ensured by decommissionObject().
+ for range 3 {
+ if version.IsRemote() {
+ if err := z.DecomTieredObject(ctx, bi.Name, version.Name, version, ObjectOptions{
+ VersionID: versionID,
+ MTime: version.ModTime,
+ UserDefined: version.Metadata,
+ SrcPoolIdx: idx,
+ DataMovement: true,
+ }); err != nil {
+ if isErrObjectNotFound(err) || isErrVersionNotFound(err) || isDataMovementOverWriteErr(err) {
+ ignore = true
+ stopFn(0, nil)
+ }
+ }
+ if !ignore {
+ stopFn(version.Size, err)
+ failure = err != nil
+ decomLogIf(ctx, err)
+ }
+ break
+ }
+ gr, err := set.GetObjectNInfo(ctx,
+ bi.Name,
+ encodeDirObject(version.Name),
+ nil,
+ http.Header{},
+ ObjectOptions{
+ VersionID: versionID,
+ NoDecryption: true,
+ NoLock: true,
+ NoAuditLog: true,
+ })
+ if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
+ // object deleted by the application, nothing to do here we move on.
+ ignore = true
+ stopFn(0, nil)
+ break
+ }
+ if err != nil && !ignore {
+ // if usage-cache.bin is not readable log and ignore it.
+ if bi.Name == minioMetaBucket && strings.Contains(version.Name, dataUsageCacheName) {
+ ignore = true
+ stopFn(version.Size, err)
+ decomLogIf(ctx, err)
+ break
+ }
+ }
+ if err != nil {
+ failure = true
+ decomLogIf(ctx, err)
+ stopFn(version.Size, err)
+ continue
+ }
+ if err = z.decommissionObject(ctx, idx, bi.Name, gr); err != nil {
+ if isErrObjectNotFound(err) || isErrVersionNotFound(err) || isDataMovementOverWriteErr(err) {
+ ignore = true
+ stopFn(0, nil)
+ break
+ }
+ stopFn(version.Size, err)
+ failure = true
+ decomLogIf(ctx, err)
+ continue
+ }
+ stopFn(version.Size, nil)
+ failure = false
+ break
+ }
+ if ignore {
+ continue
+ }
+ z.poolMetaMutex.Lock()
+ z.poolMeta.CountItem(idx, version.Size, failure)
+ z.poolMetaMutex.Unlock()
+ if failure {
+ break // break out on first error
+ }
+ decommissioned++
+ }
+
+ // if all versions were decommissioned, then we can delete the object versions.
+ if decommissioned == len(fivs.Versions) {
+ stopFn := globalDecommissionMetrics.log(decomMetricDecommissionRemoveObject, idx, bi.Name, entry.name)
+ _, err := set.DeleteObject(ctx,
+ bi.Name,
+ encodeDirObject(entry.name),
+ ObjectOptions{
+ DeletePrefix: true, // use prefix delete to delete all versions at once.
+ DeletePrefixObject: true, // use prefix delete on exact object (this is an optimization to avoid fan-out calls)
+ NoAuditLog: true,
+ },
+ )
+ stopFn(0, err)
+ auditLogDecom(ctx, "DecomDeleteObject", bi.Name, entry.name, "", err)
+ if err != nil {
+ decomLogIf(ctx, err)
+ }
+ }
+ z.poolMetaMutex.Lock()
+ z.poolMeta.TrackCurrentBucketObject(idx, bi.Name, entry.name)
+ ok, err := z.poolMeta.updateAfter(ctx, idx, z.serverPools, 30*time.Second)
+ decomLogIf(ctx, err)
+ if ok {
+ globalNotificationSys.ReloadPoolMeta(ctx)
+ }
+ z.poolMetaMutex.Unlock()
+ }
+
+ wk.Take()
+ go func(setIdx int) {
+ defer wk.Give()
+ // We will perpetually retry listing if it fails, since we cannot
+ // possibly give up in this matter
+ for !contextCanceled(ctx) {
+ err := set.listObjectsToDecommission(ctx, bi,
+ func(entry metaCacheEntry) {
+ wk.Take()
+ go decommissionEntry(entry)
+ },
+ )
+ if err == nil || errors.Is(err, context.Canceled) || errors.Is(err, errVolumeNotFound) {
+ break
+ }
+ setN := humanize.Ordinal(setIdx + 1)
+ retryDur := time.Duration(rand.Float64() * float64(5*time.Second))
+ decomLogOnceIf(ctx, fmt.Errorf("listing objects from %s set failed with %v, retrying in %v", setN, err, retryDur), "decom-listing-failed"+setN)
+ time.Sleep(retryDur)
+ }
+ }(setIdx)
+ }
+ wk.Wait()
+ return nil
+}
+
+//msgp:ignore decomMetrics
+type decomMetrics struct{}
+
+var globalDecommissionMetrics decomMetrics
+
+//msgp:ignore decomMetric
+//go:generate stringer -type=decomMetric -trimprefix=decomMetric $GOFILE
+type decomMetric uint8
+
+const (
+ decomMetricDecommissionBucket decomMetric = iota
+ decomMetricDecommissionObject
+ decomMetricDecommissionRemoveObject
+)
+
+func decomTrace(d decomMetric, poolIdx int, startTime time.Time, duration time.Duration, path string, err error, sz int64) madmin.TraceInfo {
+ var errStr string
+ if err != nil {
+ errStr = err.Error()
+ }
+ return madmin.TraceInfo{
+ TraceType: madmin.TraceDecommission,
+ Time: startTime,
+ NodeName: globalLocalNodeName,
+ FuncName: fmt.Sprintf("decommission.%s (pool-id=%d)", d.String(), poolIdx),
+ Duration: duration,
+ Path: path,
+ Error: errStr,
+ Bytes: sz,
+ }
+}
+
+func (m *decomMetrics) log(d decomMetric, poolIdx int, paths ...string) func(z int64, err error) {
+ startTime := time.Now()
+ return func(sz int64, err error) {
+ duration := time.Since(startTime)
+ if globalTrace.NumSubscribers(madmin.TraceDecommission) > 0 {
+ globalTrace.Publish(decomTrace(d, poolIdx, startTime, duration, strings.Join(paths, " "), err, sz))
+ }
+ }
+}
+
+func (z *erasureServerPools) decommissionInBackground(ctx context.Context, idx int) error {
+ pool := z.serverPools[idx]
+ z.poolMetaMutex.RLock()
+ pending := z.poolMeta.PendingBuckets(idx)
+ z.poolMetaMutex.RUnlock()
+
+ for _, bucket := range pending {
+ z.poolMetaMutex.RLock()
+ isDecommissioned := z.poolMeta.isBucketDecommissioned(idx, bucket.String())
+ z.poolMetaMutex.RUnlock()
+ if isDecommissioned {
+ if serverDebugLog {
+ console.Debugln("decommission: already done, moving on", bucket)
+ }
+
+ z.poolMetaMutex.Lock()
+ if z.poolMeta.BucketDone(idx, bucket) {
+ // remove from pendingBuckets and persist.
+ decomLogIf(ctx, z.poolMeta.save(ctx, z.serverPools))
+ }
+ z.poolMetaMutex.Unlock()
+ continue
+ }
+ if serverDebugLog {
+ console.Debugln("decommission: currently on bucket", bucket.Name)
+ }
+ stopFn := globalDecommissionMetrics.log(decomMetricDecommissionBucket, idx, bucket.Name)
+ if err := z.decommissionPool(ctx, idx, pool, bucket); err != nil {
+ stopFn(0, err)
+ return err
+ }
+ stopFn(0, nil)
+
+ z.poolMetaMutex.Lock()
+ if z.poolMeta.BucketDone(idx, bucket) {
+ decomLogIf(ctx, z.poolMeta.save(ctx, z.serverPools))
+ }
+ z.poolMetaMutex.Unlock()
+ }
+ return nil
+}
+
+func (z *erasureServerPools) checkAfterDecom(ctx context.Context, idx int) error {
+ buckets, err := z.getBucketsToDecommission(ctx)
+ if err != nil {
+ return err
+ }
+
+ pool := z.serverPools[idx]
+ for _, set := range pool.sets {
+ for _, bi := range buckets {
+ var vc *versioning.Versioning
+ var lc *lifecycle.Lifecycle
+ var lr objectlock.Retention
+ var rcfg *replication.Config
+ if bi.Name != minioMetaBucket {
+ vc, err = globalBucketVersioningSys.Get(bi.Name)
+ if err != nil {
+ return err
+ }
+
+ // Check if the current bucket has a configured lifecycle policy
+ lc, err = globalLifecycleSys.Get(bi.Name)
+ if err != nil && !errors.Is(err, BucketLifecycleNotFound{Bucket: bi.Name}) {
+ return err
+ }
+
+ // Check if bucket is object locked.
+ lr, err = globalBucketObjectLockSys.Get(bi.Name)
+ if err != nil {
+ return err
+ }
+
+ rcfg, err = getReplicationConfig(ctx, bi.Name)
+ if err != nil {
+ return err
+ }
+ }
+
+ filterLifecycle := func(bucket, object string, fi FileInfo) bool {
+ if lc == nil {
+ return false
+ }
+ versioned := vc != nil && vc.Versioned(object)
+ objInfo := fi.ToObjectInfo(bucket, object, versioned)
+
+ evt := evalActionFromLifecycle(ctx, *lc, lr, rcfg, objInfo)
+ switch {
+ case evt.Action.DeleteRestored(): // if restored copy has expired,delete it synchronously
+ applyExpiryOnTransitionedObject(ctx, z, objInfo, evt, lcEventSrc_Decom)
+ return false
+ case evt.Action.Delete():
+ globalExpiryState.enqueueByDays(objInfo, evt, lcEventSrc_Decom)
+ return true
+ default:
+ return false
+ }
+ }
+
+ var versionsFound int
+ if err = set.listObjectsToDecommission(ctx, bi, func(entry metaCacheEntry) {
+ if !entry.isObject() {
+ return
+ }
+
+ // `.usage-cache.bin` still exists, must be not readable ignore it.
+ if bi.Name == minioMetaBucket && strings.Contains(entry.name, dataUsageCacheName) {
+ // skipping bucket usage cache name, as its autogenerated.
+ return
+ }
+
+ fivs, err := entry.fileInfoVersions(bi.Name)
+ if err != nil {
+ return
+ }
+
+ var ignored int
+
+ for _, version := range fivs.Versions {
+ // Apply lifecycle rules on the objects that are expired.
+ if filterLifecycle(bi.Name, version.Name, version) {
+ ignored++
+ continue
+ }
+ if version.Deleted {
+ ignored++
+ continue
+ }
+ }
+
+ versionsFound += len(fivs.Versions) - ignored
+ }); err != nil {
+ return err
+ }
+
+ if versionsFound > 0 {
+ return fmt.Errorf("at least %d object(s)/version(s) were found in bucket `%s` after decommissioning", versionsFound, bi.Name)
+ }
+ }
+ }
+
+ return nil
+}
+
+func (z *erasureServerPools) doDecommissionInRoutine(ctx context.Context, idx int) {
+ z.poolMetaMutex.Lock()
+ var dctx context.Context
+ dctx, z.decommissionCancelers[idx] = context.WithCancel(GlobalContext)
+ z.poolMetaMutex.Unlock()
+
+ // Generate an empty request info so it can be directly modified later by audit
+ dctx = logger.SetReqInfo(dctx, &logger.ReqInfo{})
+
+ if err := z.decommissionInBackground(dctx, idx); err != nil {
+ decomLogIf(GlobalContext, err)
+ decomLogIf(GlobalContext, z.DecommissionFailed(dctx, idx))
+ return
+ }
+
+ z.poolMetaMutex.Lock()
+ failed := z.poolMeta.Pools[idx].Decommission.ItemsDecommissionFailed > 0 || contextCanceled(dctx)
+ poolCmdLine := z.poolMeta.Pools[idx].CmdLine
+ z.poolMetaMutex.Unlock()
+
+ if !failed {
+ decomLogEvent(dctx, "Decommissioning complete for pool '%s', verifying for any pending objects", poolCmdLine)
+ err := z.checkAfterDecom(dctx, idx)
+ if err != nil {
+ decomLogIf(ctx, err)
+ failed = true
+ }
+ }
+
+ if failed {
+ // Decommission failed indicate as such.
+ decomLogIf(GlobalContext, z.DecommissionFailed(dctx, idx))
+ } else {
+ // Complete the decommission..
+ decomLogIf(GlobalContext, z.CompleteDecommission(dctx, idx))
+ }
+}
+
+func (z *erasureServerPools) IsSuspended(idx int) bool {
+ z.poolMetaMutex.RLock()
+ defer z.poolMetaMutex.RUnlock()
+ return z.poolMeta.IsSuspended(idx)
+}
+
+// Decommission - start decommission session.
+func (z *erasureServerPools) Decommission(ctx context.Context, indices ...int) error {
+ if len(indices) == 0 {
+ return errInvalidArgument
+ }
+
+ if z.SinglePool() {
+ return errInvalidArgument
+ }
+
+ // Make pool unwritable before decommissioning.
+ if err := z.StartDecommission(ctx, indices...); err != nil {
+ return err
+ }
+
+ go func() {
+ for _, idx := range indices {
+ // decommission all pools serially one after
+ // the other.
+ z.doDecommissionInRoutine(ctx, idx)
+ }
+ }()
+
+ // Successfully started decommissioning.
+ return nil
+}
+
+type decomError struct {
+ Err string
+}
+
+func (d decomError) Error() string {
+ return d.Err
+}
+
+type poolSpaceInfo struct {
+ Free int64
+ Total int64
+ Used int64
+}
+
+func (z *erasureServerPools) getDecommissionPoolSpaceInfo(idx int) (pi poolSpaceInfo, err error) {
+ if idx < 0 {
+ return pi, errInvalidArgument
+ }
+ if idx+1 > len(z.serverPools) {
+ return pi, errInvalidArgument
+ }
+
+ info := z.serverPools[idx].StorageInfo(context.Background())
+ info.Backend = z.BackendInfo()
+
+ usableTotal := int64(GetTotalUsableCapacity(info.Disks, info))
+ usableFree := int64(GetTotalUsableCapacityFree(info.Disks, info))
+ return poolSpaceInfo{
+ Total: usableTotal,
+ Free: usableFree,
+ Used: usableTotal - usableFree,
+ }, nil
+}
+
+func (z *erasureServerPools) Status(ctx context.Context, idx int) (PoolStatus, error) {
+ if idx < 0 {
+ return PoolStatus{}, errInvalidArgument
+ }
+
+ pi, err := z.getDecommissionPoolSpaceInfo(idx)
+ if err != nil {
+ return PoolStatus{}, err
+ }
+
+ z.poolMetaMutex.RLock()
+ defer z.poolMetaMutex.RUnlock()
+
+ poolInfo := z.poolMeta.Pools[idx].Clone()
+ if poolInfo.Decommission != nil {
+ poolInfo.Decommission.TotalSize = pi.Total
+ poolInfo.Decommission.CurrentSize = pi.Free
+ } else {
+ poolInfo.Decommission = &PoolDecommissionInfo{
+ TotalSize: pi.Total,
+ CurrentSize: pi.Free,
+ }
+ }
+ return poolInfo, nil
+}
+
+func (z *erasureServerPools) ReloadPoolMeta(ctx context.Context) (err error) {
+ meta := poolMeta{}
+
+ if err = meta.load(ctx, z.serverPools[0], z.serverPools); err != nil {
+ return err
+ }
+
+ z.poolMetaMutex.Lock()
+ defer z.poolMetaMutex.Unlock()
+
+ z.poolMeta = meta
+ return nil
+}
+
+func (z *erasureServerPools) DecommissionCancel(ctx context.Context, idx int) (err error) {
+ if idx < 0 {
+ return errInvalidArgument
+ }
+
+ if z.SinglePool() {
+ return errInvalidArgument
+ }
+
+ z.poolMetaMutex.Lock()
+ defer z.poolMetaMutex.Unlock()
+
+ fn := z.decommissionCancelers[idx]
+ if fn == nil {
+ // canceling a decommission before it started return an error.
+ return errDecommissionNotStarted
+ }
+
+ defer fn() // cancel any active thread.
+
+ if z.poolMeta.DecommissionCancel(idx) {
+ if err = z.poolMeta.save(ctx, z.serverPools); err != nil {
+ return err
+ }
+ globalNotificationSys.ReloadPoolMeta(ctx)
+ }
+
+ return nil
+}
+
+func (z *erasureServerPools) DecommissionFailed(ctx context.Context, idx int) (err error) {
+ if idx < 0 {
+ return errInvalidArgument
+ }
+
+ if z.SinglePool() {
+ return errInvalidArgument
+ }
+
+ z.poolMetaMutex.Lock()
+ defer z.poolMetaMutex.Unlock()
+
+ if z.poolMeta.DecommissionFailed(idx) {
+ if fn := z.decommissionCancelers[idx]; fn != nil {
+ defer fn()
+ } // cancel any active thread.
+
+ if err = z.poolMeta.save(ctx, z.serverPools); err != nil {
+ return err
+ }
+ globalNotificationSys.ReloadPoolMeta(ctx)
+ }
+ return nil
+}
+
+func (z *erasureServerPools) CompleteDecommission(ctx context.Context, idx int) (err error) {
+ if idx < 0 {
+ return errInvalidArgument
+ }
+
+ if z.SinglePool() {
+ return errInvalidArgument
+ }
+
+ z.poolMetaMutex.Lock()
+ defer z.poolMetaMutex.Unlock()
+
+ if z.poolMeta.DecommissionComplete(idx) {
+ if fn := z.decommissionCancelers[idx]; fn != nil {
+ defer fn()
+ } // cancel any active thread.
+
+ if err = z.poolMeta.save(ctx, z.serverPools); err != nil {
+ return err
+ }
+ globalNotificationSys.ReloadPoolMeta(ctx)
+ }
+ return nil
+}
+
+func (z *erasureServerPools) getBucketsToDecommission(ctx context.Context) ([]decomBucketInfo, error) {
+ buckets, err := z.ListBuckets(ctx, BucketOptions{})
+ if err != nil {
+ return nil, err
+ }
+
+ decomBuckets := make([]decomBucketInfo, len(buckets))
+ for i := range buckets {
+ decomBuckets[i] = decomBucketInfo{
+ Name: buckets[i].Name,
+ }
+ }
+
+ // Buckets data are dispersed in multiple zones/sets, make
+ // sure to decommission the necessary metadata.
+ decomMetaBuckets := []decomBucketInfo{
+ {
+ Name: minioMetaBucket,
+ Prefix: minioConfigPrefix,
+ },
+ {
+ Name: minioMetaBucket,
+ Prefix: bucketMetaPrefix,
+ },
+ }
+
+ return append(decomMetaBuckets, decomBuckets...), nil
+}
+
+func (z *erasureServerPools) StartDecommission(ctx context.Context, indices ...int) (err error) {
+ if len(indices) == 0 {
+ return errInvalidArgument
+ }
+
+ if z.SinglePool() {
+ return errInvalidArgument
+ }
+
+ decomBuckets, err := z.getBucketsToDecommission(ctx)
+ if err != nil {
+ return err
+ }
+
+ for _, bucket := range decomBuckets {
+ z.HealBucket(ctx, bucket.Name, madmin.HealOpts{})
+ }
+
+ // Create .minio.sys/config, .minio.sys/buckets paths if missing,
+ // this code is present to avoid any missing meta buckets on other
+ // pools.
+ for _, metaBucket := range []string{
+ pathJoin(minioMetaBucket, minioConfigPrefix),
+ pathJoin(minioMetaBucket, bucketMetaPrefix),
+ } {
+ var bucketExists BucketExists
+ if err = z.MakeBucket(ctx, metaBucket, MakeBucketOptions{}); err != nil {
+ if !errors.As(err, &bucketExists) {
+ return err
+ }
+ }
+ }
+
+ z.poolMetaMutex.Lock()
+ defer z.poolMetaMutex.Unlock()
+
+ for _, idx := range indices {
+ pi, err := z.getDecommissionPoolSpaceInfo(idx)
+ if err != nil {
+ return err
+ }
+
+ if err = z.poolMeta.Decommission(idx, pi); err != nil {
+ return err
+ }
+
+ z.poolMeta.QueueBuckets(idx, decomBuckets)
+ }
+
+ if err = z.poolMeta.save(ctx, z.serverPools); err != nil {
+ return err
+ }
+
+ globalNotificationSys.ReloadPoolMeta(ctx)
+
+ return nil
+}
+
+func auditLogDecom(ctx context.Context, apiName, bucket, object, versionID string, err error) {
+ errStr := ""
+ if err != nil {
+ errStr = err.Error()
+ }
+ auditLogInternal(ctx, AuditLogOptions{
+ Event: "decommission",
+ APIName: apiName,
+ Bucket: bucket,
+ Object: object,
+ VersionID: versionID,
+ Error: errStr,
+ })
+}
diff --git a/cmd/erasure-server-pool-decom_gen.go b/cmd/erasure-server-pool-decom_gen.go
new file mode 100644
index 00000000..a4c6b56f
--- /dev/null
+++ b/cmd/erasure-server-pool-decom_gen.go
@@ -0,0 +1,1213 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "github.com/tinylib/msgp/msgp"
+)
+
+// DecodeMsg implements msgp.Decodable
+func (z *PoolDecommissionInfo) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "st":
+ z.StartTime, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "StartTime")
+ return
+ }
+ case "ss":
+ z.StartSize, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "StartSize")
+ return
+ }
+ case "ts":
+ z.TotalSize, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "TotalSize")
+ return
+ }
+ case "cs":
+ z.CurrentSize, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "CurrentSize")
+ return
+ }
+ case "cmp":
+ z.Complete, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "Complete")
+ return
+ }
+ case "fl":
+ z.Failed, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "Failed")
+ return
+ }
+ case "cnl":
+ z.Canceled, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "Canceled")
+ return
+ }
+ case "bkts":
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "QueuedBuckets")
+ return
+ }
+ if cap(z.QueuedBuckets) >= int(zb0002) {
+ z.QueuedBuckets = (z.QueuedBuckets)[:zb0002]
+ } else {
+ z.QueuedBuckets = make([]string, zb0002)
+ }
+ for za0001 := range z.QueuedBuckets {
+ z.QueuedBuckets[za0001], err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "QueuedBuckets", za0001)
+ return
+ }
+ }
+ case "dbkts":
+ var zb0003 uint32
+ zb0003, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "DecommissionedBuckets")
+ return
+ }
+ if cap(z.DecommissionedBuckets) >= int(zb0003) {
+ z.DecommissionedBuckets = (z.DecommissionedBuckets)[:zb0003]
+ } else {
+ z.DecommissionedBuckets = make([]string, zb0003)
+ }
+ for za0002 := range z.DecommissionedBuckets {
+ z.DecommissionedBuckets[za0002], err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "DecommissionedBuckets", za0002)
+ return
+ }
+ }
+ case "bkt":
+ z.Bucket, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "pfx":
+ z.Prefix, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Prefix")
+ return
+ }
+ case "obj":
+ z.Object, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ case "id":
+ z.ItemsDecommissioned, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "ItemsDecommissioned")
+ return
+ }
+ case "idf":
+ z.ItemsDecommissionFailed, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "ItemsDecommissionFailed")
+ return
+ }
+ case "bd":
+ z.BytesDone, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "BytesDone")
+ return
+ }
+ case "bf":
+ z.BytesFailed, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "BytesFailed")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *PoolDecommissionInfo) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 16
+ // write "st"
+ err = en.Append(0xde, 0x0, 0x10, 0xa2, 0x73, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.StartTime)
+ if err != nil {
+ err = msgp.WrapError(err, "StartTime")
+ return
+ }
+ // write "ss"
+ err = en.Append(0xa2, 0x73, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.StartSize)
+ if err != nil {
+ err = msgp.WrapError(err, "StartSize")
+ return
+ }
+ // write "ts"
+ err = en.Append(0xa2, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.TotalSize)
+ if err != nil {
+ err = msgp.WrapError(err, "TotalSize")
+ return
+ }
+ // write "cs"
+ err = en.Append(0xa2, 0x63, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.CurrentSize)
+ if err != nil {
+ err = msgp.WrapError(err, "CurrentSize")
+ return
+ }
+ // write "cmp"
+ err = en.Append(0xa3, 0x63, 0x6d, 0x70)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.Complete)
+ if err != nil {
+ err = msgp.WrapError(err, "Complete")
+ return
+ }
+ // write "fl"
+ err = en.Append(0xa2, 0x66, 0x6c)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.Failed)
+ if err != nil {
+ err = msgp.WrapError(err, "Failed")
+ return
+ }
+ // write "cnl"
+ err = en.Append(0xa3, 0x63, 0x6e, 0x6c)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.Canceled)
+ if err != nil {
+ err = msgp.WrapError(err, "Canceled")
+ return
+ }
+ // write "bkts"
+ err = en.Append(0xa4, 0x62, 0x6b, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(len(z.QueuedBuckets)))
+ if err != nil {
+ err = msgp.WrapError(err, "QueuedBuckets")
+ return
+ }
+ for za0001 := range z.QueuedBuckets {
+ err = en.WriteString(z.QueuedBuckets[za0001])
+ if err != nil {
+ err = msgp.WrapError(err, "QueuedBuckets", za0001)
+ return
+ }
+ }
+ // write "dbkts"
+ err = en.Append(0xa5, 0x64, 0x62, 0x6b, 0x74, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(len(z.DecommissionedBuckets)))
+ if err != nil {
+ err = msgp.WrapError(err, "DecommissionedBuckets")
+ return
+ }
+ for za0002 := range z.DecommissionedBuckets {
+ err = en.WriteString(z.DecommissionedBuckets[za0002])
+ if err != nil {
+ err = msgp.WrapError(err, "DecommissionedBuckets", za0002)
+ return
+ }
+ }
+ // write "bkt"
+ err = en.Append(0xa3, 0x62, 0x6b, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Bucket)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ // write "pfx"
+ err = en.Append(0xa3, 0x70, 0x66, 0x78)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Prefix)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefix")
+ return
+ }
+ // write "obj"
+ err = en.Append(0xa3, 0x6f, 0x62, 0x6a)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Object)
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ // write "id"
+ err = en.Append(0xa2, 0x69, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.ItemsDecommissioned)
+ if err != nil {
+ err = msgp.WrapError(err, "ItemsDecommissioned")
+ return
+ }
+ // write "idf"
+ err = en.Append(0xa3, 0x69, 0x64, 0x66)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.ItemsDecommissionFailed)
+ if err != nil {
+ err = msgp.WrapError(err, "ItemsDecommissionFailed")
+ return
+ }
+ // write "bd"
+ err = en.Append(0xa2, 0x62, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.BytesDone)
+ if err != nil {
+ err = msgp.WrapError(err, "BytesDone")
+ return
+ }
+ // write "bf"
+ err = en.Append(0xa2, 0x62, 0x66)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.BytesFailed)
+ if err != nil {
+ err = msgp.WrapError(err, "BytesFailed")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *PoolDecommissionInfo) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 16
+ // string "st"
+ o = append(o, 0xde, 0x0, 0x10, 0xa2, 0x73, 0x74)
+ o = msgp.AppendTime(o, z.StartTime)
+ // string "ss"
+ o = append(o, 0xa2, 0x73, 0x73)
+ o = msgp.AppendInt64(o, z.StartSize)
+ // string "ts"
+ o = append(o, 0xa2, 0x74, 0x73)
+ o = msgp.AppendInt64(o, z.TotalSize)
+ // string "cs"
+ o = append(o, 0xa2, 0x63, 0x73)
+ o = msgp.AppendInt64(o, z.CurrentSize)
+ // string "cmp"
+ o = append(o, 0xa3, 0x63, 0x6d, 0x70)
+ o = msgp.AppendBool(o, z.Complete)
+ // string "fl"
+ o = append(o, 0xa2, 0x66, 0x6c)
+ o = msgp.AppendBool(o, z.Failed)
+ // string "cnl"
+ o = append(o, 0xa3, 0x63, 0x6e, 0x6c)
+ o = msgp.AppendBool(o, z.Canceled)
+ // string "bkts"
+ o = append(o, 0xa4, 0x62, 0x6b, 0x74, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.QueuedBuckets)))
+ for za0001 := range z.QueuedBuckets {
+ o = msgp.AppendString(o, z.QueuedBuckets[za0001])
+ }
+ // string "dbkts"
+ o = append(o, 0xa5, 0x64, 0x62, 0x6b, 0x74, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.DecommissionedBuckets)))
+ for za0002 := range z.DecommissionedBuckets {
+ o = msgp.AppendString(o, z.DecommissionedBuckets[za0002])
+ }
+ // string "bkt"
+ o = append(o, 0xa3, 0x62, 0x6b, 0x74)
+ o = msgp.AppendString(o, z.Bucket)
+ // string "pfx"
+ o = append(o, 0xa3, 0x70, 0x66, 0x78)
+ o = msgp.AppendString(o, z.Prefix)
+ // string "obj"
+ o = append(o, 0xa3, 0x6f, 0x62, 0x6a)
+ o = msgp.AppendString(o, z.Object)
+ // string "id"
+ o = append(o, 0xa2, 0x69, 0x64)
+ o = msgp.AppendInt64(o, z.ItemsDecommissioned)
+ // string "idf"
+ o = append(o, 0xa3, 0x69, 0x64, 0x66)
+ o = msgp.AppendInt64(o, z.ItemsDecommissionFailed)
+ // string "bd"
+ o = append(o, 0xa2, 0x62, 0x64)
+ o = msgp.AppendInt64(o, z.BytesDone)
+ // string "bf"
+ o = append(o, 0xa2, 0x62, 0x66)
+ o = msgp.AppendInt64(o, z.BytesFailed)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *PoolDecommissionInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "st":
+ z.StartTime, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "StartTime")
+ return
+ }
+ case "ss":
+ z.StartSize, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "StartSize")
+ return
+ }
+ case "ts":
+ z.TotalSize, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "TotalSize")
+ return
+ }
+ case "cs":
+ z.CurrentSize, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "CurrentSize")
+ return
+ }
+ case "cmp":
+ z.Complete, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Complete")
+ return
+ }
+ case "fl":
+ z.Failed, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Failed")
+ return
+ }
+ case "cnl":
+ z.Canceled, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Canceled")
+ return
+ }
+ case "bkts":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "QueuedBuckets")
+ return
+ }
+ if cap(z.QueuedBuckets) >= int(zb0002) {
+ z.QueuedBuckets = (z.QueuedBuckets)[:zb0002]
+ } else {
+ z.QueuedBuckets = make([]string, zb0002)
+ }
+ for za0001 := range z.QueuedBuckets {
+ z.QueuedBuckets[za0001], bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "QueuedBuckets", za0001)
+ return
+ }
+ }
+ case "dbkts":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "DecommissionedBuckets")
+ return
+ }
+ if cap(z.DecommissionedBuckets) >= int(zb0003) {
+ z.DecommissionedBuckets = (z.DecommissionedBuckets)[:zb0003]
+ } else {
+ z.DecommissionedBuckets = make([]string, zb0003)
+ }
+ for za0002 := range z.DecommissionedBuckets {
+ z.DecommissionedBuckets[za0002], bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "DecommissionedBuckets", za0002)
+ return
+ }
+ }
+ case "bkt":
+ z.Bucket, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "pfx":
+ z.Prefix, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefix")
+ return
+ }
+ case "obj":
+ z.Object, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ case "id":
+ z.ItemsDecommissioned, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ItemsDecommissioned")
+ return
+ }
+ case "idf":
+ z.ItemsDecommissionFailed, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ItemsDecommissionFailed")
+ return
+ }
+ case "bd":
+ z.BytesDone, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "BytesDone")
+ return
+ }
+ case "bf":
+ z.BytesFailed, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "BytesFailed")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *PoolDecommissionInfo) Msgsize() (s int) {
+ s = 3 + 3 + msgp.TimeSize + 3 + msgp.Int64Size + 3 + msgp.Int64Size + 3 + msgp.Int64Size + 4 + msgp.BoolSize + 3 + msgp.BoolSize + 4 + msgp.BoolSize + 5 + msgp.ArrayHeaderSize
+ for za0001 := range z.QueuedBuckets {
+ s += msgp.StringPrefixSize + len(z.QueuedBuckets[za0001])
+ }
+ s += 6 + msgp.ArrayHeaderSize
+ for za0002 := range z.DecommissionedBuckets {
+ s += msgp.StringPrefixSize + len(z.DecommissionedBuckets[za0002])
+ }
+ s += 4 + msgp.StringPrefixSize + len(z.Bucket) + 4 + msgp.StringPrefixSize + len(z.Prefix) + 4 + msgp.StringPrefixSize + len(z.Object) + 3 + msgp.Int64Size + 4 + msgp.Int64Size + 3 + msgp.Int64Size + 3 + msgp.Int64Size
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *PoolStatus) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "id":
+ z.ID, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "ID")
+ return
+ }
+ case "cl":
+ z.CmdLine, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "CmdLine")
+ return
+ }
+ case "lu":
+ z.LastUpdate, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "LastUpdate")
+ return
+ }
+ case "dec":
+ if dc.IsNil() {
+ err = dc.ReadNil()
+ if err != nil {
+ err = msgp.WrapError(err, "Decommission")
+ return
+ }
+ z.Decommission = nil
+ } else {
+ if z.Decommission == nil {
+ z.Decommission = new(PoolDecommissionInfo)
+ }
+ err = z.Decommission.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Decommission")
+ return
+ }
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *PoolStatus) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 4
+ // write "id"
+ err = en.Append(0x84, 0xa2, 0x69, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.ID)
+ if err != nil {
+ err = msgp.WrapError(err, "ID")
+ return
+ }
+ // write "cl"
+ err = en.Append(0xa2, 0x63, 0x6c)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.CmdLine)
+ if err != nil {
+ err = msgp.WrapError(err, "CmdLine")
+ return
+ }
+ // write "lu"
+ err = en.Append(0xa2, 0x6c, 0x75)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.LastUpdate)
+ if err != nil {
+ err = msgp.WrapError(err, "LastUpdate")
+ return
+ }
+ // write "dec"
+ err = en.Append(0xa3, 0x64, 0x65, 0x63)
+ if err != nil {
+ return
+ }
+ if z.Decommission == nil {
+ err = en.WriteNil()
+ if err != nil {
+ return
+ }
+ } else {
+ err = z.Decommission.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Decommission")
+ return
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *PoolStatus) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 4
+ // string "id"
+ o = append(o, 0x84, 0xa2, 0x69, 0x64)
+ o = msgp.AppendInt(o, z.ID)
+ // string "cl"
+ o = append(o, 0xa2, 0x63, 0x6c)
+ o = msgp.AppendString(o, z.CmdLine)
+ // string "lu"
+ o = append(o, 0xa2, 0x6c, 0x75)
+ o = msgp.AppendTime(o, z.LastUpdate)
+ // string "dec"
+ o = append(o, 0xa3, 0x64, 0x65, 0x63)
+ if z.Decommission == nil {
+ o = msgp.AppendNil(o)
+ } else {
+ o, err = z.Decommission.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Decommission")
+ return
+ }
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *PoolStatus) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "id":
+ z.ID, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ID")
+ return
+ }
+ case "cl":
+ z.CmdLine, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "CmdLine")
+ return
+ }
+ case "lu":
+ z.LastUpdate, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "LastUpdate")
+ return
+ }
+ case "dec":
+ if msgp.IsNil(bts) {
+ bts, err = msgp.ReadNilBytes(bts)
+ if err != nil {
+ return
+ }
+ z.Decommission = nil
+ } else {
+ if z.Decommission == nil {
+ z.Decommission = new(PoolDecommissionInfo)
+ }
+ bts, err = z.Decommission.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Decommission")
+ return
+ }
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *PoolStatus) Msgsize() (s int) {
+ s = 1 + 3 + msgp.IntSize + 3 + msgp.StringPrefixSize + len(z.CmdLine) + 3 + msgp.TimeSize + 4
+ if z.Decommission == nil {
+ s += msgp.NilSize
+ } else {
+ s += z.Decommission.Msgsize()
+ }
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *decomError) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Err":
+ z.Err, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Err")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z decomError) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 1
+ // write "Err"
+ err = en.Append(0x81, 0xa3, 0x45, 0x72, 0x72)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Err)
+ if err != nil {
+ err = msgp.WrapError(err, "Err")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z decomError) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 1
+ // string "Err"
+ o = append(o, 0x81, 0xa3, 0x45, 0x72, 0x72)
+ o = msgp.AppendString(o, z.Err)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *decomError) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Err":
+ z.Err, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Err")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z decomError) Msgsize() (s int) {
+ s = 1 + 4 + msgp.StringPrefixSize + len(z.Err)
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *poolMeta) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "v":
+ z.Version, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "Version")
+ return
+ }
+ case "pls":
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Pools")
+ return
+ }
+ if cap(z.Pools) >= int(zb0002) {
+ z.Pools = (z.Pools)[:zb0002]
+ } else {
+ z.Pools = make([]PoolStatus, zb0002)
+ }
+ for za0001 := range z.Pools {
+ err = z.Pools[za0001].DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Pools", za0001)
+ return
+ }
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *poolMeta) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 2
+ // write "v"
+ err = en.Append(0x82, 0xa1, 0x76)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.Version)
+ if err != nil {
+ err = msgp.WrapError(err, "Version")
+ return
+ }
+ // write "pls"
+ err = en.Append(0xa3, 0x70, 0x6c, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(len(z.Pools)))
+ if err != nil {
+ err = msgp.WrapError(err, "Pools")
+ return
+ }
+ for za0001 := range z.Pools {
+ err = z.Pools[za0001].EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Pools", za0001)
+ return
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *poolMeta) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 2
+ // string "v"
+ o = append(o, 0x82, 0xa1, 0x76)
+ o = msgp.AppendInt(o, z.Version)
+ // string "pls"
+ o = append(o, 0xa3, 0x70, 0x6c, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.Pools)))
+ for za0001 := range z.Pools {
+ o, err = z.Pools[za0001].MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Pools", za0001)
+ return
+ }
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *poolMeta) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "v":
+ z.Version, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Version")
+ return
+ }
+ case "pls":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Pools")
+ return
+ }
+ if cap(z.Pools) >= int(zb0002) {
+ z.Pools = (z.Pools)[:zb0002]
+ } else {
+ z.Pools = make([]PoolStatus, zb0002)
+ }
+ for za0001 := range z.Pools {
+ bts, err = z.Pools[za0001].UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Pools", za0001)
+ return
+ }
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *poolMeta) Msgsize() (s int) {
+ s = 1 + 2 + msgp.IntSize + 4 + msgp.ArrayHeaderSize
+ for za0001 := range z.Pools {
+ s += z.Pools[za0001].Msgsize()
+ }
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *poolSpaceInfo) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Free":
+ z.Free, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "Free")
+ return
+ }
+ case "Total":
+ z.Total, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "Total")
+ return
+ }
+ case "Used":
+ z.Used, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "Used")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z poolSpaceInfo) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 3
+ // write "Free"
+ err = en.Append(0x83, 0xa4, 0x46, 0x72, 0x65, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.Free)
+ if err != nil {
+ err = msgp.WrapError(err, "Free")
+ return
+ }
+ // write "Total"
+ err = en.Append(0xa5, 0x54, 0x6f, 0x74, 0x61, 0x6c)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.Total)
+ if err != nil {
+ err = msgp.WrapError(err, "Total")
+ return
+ }
+ // write "Used"
+ err = en.Append(0xa4, 0x55, 0x73, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.Used)
+ if err != nil {
+ err = msgp.WrapError(err, "Used")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z poolSpaceInfo) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 3
+ // string "Free"
+ o = append(o, 0x83, 0xa4, 0x46, 0x72, 0x65, 0x65)
+ o = msgp.AppendInt64(o, z.Free)
+ // string "Total"
+ o = append(o, 0xa5, 0x54, 0x6f, 0x74, 0x61, 0x6c)
+ o = msgp.AppendInt64(o, z.Total)
+ // string "Used"
+ o = append(o, 0xa4, 0x55, 0x73, 0x65, 0x64)
+ o = msgp.AppendInt64(o, z.Used)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *poolSpaceInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Free":
+ z.Free, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Free")
+ return
+ }
+ case "Total":
+ z.Total, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Total")
+ return
+ }
+ case "Used":
+ z.Used, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Used")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z poolSpaceInfo) Msgsize() (s int) {
+ s = 1 + 5 + msgp.Int64Size + 6 + msgp.Int64Size + 5 + msgp.Int64Size
+ return
+}
diff --git a/cmd/erasure-server-pool-decom_gen_test.go b/cmd/erasure-server-pool-decom_gen_test.go
new file mode 100644
index 00000000..47a08be1
--- /dev/null
+++ b/cmd/erasure-server-pool-decom_gen_test.go
@@ -0,0 +1,575 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+func TestMarshalUnmarshalPoolDecommissionInfo(t *testing.T) {
+ v := PoolDecommissionInfo{}
+ 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 BenchmarkMarshalMsgPoolDecommissionInfo(b *testing.B) {
+ v := PoolDecommissionInfo{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgPoolDecommissionInfo(b *testing.B) {
+ v := PoolDecommissionInfo{}
+ 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 BenchmarkUnmarshalPoolDecommissionInfo(b *testing.B) {
+ v := PoolDecommissionInfo{}
+ 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 TestEncodeDecodePoolDecommissionInfo(t *testing.T) {
+ v := PoolDecommissionInfo{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodePoolDecommissionInfo Msgsize() is inaccurate")
+ }
+
+ vn := PoolDecommissionInfo{}
+ 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 BenchmarkEncodePoolDecommissionInfo(b *testing.B) {
+ v := PoolDecommissionInfo{}
+ 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 BenchmarkDecodePoolDecommissionInfo(b *testing.B) {
+ v := PoolDecommissionInfo{}
+ 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 TestMarshalUnmarshalPoolStatus(t *testing.T) {
+ v := PoolStatus{}
+ 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 BenchmarkMarshalMsgPoolStatus(b *testing.B) {
+ v := PoolStatus{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgPoolStatus(b *testing.B) {
+ v := PoolStatus{}
+ 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 BenchmarkUnmarshalPoolStatus(b *testing.B) {
+ v := PoolStatus{}
+ 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 TestEncodeDecodePoolStatus(t *testing.T) {
+ v := PoolStatus{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodePoolStatus Msgsize() is inaccurate")
+ }
+
+ vn := PoolStatus{}
+ 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 BenchmarkEncodePoolStatus(b *testing.B) {
+ v := PoolStatus{}
+ 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 BenchmarkDecodePoolStatus(b *testing.B) {
+ v := PoolStatus{}
+ 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 TestMarshalUnmarshaldecomError(t *testing.T) {
+ v := decomError{}
+ 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 BenchmarkMarshalMsgdecomError(b *testing.B) {
+ v := decomError{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgdecomError(b *testing.B) {
+ v := decomError{}
+ 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 BenchmarkUnmarshaldecomError(b *testing.B) {
+ v := decomError{}
+ 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 TestEncodeDecodedecomError(t *testing.T) {
+ v := decomError{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodedecomError Msgsize() is inaccurate")
+ }
+
+ vn := decomError{}
+ 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 BenchmarkEncodedecomError(b *testing.B) {
+ v := decomError{}
+ 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 BenchmarkDecodedecomError(b *testing.B) {
+ v := decomError{}
+ 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 TestMarshalUnmarshalpoolMeta(t *testing.T) {
+ v := poolMeta{}
+ 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 BenchmarkMarshalMsgpoolMeta(b *testing.B) {
+ v := poolMeta{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgpoolMeta(b *testing.B) {
+ v := poolMeta{}
+ 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 BenchmarkUnmarshalpoolMeta(b *testing.B) {
+ v := poolMeta{}
+ 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 TestEncodeDecodepoolMeta(t *testing.T) {
+ v := poolMeta{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodepoolMeta Msgsize() is inaccurate")
+ }
+
+ vn := poolMeta{}
+ 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 BenchmarkEncodepoolMeta(b *testing.B) {
+ v := poolMeta{}
+ 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 BenchmarkDecodepoolMeta(b *testing.B) {
+ v := poolMeta{}
+ 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 TestMarshalUnmarshalpoolSpaceInfo(t *testing.T) {
+ v := poolSpaceInfo{}
+ 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 BenchmarkMarshalMsgpoolSpaceInfo(b *testing.B) {
+ v := poolSpaceInfo{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgpoolSpaceInfo(b *testing.B) {
+ v := poolSpaceInfo{}
+ 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 BenchmarkUnmarshalpoolSpaceInfo(b *testing.B) {
+ v := poolSpaceInfo{}
+ 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 TestEncodeDecodepoolSpaceInfo(t *testing.T) {
+ v := poolSpaceInfo{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodepoolSpaceInfo Msgsize() is inaccurate")
+ }
+
+ vn := poolSpaceInfo{}
+ 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 BenchmarkEncodepoolSpaceInfo(b *testing.B) {
+ v := poolSpaceInfo{}
+ 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 BenchmarkDecodepoolSpaceInfo(b *testing.B) {
+ v := poolSpaceInfo{}
+ 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)
+ }
+ }
+}
diff --git a/cmd/erasure-server-pool-decom_test.go b/cmd/erasure-server-pool-decom_test.go
new file mode 100644
index 00000000..7f4c8ef1
--- /dev/null
+++ b/cmd/erasure-server-pool-decom_test.go
@@ -0,0 +1,195 @@
+// Copyright (c) 2015-2022 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "testing"
+)
+
+func prepareErasurePools() (ObjectLayer, []string, error) {
+ nDisks := 32
+ fsDirs, err := getRandomDisks(nDisks)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ pools := mustGetPoolEndpoints(0, fsDirs[:16]...)
+ pools = append(pools, mustGetPoolEndpoints(1, fsDirs[16:]...)...)
+
+ objLayer, _, err := initObjectLayer(context.Background(), pools)
+ if err != nil {
+ removeRoots(fsDirs)
+ return nil, nil, err
+ }
+ return objLayer, fsDirs, nil
+}
+
+func TestPoolMetaValidate(t *testing.T) {
+ objLayer1, fsDirs, err := prepareErasurePools()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer removeRoots(fsDirs)
+
+ meta := objLayer1.(*erasureServerPools).poolMeta
+ pools := objLayer1.(*erasureServerPools).serverPools
+
+ objLayer2, fsDirs, err := prepareErasurePools()
+ if err != nil {
+ t.Fatalf("Initialization of object layer failed for Erasure setup: %s", err)
+ }
+ defer removeRoots(fsDirs)
+
+ newPools := objLayer2.(*erasureServerPools).serverPools
+ reducedPools := pools[1:]
+ orderChangePools := []*erasureSets{
+ pools[1],
+ pools[0],
+ }
+
+ var nmeta1 poolMeta
+ nmeta1.Version = poolMetaVersion
+ nmeta1.Pools = append(nmeta1.Pools, meta.Pools...)
+ for i, pool := range nmeta1.Pools {
+ if i == 0 {
+ nmeta1.Pools[i] = PoolStatus{
+ CmdLine: pool.CmdLine,
+ ID: i,
+ LastUpdate: UTCNow(),
+ Decommission: &PoolDecommissionInfo{
+ Complete: true,
+ },
+ }
+ }
+ }
+
+ var nmeta2 poolMeta
+ nmeta2.Version = poolMetaVersion
+ nmeta2.Pools = append(nmeta2.Pools, meta.Pools...)
+ for i, pool := range nmeta2.Pools {
+ if i == 0 {
+ nmeta2.Pools[i] = PoolStatus{
+ CmdLine: pool.CmdLine,
+ ID: i,
+ LastUpdate: UTCNow(),
+ Decommission: &PoolDecommissionInfo{
+ Complete: false,
+ },
+ }
+ }
+ }
+
+ testCases := []struct {
+ meta poolMeta
+ pools []*erasureSets
+ expectedUpdate bool
+ expectedErr bool
+ name string
+ }{
+ {
+ meta: meta,
+ pools: pools,
+ name: "Correct",
+ expectedErr: false,
+ expectedUpdate: false,
+ },
+ {
+ meta: meta,
+ pools: newPools,
+ name: "Correct-Update",
+ expectedErr: false,
+ expectedUpdate: true,
+ },
+ {
+ meta: meta,
+ pools: reducedPools,
+ name: "Correct-Update",
+ expectedErr: false,
+ expectedUpdate: true,
+ },
+ {
+ meta: meta,
+ pools: orderChangePools,
+ name: "Invalid-Orderchange",
+ expectedErr: false,
+ expectedUpdate: true,
+ },
+ {
+ meta: nmeta1,
+ pools: pools,
+ name: "Invalid-Completed-Pool-Not-Removed",
+ expectedErr: false,
+ expectedUpdate: false,
+ },
+ {
+ meta: nmeta2,
+ pools: pools,
+ name: "Correct-Decom-Pending",
+ expectedErr: false,
+ expectedUpdate: false,
+ },
+ {
+ meta: nmeta2,
+ pools: reducedPools,
+ name: "Invalid-Decom-Pending-Pool-Removal",
+ expectedErr: false,
+ expectedUpdate: true,
+ },
+ {
+ meta: nmeta1,
+ pools: reducedPools,
+ name: "Correct-Decom-Pool-Removed",
+ expectedErr: false,
+ expectedUpdate: true,
+ },
+ {
+ meta: poolMeta{}, // no-pool info available fresh setup.
+ pools: pools,
+ name: "Correct-Fresh-Setup",
+ expectedErr: false,
+ expectedUpdate: true,
+ },
+ {
+ meta: nmeta2,
+ pools: orderChangePools,
+ name: "Invalid-Orderchange-Decom",
+ expectedErr: false,
+ expectedUpdate: true,
+ },
+ }
+
+ t.Parallel()
+ for _, testCase := range testCases {
+ t.Run(testCase.name, func(t *testing.T) {
+ update, err := testCase.meta.validate(testCase.pools)
+ if testCase.expectedErr {
+ t.Log(err)
+ }
+ if err != nil && !testCase.expectedErr {
+ t.Errorf("Expected success, but found %s", err)
+ }
+ if err == nil && testCase.expectedErr {
+ t.Error("Expected error, but got `nil`")
+ }
+ if update != testCase.expectedUpdate {
+ t.Errorf("Expected %t, got %t", testCase.expectedUpdate, update)
+ }
+ })
+ }
+}
diff --git a/cmd/erasure-server-pool-rebalance.go b/cmd/erasure-server-pool-rebalance.go
new file mode 100644
index 00000000..c037f0dd
--- /dev/null
+++ b/cmd/erasure-server-pool-rebalance.go
@@ -0,0 +1,1036 @@
+// Copyright (c) 2015-2022 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "math"
+ "math/rand"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/dustin/go-humanize"
+ "github.com/lithammer/shortuuid/v4"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/bucket/lifecycle"
+ 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/hash"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/env"
+ "github.com/minio/pkg/v3/workers"
+)
+
+//go:generate msgp -file $GOFILE -unexported
+
+// rebalanceStats contains per-pool rebalance statistics like number of objects,
+// versions and bytes rebalanced out of a pool
+type rebalanceStats struct {
+ InitFreeSpace uint64 `json:"initFreeSpace" msg:"ifs"` // Pool free space at the start of rebalance
+ InitCapacity uint64 `json:"initCapacity" msg:"ic"` // Pool capacity at the start of rebalance
+
+ Buckets []string `json:"buckets" msg:"bus"` // buckets being rebalanced or to be rebalanced
+ RebalancedBuckets []string `json:"rebalancedBuckets" msg:"rbs"` // buckets rebalanced
+ Bucket string `json:"bucket" msg:"bu"` // Last rebalanced bucket
+ Object string `json:"object" msg:"ob"` // Last rebalanced object
+ NumObjects uint64 `json:"numObjects" msg:"no"` // Number of objects rebalanced
+ NumVersions uint64 `json:"numVersions" msg:"nv"` // Number of versions rebalanced
+ Bytes uint64 `json:"bytes" msg:"bs"` // Number of bytes rebalanced
+ Participating bool `json:"participating" msg:"par"`
+ Info rebalanceInfo `json:"info" msg:"inf"`
+}
+
+func (rs *rebalanceStats) update(bucket string, fi FileInfo) {
+ if fi.IsLatest {
+ rs.NumObjects++
+ }
+
+ rs.NumVersions++
+ onDiskSz := int64(0)
+ if !fi.Deleted {
+ onDiskSz = fi.Size * int64(fi.Erasure.DataBlocks+fi.Erasure.ParityBlocks) / int64(fi.Erasure.DataBlocks)
+ }
+ rs.Bytes += uint64(onDiskSz)
+ rs.Bucket = bucket
+ rs.Object = fi.Name
+}
+
+type rstats []*rebalanceStats
+
+//go:generate stringer -type=rebalStatus -trimprefix=rebal $GOFILE
+type rebalStatus uint8
+
+const (
+ rebalNone rebalStatus = iota
+ rebalStarted
+ rebalCompleted
+ rebalStopped
+ rebalFailed
+)
+
+type rebalanceInfo struct {
+ StartTime time.Time `msg:"startTs"` // Time at which rebalance-start was issued
+ EndTime time.Time `msg:"stopTs"` // Time at which rebalance operation completed or rebalance-stop was called
+ Status rebalStatus `msg:"status"` // Current state of rebalance operation. One of Started|Stopped|Completed|Failed.
+}
+
+// rebalanceMeta contains information pertaining to an ongoing rebalance operation.
+type rebalanceMeta struct {
+ StoppedAt time.Time `msg:"stopTs"` // Time when rebalance-stop was issued.
+ ID string `msg:"id"` // ID of the ongoing rebalance operation
+ PercentFreeGoal float64 `msg:"pf"` // Computed from total free space and capacity at the start of rebalance
+ PoolStats []*rebalanceStats `msg:"rss"` // Per-pool rebalance stats keyed by pool index
+}
+
+var errRebalanceNotStarted = errors.New("rebalance not started")
+
+func (z *erasureServerPools) loadRebalanceMeta(ctx context.Context) error {
+ r := &rebalanceMeta{}
+ if err := r.load(ctx, z.serverPools[0]); err == nil {
+ z.rebalMu.Lock()
+ z.rebalMeta = r
+ z.updateRebalanceStats(ctx)
+ z.rebalMu.Unlock()
+ } else if !errors.Is(err, errConfigNotFound) {
+ rebalanceLogIf(ctx, fmt.Errorf("failed to load rebalance metadata, continue to restart rebalance as needed: %w", err))
+ }
+ return nil
+}
+
+// updates rebalance.bin from let's say 2 pool setup in the middle
+// of a rebalance, was expanded can cause z.rebalMeta to be outdated
+// due to a missing new pool. This function tries to handle this
+// scenario, albeit rare it seems to have occurred in the wild.
+//
+// since we do not explicitly disallow it, but it is okay for them
+// expand and then we continue to rebalance.
+func (z *erasureServerPools) updateRebalanceStats(ctx context.Context) error {
+ var ok bool
+ for i := range z.serverPools {
+ if z.findIndex(i) == -1 {
+ // Also ensure to initialize rebalanceStats to indicate
+ // its a new pool that can receive rebalanced data.
+ z.rebalMeta.PoolStats = append(z.rebalMeta.PoolStats, &rebalanceStats{})
+ ok = true
+ }
+ }
+ if ok {
+ return z.rebalMeta.save(ctx, z.serverPools[0])
+ }
+
+ return nil
+}
+
+func (z *erasureServerPools) findIndex(index int) int {
+ if z.rebalMeta == nil {
+ return 0
+ }
+ for i := range len(z.rebalMeta.PoolStats) {
+ if i == index {
+ return index
+ }
+ }
+ return -1
+}
+
+// initRebalanceMeta initializes rebalance metadata for a new rebalance
+// operation and saves it in the object store.
+func (z *erasureServerPools) initRebalanceMeta(ctx context.Context, buckets []string) (arn string, err error) {
+ r := &rebalanceMeta{
+ ID: shortuuid.New(),
+ PoolStats: make([]*rebalanceStats, len(z.serverPools)),
+ }
+
+ // Fetch disk capacity and available space.
+ si := z.StorageInfo(ctx, true)
+ diskStats := make([]struct {
+ AvailableSpace uint64
+ TotalSpace uint64
+ }, len(z.serverPools))
+ var totalCap, totalFree uint64
+ for _, disk := range si.Disks {
+ // Ignore invalid.
+ if disk.PoolIndex < 0 || len(diskStats) <= disk.PoolIndex {
+ // https://github.com/minio/minio/issues/16500
+ continue
+ }
+ totalCap += disk.TotalSpace
+ totalFree += disk.AvailableSpace
+
+ diskStats[disk.PoolIndex].AvailableSpace += disk.AvailableSpace
+ diskStats[disk.PoolIndex].TotalSpace += disk.TotalSpace
+ }
+ r.PercentFreeGoal = float64(totalFree) / float64(totalCap)
+
+ now := time.Now()
+ for idx := range z.serverPools {
+ r.PoolStats[idx] = &rebalanceStats{
+ Buckets: make([]string, len(buckets)),
+ RebalancedBuckets: make([]string, 0, len(buckets)),
+ InitFreeSpace: diskStats[idx].AvailableSpace,
+ InitCapacity: diskStats[idx].TotalSpace,
+ }
+ copy(r.PoolStats[idx].Buckets, buckets)
+
+ if pfi := float64(diskStats[idx].AvailableSpace) / float64(diskStats[idx].TotalSpace); pfi < r.PercentFreeGoal {
+ r.PoolStats[idx].Participating = true
+ r.PoolStats[idx].Info = rebalanceInfo{
+ StartTime: now,
+ Status: rebalStarted,
+ }
+ }
+ }
+
+ err = r.save(ctx, z.serverPools[0])
+ if err != nil {
+ return arn, err
+ }
+
+ z.rebalMeta = r
+ return r.ID, nil
+}
+
+func (z *erasureServerPools) updatePoolStats(poolIdx int, bucket string, fi FileInfo) {
+ z.rebalMu.Lock()
+ defer z.rebalMu.Unlock()
+
+ r := z.rebalMeta
+ if r == nil {
+ return
+ }
+
+ r.PoolStats[poolIdx].update(bucket, fi)
+}
+
+const (
+ rebalMetaName = "rebalance.bin"
+ rebalMetaFmt = 1
+ rebalMetaVer = 1
+)
+
+func (z *erasureServerPools) nextRebalBucket(poolIdx int) (string, bool) {
+ z.rebalMu.RLock()
+ defer z.rebalMu.RUnlock()
+
+ r := z.rebalMeta
+ if r == nil {
+ return "", false
+ }
+
+ ps := r.PoolStats[poolIdx]
+ if ps == nil {
+ return "", false
+ }
+
+ if ps.Info.Status == rebalCompleted || !ps.Participating {
+ return "", false
+ }
+
+ if len(ps.Buckets) == 0 {
+ return "", false
+ }
+
+ return ps.Buckets[0], true
+}
+
+func (z *erasureServerPools) bucketRebalanceDone(bucket string, poolIdx int) {
+ z.rebalMu.Lock()
+ defer z.rebalMu.Unlock()
+
+ if z.rebalMeta == nil {
+ return
+ }
+
+ ps := z.rebalMeta.PoolStats[poolIdx]
+ if ps == nil {
+ return
+ }
+
+ for i, b := range ps.Buckets {
+ if b == bucket {
+ ps.Buckets = append(ps.Buckets[:i], ps.Buckets[i+1:]...)
+ ps.RebalancedBuckets = append(ps.RebalancedBuckets, bucket)
+ break
+ }
+ }
+}
+
+func (r *rebalanceMeta) load(ctx context.Context, store objectIO) error {
+ return r.loadWithOpts(ctx, store, ObjectOptions{})
+}
+
+func (r *rebalanceMeta) loadWithOpts(ctx context.Context, store objectIO, opts ObjectOptions) error {
+ data, _, err := readConfigWithMetadata(ctx, store, rebalMetaName, opts)
+ if err != nil {
+ return err
+ }
+
+ if len(data) == 0 {
+ return nil
+ }
+ if len(data) <= 4 {
+ return fmt.Errorf("rebalanceMeta: no data")
+ }
+
+ // Read header
+ switch binary.LittleEndian.Uint16(data[0:2]) {
+ case rebalMetaFmt:
+ default:
+ return fmt.Errorf("rebalanceMeta: unknown format: %d", binary.LittleEndian.Uint16(data[0:2]))
+ }
+ switch binary.LittleEndian.Uint16(data[2:4]) {
+ case rebalMetaVer:
+ default:
+ return fmt.Errorf("rebalanceMeta: unknown version: %d", binary.LittleEndian.Uint16(data[2:4]))
+ }
+
+ // OK, parse data.
+ if _, err = r.UnmarshalMsg(data[4:]); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (r *rebalanceMeta) saveWithOpts(ctx context.Context, store objectIO, opts ObjectOptions) error {
+ if r == nil {
+ return nil
+ }
+
+ data := make([]byte, 4, r.Msgsize()+4)
+
+ // Initialize the header.
+ binary.LittleEndian.PutUint16(data[0:2], rebalMetaFmt)
+ binary.LittleEndian.PutUint16(data[2:4], rebalMetaVer)
+
+ buf, err := r.MarshalMsg(data)
+ if err != nil {
+ return err
+ }
+
+ return saveConfigWithOpts(ctx, store, rebalMetaName, buf, opts)
+}
+
+func (r *rebalanceMeta) save(ctx context.Context, store objectIO) error {
+ return r.saveWithOpts(ctx, store, ObjectOptions{})
+}
+
+func (z *erasureServerPools) IsRebalanceStarted(ctx context.Context) bool {
+ _ = z.loadRebalanceMeta(ctx)
+ z.rebalMu.RLock()
+ defer z.rebalMu.RUnlock()
+
+ r := z.rebalMeta
+ if r == nil {
+ return false
+ }
+ if !r.StoppedAt.IsZero() {
+ return false
+ }
+ for _, ps := range r.PoolStats {
+ if ps.Participating && ps.Info.Status != rebalCompleted {
+ return true
+ }
+ }
+ return false
+}
+
+func (z *erasureServerPools) IsPoolRebalancing(poolIndex int) bool {
+ z.rebalMu.RLock()
+ defer z.rebalMu.RUnlock()
+
+ if r := z.rebalMeta; r != nil {
+ if !r.StoppedAt.IsZero() {
+ return false
+ }
+ ps := r.PoolStats[poolIndex]
+ return ps.Participating && ps.Info.Status == rebalStarted
+ }
+ return false
+}
+
+func (z *erasureServerPools) rebalanceBuckets(ctx context.Context, poolIdx int) (err error) {
+ doneCh := make(chan error, 1)
+ defer xioutil.SafeClose(doneCh)
+
+ // Save rebalance.bin periodically.
+ go func() {
+ // Update rebalance.bin periodically once every 5-10s, chosen randomly
+ // to avoid multiple pool leaders herding to update around the same
+ // time.
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+ randSleepFor := func() time.Duration {
+ return 5*time.Second + time.Duration(float64(5*time.Second)*r.Float64())
+ }
+
+ timer := time.NewTimer(randSleepFor())
+ defer timer.Stop()
+
+ var (
+ quit bool
+ traceMsg string
+ notify bool // if status changed, notify nodes to reload rebalance metadata
+ )
+
+ for {
+ select {
+ case rebalErr := <-doneCh:
+ quit = true
+ notify = true
+ now := time.Now()
+ var status rebalStatus
+
+ switch {
+ case errors.Is(rebalErr, context.Canceled):
+ status = rebalStopped
+ traceMsg = fmt.Sprintf("stopped at %s", now)
+ case rebalErr == nil:
+ status = rebalCompleted
+ traceMsg = fmt.Sprintf("completed at %s", now)
+ default:
+ status = rebalFailed
+ traceMsg = fmt.Sprintf("stopped at %s with err: %v", now, rebalErr)
+ }
+
+ z.rebalMu.Lock()
+ z.rebalMeta.PoolStats[poolIdx].Info.Status = status
+ z.rebalMeta.PoolStats[poolIdx].Info.EndTime = now
+ z.rebalMu.Unlock()
+
+ case <-timer.C:
+ notify = false
+ traceMsg = fmt.Sprintf("saved at %s", time.Now())
+ }
+
+ stopFn := globalRebalanceMetrics.log(rebalanceMetricSaveMetadata, poolIdx, traceMsg)
+ err := z.saveRebalanceStats(GlobalContext, poolIdx, rebalSaveStats)
+ stopFn(0, err)
+ if err == nil && notify {
+ globalNotificationSys.LoadRebalanceMeta(GlobalContext, false)
+ }
+ rebalanceLogIf(GlobalContext, err)
+
+ if quit {
+ return
+ }
+
+ timer.Reset(randSleepFor())
+ }
+ }()
+
+ rebalanceLogEvent(ctx, "Pool %d rebalancing is started", poolIdx+1)
+
+ for {
+ select {
+ case <-ctx.Done():
+ doneCh <- ctx.Err()
+ return err
+ default:
+ }
+
+ bucket, ok := z.nextRebalBucket(poolIdx)
+ if !ok {
+ // no more buckets to rebalance or target free_space/capacity reached
+ break
+ }
+
+ stopFn := globalRebalanceMetrics.log(rebalanceMetricRebalanceBucket, poolIdx, bucket)
+ if err = z.rebalanceBucket(ctx, bucket, poolIdx); err != nil {
+ stopFn(0, err)
+ if errors.Is(err, errServerNotInitialized) || errors.Is(err, errBucketMetadataNotInitialized) {
+ continue
+ }
+ rebalanceLogIf(GlobalContext, err)
+ doneCh <- err
+ return err
+ }
+ stopFn(0, nil)
+ z.bucketRebalanceDone(bucket, poolIdx)
+ }
+
+ rebalanceLogEvent(GlobalContext, "Pool %d rebalancing is done", poolIdx+1)
+
+ return err
+}
+
+func (z *erasureServerPools) checkIfRebalanceDone(poolIdx int) bool {
+ z.rebalMu.Lock()
+ defer z.rebalMu.Unlock()
+
+ // check if enough objects have been rebalanced
+ r := z.rebalMeta
+ poolStats := r.PoolStats[poolIdx]
+ if poolStats.Info.Status == rebalCompleted {
+ return true
+ }
+
+ pfi := float64(poolStats.InitFreeSpace+poolStats.Bytes) / float64(poolStats.InitCapacity)
+ // Mark pool rebalance as done if within 5% from PercentFreeGoal.
+ if diff := math.Abs(pfi - r.PercentFreeGoal); diff <= 0.05 {
+ r.PoolStats[poolIdx].Info.Status = rebalCompleted
+ r.PoolStats[poolIdx].Info.EndTime = time.Now()
+ return true
+ }
+
+ return false
+}
+
+func (set *erasureObjects) listObjectsToRebalance(ctx context.Context, bucketName string, fn func(entry metaCacheEntry)) error {
+ disks, _ := set.getOnlineDisksWithHealing(false)
+ if len(disks) == 0 {
+ return fmt.Errorf("no online drives found for set with endpoints %s", set.getEndpoints())
+ }
+
+ // However many we ask, versions must exist on ~50%
+ listingQuorum := (set.setDriveCount + 1) / 2
+
+ // How to resolve partial results.
+ resolver := metadataResolutionParams{
+ dirQuorum: listingQuorum, // make sure to capture all quorum ratios
+ objQuorum: listingQuorum, // make sure to capture all quorum ratios
+ bucket: bucketName,
+ }
+
+ err := listPathRaw(ctx, listPathRawOptions{
+ disks: disks,
+ bucket: bucketName,
+ recursive: true,
+ forwardTo: "",
+ minDisks: listingQuorum,
+ reportNotFound: false,
+ agreed: fn,
+ partial: func(entries metaCacheEntries, _ []error) {
+ entry, ok := entries.resolve(&resolver)
+ if ok {
+ fn(*entry)
+ }
+ },
+ finished: nil,
+ })
+ return err
+}
+
+// rebalanceBucket rebalances objects under bucket in poolIdx pool
+func (z *erasureServerPools) rebalanceBucket(ctx context.Context, bucket string, poolIdx int) (err error) {
+ ctx = logger.SetReqInfo(ctx, &logger.ReqInfo{})
+
+ var vc *versioning.Versioning
+ var lc *lifecycle.Lifecycle
+ var lr objectlock.Retention
+ var rcfg *replication.Config
+ if bucket != minioMetaBucket {
+ vc, err = globalBucketVersioningSys.Get(bucket)
+ if err != nil {
+ return err
+ }
+
+ // Check if the current bucket has a configured lifecycle policy
+ lc, err = globalLifecycleSys.Get(bucket)
+ if err != nil && !errors.Is(err, BucketLifecycleNotFound{Bucket: bucket}) {
+ return err
+ }
+
+ // Check if bucket is object locked.
+ lr, err = globalBucketObjectLockSys.Get(bucket)
+ if err != nil {
+ return err
+ }
+
+ rcfg, err = getReplicationConfig(ctx, bucket)
+ if err != nil {
+ return err
+ }
+ }
+
+ pool := z.serverPools[poolIdx]
+
+ const envRebalanceWorkers = "_MINIO_REBALANCE_WORKERS"
+ workerSize, err := env.GetInt(envRebalanceWorkers, len(pool.sets))
+ if err != nil {
+ rebalanceLogIf(ctx, fmt.Errorf("invalid workers value err: %v, defaulting to %d", err, len(pool.sets)))
+ workerSize = len(pool.sets)
+ }
+
+ // Each decom worker needs one List() goroutine/worker
+ // add that many extra workers.
+ workerSize += len(pool.sets)
+
+ wk, err := workers.New(workerSize)
+ if err != nil {
+ return err
+ }
+
+ for setIdx, set := range pool.sets {
+ filterLifecycle := func(bucket, object string, fi FileInfo) bool {
+ if lc == nil {
+ return false
+ }
+ versioned := vc != nil && vc.Versioned(object)
+ objInfo := fi.ToObjectInfo(bucket, object, versioned)
+
+ evt := evalActionFromLifecycle(ctx, *lc, lr, rcfg, objInfo)
+ if evt.Action.Delete() {
+ globalExpiryState.enqueueByDays(objInfo, evt, lcEventSrc_Rebal)
+ return true
+ }
+ return false
+ }
+
+ rebalanceEntry := func(entry metaCacheEntry) {
+ defer wk.Give()
+
+ if entry.isDir() {
+ return
+ }
+
+ // rebalance on poolIdx has reached its goal
+ if z.checkIfRebalanceDone(poolIdx) {
+ return
+ }
+
+ fivs, err := entry.fileInfoVersions(bucket)
+ if err != nil {
+ return
+ }
+
+ // We need a reversed order for rebalance,
+ // to create the appropriate stack.
+ versionsSorter(fivs.Versions).reverse()
+
+ var rebalanced, expired int
+ for _, version := range fivs.Versions {
+ stopFn := globalRebalanceMetrics.log(rebalanceMetricRebalanceObject, poolIdx, bucket, version.Name, version.VersionID)
+
+ // Skip transitioned objects for now. TBD
+ if version.IsRemote() {
+ stopFn(version.Size, errors.New("ILM Tiered version will be skipped for now"))
+ continue
+ }
+
+ // Apply lifecycle rules on the objects that are expired.
+ if filterLifecycle(bucket, version.Name, version) {
+ expired++
+ stopFn(version.Size, errors.New("ILM expired object/version will be skipped"))
+ continue
+ }
+
+ // any object with only single DEL marker we don't need
+ // to rebalance, just skip it, this also includes
+ // any other versions that have already expired.
+ remainingVersions := len(fivs.Versions) - expired
+ if version.Deleted && remainingVersions == 1 {
+ rebalanced++
+ stopFn(version.Size, errors.New("DELETE marked object with no other non-current versions will be skipped"))
+ continue
+ }
+
+ versionID := version.VersionID
+ if versionID == "" {
+ versionID = nullVersionID
+ }
+
+ var failure, ignore bool
+ if version.Deleted {
+ _, err := z.DeleteObject(ctx,
+ bucket,
+ version.Name,
+ ObjectOptions{
+ Versioned: true,
+ VersionID: versionID,
+ MTime: version.ModTime,
+ DeleteReplication: version.ReplicationState,
+ SrcPoolIdx: poolIdx,
+ DataMovement: true,
+ DeleteMarker: true, // make sure we create a delete marker
+ SkipRebalancing: true, // make sure we skip the decommissioned pool
+ NoAuditLog: true,
+ })
+ // This can happen when rebalance stop races with ongoing rebalance workers.
+ // These rebalance failures can be ignored.
+ if err != nil {
+ // This can happen when rebalance stop races with ongoing rebalance workers.
+ // These rebalance failures can be ignored.
+ if isErrObjectNotFound(err) || isErrVersionNotFound(err) || isDataMovementOverWriteErr(err) {
+ ignore = true
+ stopFn(0, nil)
+ continue
+ }
+ }
+ stopFn(version.Size, err)
+ rebalanceLogIf(ctx, err)
+ failure = err != nil
+ if !failure {
+ z.updatePoolStats(poolIdx, bucket, version)
+ rebalanced++
+ }
+ auditLogRebalance(ctx, "Rebalance:DeleteMarker", bucket, version.Name, versionID, err)
+ continue
+ }
+
+ for range 3 {
+ // GetObjectReader.Close is called by rebalanceObject
+ gr, err := set.GetObjectNInfo(ctx,
+ bucket,
+ encodeDirObject(version.Name),
+ nil,
+ http.Header{},
+ ObjectOptions{
+ VersionID: versionID,
+ NoDecryption: true,
+ NoLock: true,
+ NoAuditLog: true,
+ })
+ if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
+ // object deleted by the application, nothing to do here we move on.
+ ignore = true
+ stopFn(0, nil)
+ break
+ }
+ if err != nil {
+ failure = true
+ rebalanceLogIf(ctx, err)
+ stopFn(0, err)
+ continue
+ }
+
+ if err = z.rebalanceObject(ctx, poolIdx, bucket, gr); err != nil {
+ // This can happen when rebalance stop races with ongoing rebalance workers.
+ // These rebalance failures can be ignored.
+ if isErrObjectNotFound(err) || isErrVersionNotFound(err) || isDataMovementOverWriteErr(err) {
+ ignore = true
+ stopFn(0, nil)
+ break
+ }
+ failure = true
+ rebalanceLogIf(ctx, err)
+ stopFn(version.Size, err)
+ continue
+ }
+
+ stopFn(version.Size, nil)
+ failure = false
+ break
+ }
+ if ignore {
+ continue
+ }
+ if failure {
+ break // break out on first error
+ }
+ z.updatePoolStats(poolIdx, bucket, version)
+ rebalanced++
+ }
+
+ // if all versions were rebalanced, we can delete the object versions.
+ if rebalanced == len(fivs.Versions) {
+ stopFn := globalRebalanceMetrics.log(rebalanceMetricRebalanceRemoveObject, poolIdx, bucket, entry.name)
+ _, err := set.DeleteObject(ctx,
+ bucket,
+ encodeDirObject(entry.name),
+ ObjectOptions{
+ DeletePrefix: true, // use prefix delete to delete all versions at once.
+ DeletePrefixObject: true, // use prefix delete on exact object (this is an optimization to avoid fan-out calls)
+ NoAuditLog: true,
+ },
+ )
+ stopFn(0, err)
+ auditLogRebalance(ctx, "Rebalance:DeleteObject", bucket, entry.name, "", err)
+ if err != nil {
+ rebalanceLogIf(ctx, err)
+ }
+ }
+ }
+
+ wk.Take()
+ go func(setIdx int) {
+ defer wk.Give()
+ err := set.listObjectsToRebalance(ctx, bucket,
+ func(entry metaCacheEntry) {
+ wk.Take()
+ go rebalanceEntry(entry)
+ },
+ )
+ if err == nil || errors.Is(err, context.Canceled) {
+ return
+ }
+ setN := humanize.Ordinal(setIdx + 1)
+ rebalanceLogIf(ctx, fmt.Errorf("listing objects from %s set failed with %v", setN, err), "rebalance-listing-failed"+setN)
+ }(setIdx)
+ }
+
+ wk.Wait()
+ return nil
+}
+
+type rebalSaveOpts uint8
+
+const (
+ rebalSaveStats rebalSaveOpts = iota
+ rebalSaveStoppedAt
+)
+
+func (z *erasureServerPools) saveRebalanceStats(ctx context.Context, poolIdx int, opts rebalSaveOpts) error {
+ lock := z.serverPools[0].NewNSLock(minioMetaBucket, rebalMetaName)
+ lkCtx, err := lock.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ rebalanceLogIf(ctx, fmt.Errorf("failed to acquire write lock on %s/%s: %w", minioMetaBucket, rebalMetaName, err))
+ return err
+ }
+ defer lock.Unlock(lkCtx)
+
+ ctx = lkCtx.Context()
+ noLockOpts := ObjectOptions{NoLock: true}
+ r := &rebalanceMeta{}
+ err = r.loadWithOpts(ctx, z.serverPools[0], noLockOpts)
+ if err != nil && !errors.Is(err, errConfigNotFound) {
+ return err
+ }
+
+ z.rebalMu.Lock()
+ defer z.rebalMu.Unlock()
+
+ // if not found, we store the memory metadata back
+ // when rebalance status changed, will notify all nodes update status to memory, we can treat the memory metadata is the latest status
+ if errors.Is(err, errConfigNotFound) {
+ r = z.rebalMeta
+ }
+
+ switch opts {
+ case rebalSaveStoppedAt:
+ r.StoppedAt = time.Now()
+ case rebalSaveStats:
+ if z.rebalMeta != nil {
+ r.PoolStats[poolIdx] = z.rebalMeta.PoolStats[poolIdx]
+ }
+ }
+ z.rebalMeta = r
+
+ return z.rebalMeta.saveWithOpts(ctx, z.serverPools[0], noLockOpts)
+}
+
+func auditLogRebalance(ctx context.Context, apiName, bucket, object, versionID string, err error) {
+ errStr := ""
+ if err != nil {
+ errStr = err.Error()
+ }
+ auditLogInternal(ctx, AuditLogOptions{
+ Event: "rebalance",
+ APIName: apiName,
+ Bucket: bucket,
+ Object: object,
+ VersionID: versionID,
+ Error: errStr,
+ })
+}
+
+func (z *erasureServerPools) rebalanceObject(ctx context.Context, poolIdx int, bucket string, gr *GetObjectReader) (err error) {
+ oi := gr.ObjInfo
+
+ defer func() {
+ gr.Close()
+ auditLogRebalance(ctx, "RebalanceCopyData", oi.Bucket, oi.Name, oi.VersionID, err)
+ }()
+
+ actualSize, err := oi.GetActualSize()
+ if err != nil {
+ return err
+ }
+
+ if oi.isMultipart() {
+ res, err := z.NewMultipartUpload(ctx, bucket, oi.Name, ObjectOptions{
+ VersionID: oi.VersionID,
+ UserDefined: oi.UserDefined,
+ NoAuditLog: true,
+ DataMovement: true,
+ SrcPoolIdx: poolIdx,
+ })
+ if err != nil {
+ return fmt.Errorf("rebalanceObject: NewMultipartUpload() %w", err)
+ }
+ defer z.AbortMultipartUpload(ctx, bucket, oi.Name, res.UploadID, ObjectOptions{NoAuditLog: true})
+
+ parts := make([]CompletePart, len(oi.Parts))
+ for i, part := range oi.Parts {
+ hr, err := hash.NewReader(ctx, io.LimitReader(gr, part.Size), part.Size, "", "", part.ActualSize)
+ if err != nil {
+ return fmt.Errorf("rebalanceObject: hash.NewReader() %w", err)
+ }
+ pi, err := z.PutObjectPart(ctx, bucket, oi.Name, res.UploadID,
+ part.Number,
+ NewPutObjReader(hr),
+ ObjectOptions{
+ PreserveETag: part.ETag, // Preserve original ETag to ensure same metadata.
+ IndexCB: func() []byte {
+ return part.Index // Preserve part Index to ensure decompression works.
+ },
+ NoAuditLog: true,
+ })
+ if err != nil {
+ return fmt.Errorf("rebalanceObject: PutObjectPart() %w", err)
+ }
+ parts[i] = CompletePart{
+ ETag: pi.ETag,
+ PartNumber: pi.PartNumber,
+ }
+ }
+ _, err = z.CompleteMultipartUpload(ctx, bucket, oi.Name, res.UploadID, parts, ObjectOptions{
+ DataMovement: true,
+ MTime: oi.ModTime,
+ NoAuditLog: true,
+ })
+ if err != nil {
+ err = fmt.Errorf("rebalanceObject: CompleteMultipartUpload() %w", err)
+ }
+ return err
+ }
+
+ hr, err := hash.NewReader(ctx, gr, oi.Size, "", "", actualSize)
+ if err != nil {
+ return fmt.Errorf("rebalanceObject: hash.NewReader() %w", err)
+ }
+
+ _, err = z.PutObject(ctx,
+ bucket,
+ oi.Name,
+ NewPutObjReader(hr),
+ ObjectOptions{
+ SrcPoolIdx: poolIdx,
+ DataMovement: true,
+ VersionID: oi.VersionID,
+ MTime: oi.ModTime,
+ UserDefined: oi.UserDefined,
+ PreserveETag: oi.ETag, // Preserve original ETag to ensure same metadata.
+ IndexCB: func() []byte {
+ return oi.Parts[0].Index // Preserve part Index to ensure decompression works.
+ },
+ NoAuditLog: true,
+ })
+ if err != nil {
+ err = fmt.Errorf("rebalanceObject: PutObject() %w", err)
+ }
+ return err
+}
+
+func (z *erasureServerPools) StartRebalance() {
+ z.rebalMu.Lock()
+ if z.rebalMeta == nil || !z.rebalMeta.StoppedAt.IsZero() { // rebalance not running, nothing to do
+ z.rebalMu.Unlock()
+ return
+ }
+ ctx, cancel := context.WithCancel(GlobalContext)
+ z.rebalCancel = cancel // to be used when rebalance-stop is called
+ z.rebalMu.Unlock()
+
+ z.rebalMu.RLock()
+ participants := make([]bool, len(z.rebalMeta.PoolStats))
+ for i, ps := range z.rebalMeta.PoolStats {
+ // skip pools which have completed rebalancing
+ if ps.Info.Status != rebalStarted {
+ continue
+ }
+
+ participants[i] = ps.Participating
+ }
+ z.rebalMu.RUnlock()
+
+ for poolIdx, doRebalance := range participants {
+ if !doRebalance {
+ continue
+ }
+ // nothing to do if this node is not pool's first node (i.e pool's rebalance 'leader').
+ if !globalEndpoints[poolIdx].Endpoints[0].IsLocal {
+ continue
+ }
+
+ go func(idx int) {
+ stopfn := globalRebalanceMetrics.log(rebalanceMetricRebalanceBuckets, idx)
+ err := z.rebalanceBuckets(ctx, idx)
+ stopfn(0, err)
+ }(poolIdx)
+ }
+}
+
+// StopRebalance signals the rebalance goroutine running on this node (if any)
+// to stop, using the context.CancelFunc(s) saved at the time ofStartRebalance.
+func (z *erasureServerPools) StopRebalance() error {
+ z.rebalMu.Lock()
+ defer z.rebalMu.Unlock()
+
+ r := z.rebalMeta
+ if r == nil { // rebalance not running in this node, nothing to do
+ return nil
+ }
+
+ if cancel := z.rebalCancel; cancel != nil {
+ cancel()
+ z.rebalCancel = nil
+ }
+ return nil
+}
+
+// for rebalance trace support
+type rebalanceMetrics struct{}
+
+var globalRebalanceMetrics rebalanceMetrics
+
+//go:generate stringer -type=rebalanceMetric -trimprefix=rebalanceMetric $GOFILE
+type rebalanceMetric uint8
+
+const (
+ rebalanceMetricRebalanceBuckets rebalanceMetric = iota
+ rebalanceMetricRebalanceBucket
+ rebalanceMetricRebalanceObject
+ rebalanceMetricRebalanceRemoveObject
+ rebalanceMetricSaveMetadata
+)
+
+var errDataMovementSrcDstPoolSame = errors.New("source and destination pool are the same")
+
+func rebalanceTrace(r rebalanceMetric, poolIdx int, startTime time.Time, duration time.Duration, err error, path string, sz int64) madmin.TraceInfo {
+ var errStr string
+ if err != nil {
+ errStr = err.Error()
+ }
+ return madmin.TraceInfo{
+ TraceType: madmin.TraceRebalance,
+ Time: startTime,
+ NodeName: globalLocalNodeName,
+ FuncName: fmt.Sprintf("rebalance.%s (pool-id=%d)", r.String(), poolIdx),
+ Duration: duration,
+ Path: path,
+ Error: errStr,
+ Bytes: sz,
+ }
+}
+
+func (p *rebalanceMetrics) log(r rebalanceMetric, poolIdx int, paths ...string) func(sz int64, err error) {
+ startTime := time.Now()
+ return func(sz int64, err error) {
+ duration := time.Since(startTime)
+ if globalTrace.NumSubscribers(madmin.TraceRebalance) > 0 {
+ globalTrace.Publish(rebalanceTrace(r, poolIdx, startTime, duration, err, strings.Join(paths, " "), sz))
+ }
+ }
+}
diff --git a/cmd/erasure-server-pool-rebalance_gen.go b/cmd/erasure-server-pool-rebalance_gen.go
new file mode 100644
index 00000000..842adc11
--- /dev/null
+++ b/cmd/erasure-server-pool-rebalance_gen.go
@@ -0,0 +1,1230 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "github.com/tinylib/msgp/msgp"
+)
+
+// DecodeMsg implements msgp.Decodable
+func (z *rebalSaveOpts) DecodeMsg(dc *msgp.Reader) (err error) {
+ {
+ var zb0001 uint8
+ zb0001, err = dc.ReadUint8()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ (*z) = rebalSaveOpts(zb0001)
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z rebalSaveOpts) EncodeMsg(en *msgp.Writer) (err error) {
+ err = en.WriteUint8(uint8(z))
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z rebalSaveOpts) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ o = msgp.AppendUint8(o, uint8(z))
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *rebalSaveOpts) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ {
+ var zb0001 uint8
+ zb0001, bts, err = msgp.ReadUint8Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ (*z) = rebalSaveOpts(zb0001)
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z rebalSaveOpts) Msgsize() (s int) {
+ s = msgp.Uint8Size
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *rebalStatus) DecodeMsg(dc *msgp.Reader) (err error) {
+ {
+ var zb0001 uint8
+ zb0001, err = dc.ReadUint8()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ (*z) = rebalStatus(zb0001)
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z rebalStatus) EncodeMsg(en *msgp.Writer) (err error) {
+ err = en.WriteUint8(uint8(z))
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z rebalStatus) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ o = msgp.AppendUint8(o, uint8(z))
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *rebalStatus) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ {
+ var zb0001 uint8
+ zb0001, bts, err = msgp.ReadUint8Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ (*z) = rebalStatus(zb0001)
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z rebalStatus) Msgsize() (s int) {
+ s = msgp.Uint8Size
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *rebalanceInfo) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "startTs":
+ z.StartTime, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "StartTime")
+ return
+ }
+ case "stopTs":
+ z.EndTime, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "EndTime")
+ return
+ }
+ case "status":
+ {
+ var zb0002 uint8
+ zb0002, err = dc.ReadUint8()
+ if err != nil {
+ err = msgp.WrapError(err, "Status")
+ return
+ }
+ z.Status = rebalStatus(zb0002)
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z rebalanceInfo) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 3
+ // write "startTs"
+ err = en.Append(0x83, 0xa7, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.StartTime)
+ if err != nil {
+ err = msgp.WrapError(err, "StartTime")
+ return
+ }
+ // write "stopTs"
+ err = en.Append(0xa6, 0x73, 0x74, 0x6f, 0x70, 0x54, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.EndTime)
+ if err != nil {
+ err = msgp.WrapError(err, "EndTime")
+ return
+ }
+ // write "status"
+ err = en.Append(0xa6, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint8(uint8(z.Status))
+ if err != nil {
+ err = msgp.WrapError(err, "Status")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z rebalanceInfo) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 3
+ // string "startTs"
+ o = append(o, 0x83, 0xa7, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x73)
+ o = msgp.AppendTime(o, z.StartTime)
+ // string "stopTs"
+ o = append(o, 0xa6, 0x73, 0x74, 0x6f, 0x70, 0x54, 0x73)
+ o = msgp.AppendTime(o, z.EndTime)
+ // string "status"
+ o = append(o, 0xa6, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73)
+ o = msgp.AppendUint8(o, uint8(z.Status))
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *rebalanceInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "startTs":
+ z.StartTime, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "StartTime")
+ return
+ }
+ case "stopTs":
+ z.EndTime, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "EndTime")
+ return
+ }
+ case "status":
+ {
+ var zb0002 uint8
+ zb0002, bts, err = msgp.ReadUint8Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Status")
+ return
+ }
+ z.Status = rebalStatus(zb0002)
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z rebalanceInfo) Msgsize() (s int) {
+ s = 1 + 8 + msgp.TimeSize + 7 + msgp.TimeSize + 7 + msgp.Uint8Size
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *rebalanceMeta) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "stopTs":
+ z.StoppedAt, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "StoppedAt")
+ return
+ }
+ case "id":
+ z.ID, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "ID")
+ return
+ }
+ case "pf":
+ z.PercentFreeGoal, err = dc.ReadFloat64()
+ if err != nil {
+ err = msgp.WrapError(err, "PercentFreeGoal")
+ return
+ }
+ case "rss":
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "PoolStats")
+ return
+ }
+ if cap(z.PoolStats) >= int(zb0002) {
+ z.PoolStats = (z.PoolStats)[:zb0002]
+ } else {
+ z.PoolStats = make([]*rebalanceStats, zb0002)
+ }
+ for za0001 := range z.PoolStats {
+ if dc.IsNil() {
+ err = dc.ReadNil()
+ if err != nil {
+ err = msgp.WrapError(err, "PoolStats", za0001)
+ return
+ }
+ z.PoolStats[za0001] = nil
+ } else {
+ if z.PoolStats[za0001] == nil {
+ z.PoolStats[za0001] = new(rebalanceStats)
+ }
+ err = z.PoolStats[za0001].DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "PoolStats", za0001)
+ return
+ }
+ }
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *rebalanceMeta) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 4
+ // write "stopTs"
+ err = en.Append(0x84, 0xa6, 0x73, 0x74, 0x6f, 0x70, 0x54, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.StoppedAt)
+ if err != nil {
+ err = msgp.WrapError(err, "StoppedAt")
+ return
+ }
+ // write "id"
+ err = en.Append(0xa2, 0x69, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.ID)
+ if err != nil {
+ err = msgp.WrapError(err, "ID")
+ return
+ }
+ // write "pf"
+ err = en.Append(0xa2, 0x70, 0x66)
+ if err != nil {
+ return
+ }
+ err = en.WriteFloat64(z.PercentFreeGoal)
+ if err != nil {
+ err = msgp.WrapError(err, "PercentFreeGoal")
+ return
+ }
+ // write "rss"
+ err = en.Append(0xa3, 0x72, 0x73, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(len(z.PoolStats)))
+ if err != nil {
+ err = msgp.WrapError(err, "PoolStats")
+ return
+ }
+ for za0001 := range z.PoolStats {
+ if z.PoolStats[za0001] == nil {
+ err = en.WriteNil()
+ if err != nil {
+ return
+ }
+ } else {
+ err = z.PoolStats[za0001].EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "PoolStats", za0001)
+ return
+ }
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *rebalanceMeta) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 4
+ // string "stopTs"
+ o = append(o, 0x84, 0xa6, 0x73, 0x74, 0x6f, 0x70, 0x54, 0x73)
+ o = msgp.AppendTime(o, z.StoppedAt)
+ // string "id"
+ o = append(o, 0xa2, 0x69, 0x64)
+ o = msgp.AppendString(o, z.ID)
+ // string "pf"
+ o = append(o, 0xa2, 0x70, 0x66)
+ o = msgp.AppendFloat64(o, z.PercentFreeGoal)
+ // string "rss"
+ o = append(o, 0xa3, 0x72, 0x73, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.PoolStats)))
+ for za0001 := range z.PoolStats {
+ if z.PoolStats[za0001] == nil {
+ o = msgp.AppendNil(o)
+ } else {
+ o, err = z.PoolStats[za0001].MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "PoolStats", za0001)
+ return
+ }
+ }
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *rebalanceMeta) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "stopTs":
+ z.StoppedAt, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "StoppedAt")
+ return
+ }
+ case "id":
+ z.ID, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ID")
+ return
+ }
+ case "pf":
+ z.PercentFreeGoal, bts, err = msgp.ReadFloat64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "PercentFreeGoal")
+ return
+ }
+ case "rss":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "PoolStats")
+ return
+ }
+ if cap(z.PoolStats) >= int(zb0002) {
+ z.PoolStats = (z.PoolStats)[:zb0002]
+ } else {
+ z.PoolStats = make([]*rebalanceStats, zb0002)
+ }
+ for za0001 := range z.PoolStats {
+ if msgp.IsNil(bts) {
+ bts, err = msgp.ReadNilBytes(bts)
+ if err != nil {
+ return
+ }
+ z.PoolStats[za0001] = nil
+ } else {
+ if z.PoolStats[za0001] == nil {
+ z.PoolStats[za0001] = new(rebalanceStats)
+ }
+ bts, err = z.PoolStats[za0001].UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "PoolStats", za0001)
+ return
+ }
+ }
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *rebalanceMeta) Msgsize() (s int) {
+ s = 1 + 7 + msgp.TimeSize + 3 + msgp.StringPrefixSize + len(z.ID) + 3 + msgp.Float64Size + 4 + msgp.ArrayHeaderSize
+ for za0001 := range z.PoolStats {
+ if z.PoolStats[za0001] == nil {
+ s += msgp.NilSize
+ } else {
+ s += z.PoolStats[za0001].Msgsize()
+ }
+ }
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *rebalanceMetric) DecodeMsg(dc *msgp.Reader) (err error) {
+ {
+ var zb0001 uint8
+ zb0001, err = dc.ReadUint8()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ (*z) = rebalanceMetric(zb0001)
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z rebalanceMetric) EncodeMsg(en *msgp.Writer) (err error) {
+ err = en.WriteUint8(uint8(z))
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z rebalanceMetric) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ o = msgp.AppendUint8(o, uint8(z))
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *rebalanceMetric) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ {
+ var zb0001 uint8
+ zb0001, bts, err = msgp.ReadUint8Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ (*z) = rebalanceMetric(zb0001)
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z rebalanceMetric) Msgsize() (s int) {
+ s = msgp.Uint8Size
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *rebalanceMetrics) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z rebalanceMetrics) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 0
+ _ = z
+ err = en.Append(0x80)
+ if err != nil {
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z rebalanceMetrics) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 0
+ _ = z
+ o = append(o, 0x80)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *rebalanceMetrics) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z rebalanceMetrics) Msgsize() (s int) {
+ s = 1
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *rebalanceStats) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ifs":
+ z.InitFreeSpace, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "InitFreeSpace")
+ return
+ }
+ case "ic":
+ z.InitCapacity, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "InitCapacity")
+ return
+ }
+ case "bus":
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Buckets")
+ return
+ }
+ if cap(z.Buckets) >= int(zb0002) {
+ z.Buckets = (z.Buckets)[:zb0002]
+ } else {
+ z.Buckets = make([]string, zb0002)
+ }
+ for za0001 := range z.Buckets {
+ z.Buckets[za0001], err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Buckets", za0001)
+ return
+ }
+ }
+ case "rbs":
+ var zb0003 uint32
+ zb0003, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "RebalancedBuckets")
+ return
+ }
+ if cap(z.RebalancedBuckets) >= int(zb0003) {
+ z.RebalancedBuckets = (z.RebalancedBuckets)[:zb0003]
+ } else {
+ z.RebalancedBuckets = make([]string, zb0003)
+ }
+ for za0002 := range z.RebalancedBuckets {
+ z.RebalancedBuckets[za0002], err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "RebalancedBuckets", za0002)
+ return
+ }
+ }
+ case "bu":
+ z.Bucket, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "ob":
+ z.Object, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ case "no":
+ z.NumObjects, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "NumObjects")
+ return
+ }
+ case "nv":
+ z.NumVersions, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "NumVersions")
+ return
+ }
+ case "bs":
+ z.Bytes, err = dc.ReadUint64()
+ if err != nil {
+ err = msgp.WrapError(err, "Bytes")
+ return
+ }
+ case "par":
+ z.Participating, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "Participating")
+ return
+ }
+ case "inf":
+ err = z.Info.DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, "Info")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *rebalanceStats) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 11
+ // write "ifs"
+ err = en.Append(0x8b, 0xa3, 0x69, 0x66, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.InitFreeSpace)
+ if err != nil {
+ err = msgp.WrapError(err, "InitFreeSpace")
+ return
+ }
+ // write "ic"
+ err = en.Append(0xa2, 0x69, 0x63)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.InitCapacity)
+ if err != nil {
+ err = msgp.WrapError(err, "InitCapacity")
+ return
+ }
+ // write "bus"
+ err = en.Append(0xa3, 0x62, 0x75, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(len(z.Buckets)))
+ if err != nil {
+ err = msgp.WrapError(err, "Buckets")
+ return
+ }
+ for za0001 := range z.Buckets {
+ err = en.WriteString(z.Buckets[za0001])
+ if err != nil {
+ err = msgp.WrapError(err, "Buckets", za0001)
+ return
+ }
+ }
+ // write "rbs"
+ err = en.Append(0xa3, 0x72, 0x62, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(len(z.RebalancedBuckets)))
+ if err != nil {
+ err = msgp.WrapError(err, "RebalancedBuckets")
+ return
+ }
+ for za0002 := range z.RebalancedBuckets {
+ err = en.WriteString(z.RebalancedBuckets[za0002])
+ if err != nil {
+ err = msgp.WrapError(err, "RebalancedBuckets", za0002)
+ return
+ }
+ }
+ // write "bu"
+ err = en.Append(0xa2, 0x62, 0x75)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Bucket)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ // write "ob"
+ err = en.Append(0xa2, 0x6f, 0x62)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Object)
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ // write "no"
+ err = en.Append(0xa2, 0x6e, 0x6f)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.NumObjects)
+ if err != nil {
+ err = msgp.WrapError(err, "NumObjects")
+ return
+ }
+ // write "nv"
+ err = en.Append(0xa2, 0x6e, 0x76)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.NumVersions)
+ if err != nil {
+ err = msgp.WrapError(err, "NumVersions")
+ return
+ }
+ // write "bs"
+ err = en.Append(0xa2, 0x62, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint64(z.Bytes)
+ if err != nil {
+ err = msgp.WrapError(err, "Bytes")
+ return
+ }
+ // write "par"
+ err = en.Append(0xa3, 0x70, 0x61, 0x72)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.Participating)
+ if err != nil {
+ err = msgp.WrapError(err, "Participating")
+ return
+ }
+ // write "inf"
+ err = en.Append(0xa3, 0x69, 0x6e, 0x66)
+ if err != nil {
+ return
+ }
+ err = z.Info.EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, "Info")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *rebalanceStats) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 11
+ // string "ifs"
+ o = append(o, 0x8b, 0xa3, 0x69, 0x66, 0x73)
+ o = msgp.AppendUint64(o, z.InitFreeSpace)
+ // string "ic"
+ o = append(o, 0xa2, 0x69, 0x63)
+ o = msgp.AppendUint64(o, z.InitCapacity)
+ // string "bus"
+ o = append(o, 0xa3, 0x62, 0x75, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.Buckets)))
+ for za0001 := range z.Buckets {
+ o = msgp.AppendString(o, z.Buckets[za0001])
+ }
+ // string "rbs"
+ o = append(o, 0xa3, 0x72, 0x62, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.RebalancedBuckets)))
+ for za0002 := range z.RebalancedBuckets {
+ o = msgp.AppendString(o, z.RebalancedBuckets[za0002])
+ }
+ // string "bu"
+ o = append(o, 0xa2, 0x62, 0x75)
+ o = msgp.AppendString(o, z.Bucket)
+ // string "ob"
+ o = append(o, 0xa2, 0x6f, 0x62)
+ o = msgp.AppendString(o, z.Object)
+ // string "no"
+ o = append(o, 0xa2, 0x6e, 0x6f)
+ o = msgp.AppendUint64(o, z.NumObjects)
+ // string "nv"
+ o = append(o, 0xa2, 0x6e, 0x76)
+ o = msgp.AppendUint64(o, z.NumVersions)
+ // string "bs"
+ o = append(o, 0xa2, 0x62, 0x73)
+ o = msgp.AppendUint64(o, z.Bytes)
+ // string "par"
+ o = append(o, 0xa3, 0x70, 0x61, 0x72)
+ o = msgp.AppendBool(o, z.Participating)
+ // string "inf"
+ o = append(o, 0xa3, 0x69, 0x6e, 0x66)
+ o, err = z.Info.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Info")
+ return
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *rebalanceStats) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ifs":
+ z.InitFreeSpace, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "InitFreeSpace")
+ return
+ }
+ case "ic":
+ z.InitCapacity, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "InitCapacity")
+ return
+ }
+ case "bus":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Buckets")
+ return
+ }
+ if cap(z.Buckets) >= int(zb0002) {
+ z.Buckets = (z.Buckets)[:zb0002]
+ } else {
+ z.Buckets = make([]string, zb0002)
+ }
+ for za0001 := range z.Buckets {
+ z.Buckets[za0001], bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Buckets", za0001)
+ return
+ }
+ }
+ case "rbs":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "RebalancedBuckets")
+ return
+ }
+ if cap(z.RebalancedBuckets) >= int(zb0003) {
+ z.RebalancedBuckets = (z.RebalancedBuckets)[:zb0003]
+ } else {
+ z.RebalancedBuckets = make([]string, zb0003)
+ }
+ for za0002 := range z.RebalancedBuckets {
+ z.RebalancedBuckets[za0002], bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "RebalancedBuckets", za0002)
+ return
+ }
+ }
+ case "bu":
+ z.Bucket, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "ob":
+ z.Object, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ case "no":
+ z.NumObjects, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "NumObjects")
+ return
+ }
+ case "nv":
+ z.NumVersions, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "NumVersions")
+ return
+ }
+ case "bs":
+ z.Bytes, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Bytes")
+ return
+ }
+ case "par":
+ z.Participating, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Participating")
+ return
+ }
+ case "inf":
+ bts, err = z.Info.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Info")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *rebalanceStats) Msgsize() (s int) {
+ s = 1 + 4 + msgp.Uint64Size + 3 + msgp.Uint64Size + 4 + msgp.ArrayHeaderSize
+ for za0001 := range z.Buckets {
+ s += msgp.StringPrefixSize + len(z.Buckets[za0001])
+ }
+ s += 4 + msgp.ArrayHeaderSize
+ for za0002 := range z.RebalancedBuckets {
+ s += msgp.StringPrefixSize + len(z.RebalancedBuckets[za0002])
+ }
+ s += 3 + msgp.StringPrefixSize + len(z.Bucket) + 3 + msgp.StringPrefixSize + len(z.Object) + 3 + msgp.Uint64Size + 3 + msgp.Uint64Size + 3 + msgp.Uint64Size + 4 + msgp.BoolSize + 4 + z.Info.Msgsize()
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *rstats) DecodeMsg(dc *msgp.Reader) (err error) {
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if cap((*z)) >= int(zb0002) {
+ (*z) = (*z)[:zb0002]
+ } else {
+ (*z) = make(rstats, zb0002)
+ }
+ for zb0001 := range *z {
+ if dc.IsNil() {
+ err = dc.ReadNil()
+ if err != nil {
+ err = msgp.WrapError(err, zb0001)
+ return
+ }
+ (*z)[zb0001] = nil
+ } else {
+ if (*z)[zb0001] == nil {
+ (*z)[zb0001] = new(rebalanceStats)
+ }
+ err = (*z)[zb0001].DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, zb0001)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z rstats) EncodeMsg(en *msgp.Writer) (err error) {
+ err = en.WriteArrayHeader(uint32(len(z)))
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0003 := range z {
+ if z[zb0003] == nil {
+ err = en.WriteNil()
+ if err != nil {
+ return
+ }
+ } else {
+ err = z[zb0003].EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, zb0003)
+ return
+ }
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z rstats) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ o = msgp.AppendArrayHeader(o, uint32(len(z)))
+ for zb0003 := range z {
+ if z[zb0003] == nil {
+ o = msgp.AppendNil(o)
+ } else {
+ o, err = z[zb0003].MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, zb0003)
+ return
+ }
+ }
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *rstats) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if cap((*z)) >= int(zb0002) {
+ (*z) = (*z)[:zb0002]
+ } else {
+ (*z) = make(rstats, zb0002)
+ }
+ for zb0001 := range *z {
+ if msgp.IsNil(bts) {
+ bts, err = msgp.ReadNilBytes(bts)
+ if err != nil {
+ return
+ }
+ (*z)[zb0001] = nil
+ } else {
+ if (*z)[zb0001] == nil {
+ (*z)[zb0001] = new(rebalanceStats)
+ }
+ bts, err = (*z)[zb0001].UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, zb0001)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z rstats) Msgsize() (s int) {
+ s = msgp.ArrayHeaderSize
+ for zb0003 := range z {
+ if z[zb0003] == nil {
+ s += msgp.NilSize
+ } else {
+ s += z[zb0003].Msgsize()
+ }
+ }
+ return
+}
diff --git a/cmd/erasure-server-pool-rebalance_gen_test.go b/cmd/erasure-server-pool-rebalance_gen_test.go
new file mode 100644
index 00000000..4365ccfa
--- /dev/null
+++ b/cmd/erasure-server-pool-rebalance_gen_test.go
@@ -0,0 +1,575 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+func TestMarshalUnmarshalrebalanceInfo(t *testing.T) {
+ v := rebalanceInfo{}
+ 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 BenchmarkMarshalMsgrebalanceInfo(b *testing.B) {
+ v := rebalanceInfo{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgrebalanceInfo(b *testing.B) {
+ v := rebalanceInfo{}
+ 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 BenchmarkUnmarshalrebalanceInfo(b *testing.B) {
+ v := rebalanceInfo{}
+ 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 TestEncodeDecoderebalanceInfo(t *testing.T) {
+ v := rebalanceInfo{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecoderebalanceInfo Msgsize() is inaccurate")
+ }
+
+ vn := rebalanceInfo{}
+ 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 BenchmarkEncoderebalanceInfo(b *testing.B) {
+ v := rebalanceInfo{}
+ 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 BenchmarkDecoderebalanceInfo(b *testing.B) {
+ v := rebalanceInfo{}
+ 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 TestMarshalUnmarshalrebalanceMeta(t *testing.T) {
+ v := rebalanceMeta{}
+ 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 BenchmarkMarshalMsgrebalanceMeta(b *testing.B) {
+ v := rebalanceMeta{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgrebalanceMeta(b *testing.B) {
+ v := rebalanceMeta{}
+ 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 BenchmarkUnmarshalrebalanceMeta(b *testing.B) {
+ v := rebalanceMeta{}
+ 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 TestEncodeDecoderebalanceMeta(t *testing.T) {
+ v := rebalanceMeta{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecoderebalanceMeta Msgsize() is inaccurate")
+ }
+
+ vn := rebalanceMeta{}
+ 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 BenchmarkEncoderebalanceMeta(b *testing.B) {
+ v := rebalanceMeta{}
+ 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 BenchmarkDecoderebalanceMeta(b *testing.B) {
+ v := rebalanceMeta{}
+ 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 TestMarshalUnmarshalrebalanceMetrics(t *testing.T) {
+ v := rebalanceMetrics{}
+ 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 BenchmarkMarshalMsgrebalanceMetrics(b *testing.B) {
+ v := rebalanceMetrics{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgrebalanceMetrics(b *testing.B) {
+ v := rebalanceMetrics{}
+ 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 BenchmarkUnmarshalrebalanceMetrics(b *testing.B) {
+ v := rebalanceMetrics{}
+ 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 TestEncodeDecoderebalanceMetrics(t *testing.T) {
+ v := rebalanceMetrics{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecoderebalanceMetrics Msgsize() is inaccurate")
+ }
+
+ vn := rebalanceMetrics{}
+ 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 BenchmarkEncoderebalanceMetrics(b *testing.B) {
+ v := rebalanceMetrics{}
+ 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 BenchmarkDecoderebalanceMetrics(b *testing.B) {
+ v := rebalanceMetrics{}
+ 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 TestMarshalUnmarshalrebalanceStats(t *testing.T) {
+ v := rebalanceStats{}
+ 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 BenchmarkMarshalMsgrebalanceStats(b *testing.B) {
+ v := rebalanceStats{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgrebalanceStats(b *testing.B) {
+ v := rebalanceStats{}
+ 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 BenchmarkUnmarshalrebalanceStats(b *testing.B) {
+ v := rebalanceStats{}
+ 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 TestEncodeDecoderebalanceStats(t *testing.T) {
+ v := rebalanceStats{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecoderebalanceStats Msgsize() is inaccurate")
+ }
+
+ vn := rebalanceStats{}
+ 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 BenchmarkEncoderebalanceStats(b *testing.B) {
+ v := rebalanceStats{}
+ 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 BenchmarkDecoderebalanceStats(b *testing.B) {
+ v := rebalanceStats{}
+ 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 TestMarshalUnmarshalrstats(t *testing.T) {
+ v := rstats{}
+ 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 BenchmarkMarshalMsgrstats(b *testing.B) {
+ v := rstats{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgrstats(b *testing.B) {
+ v := rstats{}
+ 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 BenchmarkUnmarshalrstats(b *testing.B) {
+ v := rstats{}
+ 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 TestEncodeDecoderstats(t *testing.T) {
+ v := rstats{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecoderstats Msgsize() is inaccurate")
+ }
+
+ vn := rstats{}
+ 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 BenchmarkEncoderstats(b *testing.B) {
+ v := rstats{}
+ 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 BenchmarkDecoderstats(b *testing.B) {
+ v := rstats{}
+ 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)
+ }
+ }
+}
diff --git a/cmd/erasure-server-pool.go b/cmd/erasure-server-pool.go
new file mode 100644
index 00000000..6b12ec4f
--- /dev/null
+++ b/cmd/erasure-server-pool.go
@@ -0,0 +1,3005 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "math/rand"
+ "net/http"
+ "path"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/dustin/go-humanize"
+ "github.com/google/uuid"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio-go/v7/pkg/s3utils"
+ "github.com/minio/minio-go/v7/pkg/set"
+ "github.com/minio/minio-go/v7/pkg/tags"
+ "github.com/minio/minio/internal/bpool"
+ "github.com/minio/minio/internal/cachevalue"
+ "github.com/minio/minio/internal/config/storageclass"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/sync/errgroup"
+ "github.com/minio/pkg/v3/wildcard"
+ "github.com/minio/pkg/v3/workers"
+ "github.com/puzpuzpuz/xsync/v3"
+)
+
+type erasureServerPools struct {
+ poolMetaMutex sync.RWMutex
+ poolMeta poolMeta
+
+ rebalMu sync.RWMutex
+ rebalMeta *rebalanceMeta
+ rebalCancel context.CancelFunc
+
+ deploymentID [16]byte
+ distributionAlgo string
+
+ serverPools []*erasureSets
+
+ // Active decommission canceler
+ decommissionCancelers []context.CancelFunc
+
+ s3Peer *S3PeerSys
+
+ mpCache *xsync.MapOf[string, MultipartInfo]
+}
+
+func (z *erasureServerPools) SinglePool() bool {
+ return len(z.serverPools) == 1
+}
+
+// Initialize new pool of erasure sets.
+func newErasureServerPools(ctx context.Context, endpointServerPools EndpointServerPools) (ObjectLayer, error) {
+ var (
+ deploymentID string
+ commonParityDrives int
+ err error
+
+ formats = make([]*formatErasureV3, len(endpointServerPools))
+ storageDisks = make([][]StorageAPI, len(endpointServerPools))
+ z = &erasureServerPools{
+ serverPools: make([]*erasureSets, len(endpointServerPools)),
+ s3Peer: NewS3PeerSys(endpointServerPools),
+ distributionAlgo: formatErasureVersionV3DistributionAlgoV3,
+ }
+ )
+
+ // Maximum number of reusable buffers per node at any given point in time.
+ n := uint64(1024) // single node single/multiple drives set this to 1024 entries
+
+ if globalIsDistErasure {
+ n = 2048
+ }
+
+ // Avoid allocating more than half of the available memory
+ if maxN := availableMemory() / (blockSizeV2 * 2); n > maxN {
+ n = maxN
+ }
+
+ if globalIsCICD || strconv.IntSize == 32 {
+ n = 256 // 256MiB for CI/CD environments is sufficient or on 32bit platforms.
+ }
+
+ // Initialize byte pool once for all sets, bpool size is set to
+ // setCount * setDriveCount with each memory upto blockSizeV2.
+ buffers := bpool.NewBytePoolCap(n, blockSizeV2, blockSizeV2*2)
+ if n >= 16384 {
+ // pre-populate buffers only n >= 16384 which is (32Gi/2Mi)
+ // for all setups smaller than this avoid pre-alloc.
+ buffers.Populate()
+ }
+ globalBytePoolCap.Store(buffers)
+
+ var localDrives []StorageAPI
+ local := endpointServerPools.FirstLocal()
+ for i, ep := range endpointServerPools {
+ // If storage class is not set during startup, default values are used
+ // -- Default for Reduced Redundancy Storage class is, parity = 2
+ // -- Default for Standard Storage class is, parity = 2 - disks 4, 5
+ // -- Default for Standard Storage class is, parity = 3 - disks 6, 7
+ // -- Default for Standard Storage class is, parity = 4 - disks 8 to 16
+ if commonParityDrives == 0 {
+ commonParityDrives, err = ecDrivesNoConfig(ep.DrivesPerSet)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if err = storageclass.ValidateParity(commonParityDrives, ep.DrivesPerSet); err != nil {
+ return nil, fmt.Errorf("parity validation returned an error: %w <- (%d, %d), for pool(%s)", err, commonParityDrives, ep.DrivesPerSet, humanize.Ordinal(i+1))
+ }
+
+ bootstrapTrace("waitForFormatErasure: loading disks", func() {
+ storageDisks[i], formats[i], err = waitForFormatErasure(local, ep.Endpoints, i+1,
+ ep.SetCount, ep.DrivesPerSet, deploymentID)
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ if deploymentID == "" {
+ // all pools should have same deployment ID
+ deploymentID = formats[i].ID
+ }
+
+ // Validate if users brought different DeploymentID pools.
+ if deploymentID != formats[i].ID {
+ return nil, fmt.Errorf("all pools must have same deployment ID - expected %s, got %s for pool(%s)", deploymentID, formats[i].ID, humanize.Ordinal(i+1))
+ }
+
+ bootstrapTrace(fmt.Sprintf("newErasureSets: initializing %s pool", humanize.Ordinal(i+1)), func() {
+ z.serverPools[i], err = newErasureSets(ctx, ep, storageDisks[i], formats[i], commonParityDrives, i)
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ if deploymentID != "" && bytes.Equal(z.deploymentID[:], []byte{}) {
+ z.deploymentID = uuid.MustParse(deploymentID)
+ }
+
+ for _, storageDisk := range storageDisks[i] {
+ if storageDisk != nil && storageDisk.IsLocal() {
+ localDrives = append(localDrives, storageDisk)
+ }
+ }
+ }
+
+ if !globalIsDistErasure {
+ globalLocalDrivesMu.Lock()
+ globalLocalDrivesMap = make(map[string]StorageAPI, len(localDrives))
+ for _, drive := range localDrives {
+ globalLocalDrivesMap[drive.Endpoint().String()] = drive
+ }
+ globalLocalDrivesMu.Unlock()
+ }
+
+ z.decommissionCancelers = make([]context.CancelFunc, len(z.serverPools))
+
+ // Initialize the pool meta, but set it to not save.
+ // When z.Init below has loaded the poolmeta will be initialized,
+ // and allowed to save.
+ z.poolMeta = newPoolMeta(z, poolMeta{})
+ z.poolMeta.dontSave = true
+
+ bootstrapTrace("newSharedLock", func() {
+ globalLeaderLock = newSharedLock(GlobalContext, z, "leader.lock")
+ })
+
+ // Start self healing after the object initialization
+ // so various tasks will be useful
+ bootstrapTrace("initAutoHeal", func() {
+ initAutoHeal(GlobalContext, z)
+ })
+
+ // initialize the object layer.
+ defer setObjectLayer(z)
+
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+ attempt := 1
+ for {
+ var err error
+ bootstrapTrace(fmt.Sprintf("poolMeta.Init: loading pool metadata, attempt: %d", attempt), func() {
+ err = z.Init(ctx) // Initializes all pools.
+ })
+ if err != nil {
+ if !configRetriableErrors(err) {
+ logger.Fatal(err, "Unable to initialize backend")
+ }
+ retry := time.Duration(r.Float64() * float64(5*time.Second))
+ storageLogIf(ctx, fmt.Errorf("Unable to initialize backend: %w, retrying in %s", err, retry))
+ time.Sleep(retry)
+ attempt++
+ continue
+ }
+ break
+ }
+
+ // initialize the incomplete uploads cache
+ z.mpCache = xsync.NewMapOf[string, MultipartInfo]()
+
+ go z.cleanupStaleMPCache(ctx)
+
+ return z, nil
+}
+
+func (z *erasureServerPools) cleanupStaleMPCache(ctx context.Context) {
+ timer := time.NewTimer(globalAPIConfig.getStaleUploadsCleanupInterval())
+ defer timer.Stop()
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-timer.C:
+ z.mpCache.Range(func(id string, info MultipartInfo) bool {
+ if time.Since(info.Initiated) >= globalAPIConfig.getStaleUploadsExpiry() {
+ z.mpCache.Delete(id)
+ // No need to notify to peers, each node will delete its own cache.
+ }
+ return true
+ })
+
+ // Reset for the next interval
+ timer.Reset(globalAPIConfig.getStaleUploadsCleanupInterval())
+ }
+ }
+}
+
+func (z *erasureServerPools) NewNSLock(bucket string, objects ...string) RWLocker {
+ return z.serverPools[0].NewNSLock(bucket, objects...)
+}
+
+// GetDisksID will return disks by their ID.
+func (z *erasureServerPools) GetDisksID(ids ...string) []StorageAPI {
+ idMap := make(map[string]struct{}, len(ids))
+ for _, id := range ids {
+ idMap[id] = struct{}{}
+ }
+ res := make([]StorageAPI, 0, len(idMap))
+ for _, s := range z.serverPools {
+ for _, set := range s.sets {
+ for _, disk := range set.getDisks() {
+ if disk == OfflineDisk {
+ continue
+ }
+ if id, _ := disk.GetDiskID(); id != "" {
+ if _, ok := idMap[id]; ok {
+ res = append(res, disk)
+ }
+ }
+ }
+ }
+ }
+ return res
+}
+
+// GetRawData will return all files with a given raw path to the callback.
+// Errors are ignored, only errors from the callback are returned.
+// For now only direct file paths are supported.
+func (z *erasureServerPools) GetRawData(ctx context.Context, volume, file string, fn func(r io.Reader, host string, disk string, filename string, info StatInfo) error) error {
+ found := 0
+ for _, s := range z.serverPools {
+ for _, set := range s.sets {
+ for _, disk := range set.getDisks() {
+ if disk == OfflineDisk {
+ continue
+ }
+ stats, err := disk.StatInfoFile(ctx, volume, file, true)
+ if err != nil {
+ continue
+ }
+ for _, si := range stats {
+ found++
+ var r io.ReadCloser
+ if !si.Dir {
+ r, err = disk.ReadFileStream(ctx, volume, si.Name, 0, si.Size)
+ if err != nil {
+ continue
+ }
+ } else {
+ r = io.NopCloser(bytes.NewBuffer([]byte{}))
+ }
+ // Keep disk path instead of ID, to ensure that the downloaded zip file can be
+ // easily automated with `minio server hostname{1...n}/disk{1...m}`.
+ err = fn(r, disk.Hostname(), disk.Endpoint().Path, pathJoin(volume, si.Name), si)
+ r.Close()
+ if err != nil {
+ return err
+ }
+ }
+ }
+ }
+ }
+ if found == 0 {
+ return errFileNotFound
+ }
+ return nil
+}
+
+// Return the disks belonging to the poolIdx, and setIdx.
+func (z *erasureServerPools) GetDisks(poolIdx, setIdx int) ([]StorageAPI, error) {
+ if poolIdx < len(z.serverPools) && setIdx < len(z.serverPools[poolIdx].sets) {
+ return z.serverPools[poolIdx].sets[setIdx].getDisks(), nil
+ }
+ return nil, fmt.Errorf("Matching pool %s, set %s not found", humanize.Ordinal(poolIdx+1), humanize.Ordinal(setIdx+1))
+}
+
+// Return the count of disks in each pool
+func (z *erasureServerPools) SetDriveCounts() []int {
+ setDriveCounts := make([]int, len(z.serverPools))
+ for i := range z.serverPools {
+ setDriveCounts[i] = z.serverPools[i].SetDriveCount()
+ }
+ return setDriveCounts
+}
+
+type serverPoolsAvailableSpace []poolAvailableSpace
+
+type poolAvailableSpace struct {
+ Index int
+ Available uint64 // in bytes
+ MaxUsedPct int // Used disk percentage of most filled disk, rounded down.
+}
+
+// TotalAvailable - total available space
+func (p serverPoolsAvailableSpace) TotalAvailable() uint64 {
+ total := uint64(0)
+ for _, z := range p {
+ total += z.Available
+ }
+ return total
+}
+
+// FilterMaxUsed will filter out any pools that has used percent bigger than max,
+// unless all have that, in which case all are preserved.
+func (p serverPoolsAvailableSpace) FilterMaxUsed(maxUsed int) {
+ // We aren't modifying p, only entries in it, so we don't need to receive a pointer.
+ if len(p) <= 1 {
+ // Nothing to do.
+ return
+ }
+ var ok bool
+ for _, z := range p {
+ if z.Available > 0 && z.MaxUsedPct < maxUsed {
+ ok = true
+ break
+ }
+ }
+ if !ok {
+ // All above limit.
+ // Do not modify
+ return
+ }
+
+ // Remove entries that are above.
+ for i, z := range p {
+ if z.Available > 0 && z.MaxUsedPct < maxUsed {
+ continue
+ }
+ p[i].Available = 0
+ }
+}
+
+// getAvailablePoolIdx will return an index that can hold size bytes.
+// -1 is returned if no serverPools have available space for the size given.
+func (z *erasureServerPools) getAvailablePoolIdx(ctx context.Context, bucket, object string, size int64) int {
+ serverPools := z.getServerPoolsAvailableSpace(ctx, bucket, object, size)
+ serverPools.FilterMaxUsed(100 - (100 * diskReserveFraction))
+ total := serverPools.TotalAvailable()
+ if total == 0 {
+ return -1
+ }
+ // choose when we reach this many
+ choose := rand.Uint64() % total
+ atTotal := uint64(0)
+ for _, pool := range serverPools {
+ atTotal += pool.Available
+ if atTotal > choose && pool.Available > 0 {
+ return pool.Index
+ }
+ }
+ // Should not happen, but print values just in case.
+ storageLogIf(ctx, fmt.Errorf("reached end of serverPools (total: %v, atTotal: %v, choose: %v)", total, atTotal, choose))
+ return -1
+}
+
+// getServerPoolsAvailableSpace will return the available space of each pool after storing the content.
+// If there is not enough space the pool will return 0 bytes available.
+// The size of each will be multiplied by the number of sets.
+// Negative sizes are seen as 0 bytes.
+func (z *erasureServerPools) getServerPoolsAvailableSpace(ctx context.Context, bucket, object string, size int64) serverPoolsAvailableSpace {
+ serverPools := make(serverPoolsAvailableSpace, len(z.serverPools))
+
+ storageInfos := make([][]*DiskInfo, len(z.serverPools))
+ nSets := make([]int, len(z.serverPools))
+ g := errgroup.WithNErrs(len(z.serverPools))
+ for index := range z.serverPools {
+ // Skip suspended pools or pools participating in rebalance for any new
+ // I/O.
+ if z.IsSuspended(index) || z.IsPoolRebalancing(index) {
+ continue
+ }
+ pool := z.serverPools[index]
+ nSets[index] = pool.setCount
+ g.Go(func() error {
+ // Get the set where it would be placed.
+ storageInfos[index] = getDiskInfos(ctx, pool.getHashedSet(object).getDisks()...)
+ return nil
+ }, index)
+ }
+
+ // Wait for the go routines.
+ g.Wait()
+
+ for i, zinfo := range storageInfos {
+ if zinfo == nil {
+ serverPools[i] = poolAvailableSpace{Index: i}
+ continue
+ }
+ var available uint64
+ if !isMinioMetaBucketName(bucket) {
+ if avail, err := hasSpaceFor(zinfo, size); err != nil || !avail {
+ serverPools[i] = poolAvailableSpace{Index: i}
+ continue
+ }
+ }
+ var maxUsedPct int
+ for _, disk := range zinfo {
+ if disk == nil || disk.Total == 0 {
+ continue
+ }
+ available += disk.Total - disk.Used
+
+ // set maxUsedPct to the value from the disk with the least space percentage.
+ if pctUsed := int(disk.Used * 100 / disk.Total); pctUsed > maxUsedPct {
+ maxUsedPct = pctUsed
+ }
+ }
+
+ // Since we are comparing pools that may have a different number of sets
+ // we multiply by the number of sets in the pool.
+ // This will compensate for differences in set sizes
+ // when choosing destination pool.
+ // Different set sizes are already compensated by less disks.
+ available *= uint64(nSets[i])
+
+ serverPools[i] = poolAvailableSpace{
+ Index: i,
+ Available: available,
+ MaxUsedPct: maxUsedPct,
+ }
+ }
+ return serverPools
+}
+
+// PoolObjInfo represents the state of current object version per pool
+type PoolObjInfo struct {
+ Index int
+ ObjInfo ObjectInfo
+ Err error
+}
+
+type poolErrs struct {
+ Index int
+ Err error
+}
+
+func (z *erasureServerPools) getPoolInfoExistingWithOpts(ctx context.Context, bucket, object string, opts ObjectOptions) (PoolObjInfo, []poolErrs, error) {
+ var noReadQuorumPools []poolErrs
+ poolObjInfos := make([]PoolObjInfo, len(z.serverPools))
+ poolOpts := make([]ObjectOptions, len(z.serverPools))
+ for i := range z.serverPools {
+ poolOpts[i] = opts
+ }
+
+ var wg sync.WaitGroup
+ for i, pool := range z.serverPools {
+ wg.Add(1)
+ go func(i int, pool *erasureSets, opts ObjectOptions) {
+ defer wg.Done()
+ // remember the pool index, we may sort the slice original index might be lost.
+ pinfo := PoolObjInfo{
+ Index: i,
+ }
+ // do not remove this check as it can lead to inconsistencies
+ // for all callers of bucket replication.
+ if !opts.MetadataChg {
+ opts.VersionID = ""
+ }
+ pinfo.ObjInfo, pinfo.Err = pool.GetObjectInfo(ctx, bucket, object, opts)
+ poolObjInfos[i] = pinfo
+ }(i, pool, poolOpts[i])
+ }
+ wg.Wait()
+
+ // Sort the objInfos such that we always serve latest
+ // this is a defensive change to handle any duplicate
+ // content that may have been created, we always serve
+ // the latest object.
+ sort.Slice(poolObjInfos, func(i, j int) bool {
+ mtime1 := poolObjInfos[i].ObjInfo.ModTime
+ mtime2 := poolObjInfos[j].ObjInfo.ModTime
+ return mtime1.After(mtime2)
+ })
+
+ defPool := PoolObjInfo{Index: -1}
+ for _, pinfo := range poolObjInfos {
+ // skip all objects from suspended pools if asked by the
+ // caller.
+ if opts.SkipDecommissioned && z.IsSuspended(pinfo.Index) {
+ continue
+ }
+ // Skip object if it's from pools participating in a rebalance operation.
+ if opts.SkipRebalancing && z.IsPoolRebalancing(pinfo.Index) {
+ continue
+ }
+ if pinfo.Err == nil {
+ // found a pool
+ return pinfo, z.poolsWithObject(poolObjInfos, opts), nil
+ }
+
+ if isErrReadQuorum(pinfo.Err) && !opts.MetadataChg {
+ // read quorum is returned when the object is visibly
+ // present but its unreadable, we simply ask the writes to
+ // schedule to this pool instead. If there is no quorum
+ // it will fail anyways, however if there is quorum available
+ // with enough disks online but sufficiently inconsistent to
+ // break parity threshold, allow them to be overwritten
+ // or allow new versions to be added.
+
+ return pinfo, z.poolsWithObject(poolObjInfos, opts), nil
+ }
+ defPool = pinfo
+ if !isErrObjectNotFound(pinfo.Err) && !isErrVersionNotFound(pinfo.Err) {
+ return pinfo, noReadQuorumPools, pinfo.Err
+ }
+
+ // No object exists or its a delete marker,
+ // check objInfo to confirm.
+ if pinfo.ObjInfo.DeleteMarker && pinfo.ObjInfo.Name != "" {
+ return pinfo, noReadQuorumPools, nil
+ }
+ }
+ if opts.ReplicationRequest && opts.DeleteMarker && defPool.Index >= 0 {
+ // If the request is a delete marker replication request, return a default pool
+ // in cases where the object does not exist.
+ // This is to ensure that the delete marker is replicated to the destination.
+ return defPool, noReadQuorumPools, nil
+ }
+ return PoolObjInfo{}, noReadQuorumPools, toObjectErr(errFileNotFound, bucket, object)
+}
+
+// return all pools with read quorum error or no error for an object with given opts.Note that this array is
+// returned in the order of recency of object ModTime.
+func (z *erasureServerPools) poolsWithObject(pools []PoolObjInfo, opts ObjectOptions) (errs []poolErrs) {
+ for _, pool := range pools {
+ if opts.SkipDecommissioned && z.IsSuspended(pool.Index) {
+ continue
+ }
+ // Skip object if it's from pools participating in a rebalance operation.
+ if opts.SkipRebalancing && z.IsPoolRebalancing(pool.Index) {
+ continue
+ }
+ if isErrReadQuorum(pool.Err) || pool.Err == nil {
+ errs = append(errs, poolErrs{Err: pool.Err, Index: pool.Index})
+ }
+ }
+ return errs
+}
+
+func (z *erasureServerPools) getPoolIdxExistingWithOpts(ctx context.Context, bucket, object string, opts ObjectOptions) (idx int, err error) {
+ pinfo, _, err := z.getPoolInfoExistingWithOpts(ctx, bucket, object, opts)
+ if err != nil {
+ return -1, err
+ }
+ return pinfo.Index, nil
+}
+
+// getPoolIdxExistingNoLock returns the (first) found object pool index containing an object.
+// If the object exists, but the latest version is a delete marker, the index with it is still returned.
+// If the object does not exist ObjectNotFound error is returned.
+// If any other error is found, it is returned.
+// The check is skipped if there is only one pool, and 0, nil is always returned in that case.
+func (z *erasureServerPools) getPoolIdxExistingNoLock(ctx context.Context, bucket, object string) (idx int, err error) {
+ return z.getPoolIdxExistingWithOpts(ctx, bucket, object, ObjectOptions{
+ NoLock: true,
+ SkipDecommissioned: true,
+ SkipRebalancing: true,
+ })
+}
+
+func (z *erasureServerPools) getPoolIdxNoLock(ctx context.Context, bucket, object string, size int64) (idx int, err error) {
+ idx, err = z.getPoolIdxExistingNoLock(ctx, bucket, object)
+ if err != nil && !isErrObjectNotFound(err) {
+ return idx, err
+ }
+
+ if isErrObjectNotFound(err) {
+ idx = z.getAvailablePoolIdx(ctx, bucket, object, size)
+ if idx < 0 {
+ return -1, toObjectErr(errDiskFull)
+ }
+ }
+
+ return idx, nil
+}
+
+// getPoolIdx returns the found previous object and its corresponding pool idx,
+// if none are found falls back to most available space pool, this function is
+// designed to be only used by PutObject, CopyObject (newObject creation) and NewMultipartUpload.
+func (z *erasureServerPools) getPoolIdx(ctx context.Context, bucket, object string, size int64) (idx int, err error) {
+ pinfo, _, err := z.getPoolInfoExistingWithOpts(ctx, bucket, object, ObjectOptions{
+ SkipDecommissioned: true,
+ SkipRebalancing: true,
+ })
+
+ if err != nil && !isErrObjectNotFound(err) {
+ return -1, err
+ }
+
+ idx = pinfo.Index
+ if isErrObjectNotFound(err) || pinfo.Err == nil {
+ // will generate a temp object
+ idx = z.getAvailablePoolIdx(ctx, bucket, object, size)
+ if idx < 0 {
+ return -1, toObjectErr(errDiskFull)
+ }
+ }
+
+ return idx, nil
+}
+
+func (z *erasureServerPools) Shutdown(ctx context.Context) error {
+ g := errgroup.WithNErrs(len(z.serverPools))
+
+ for index := range z.serverPools {
+ g.Go(func() error {
+ return z.serverPools[index].Shutdown(ctx)
+ }, index)
+ }
+
+ for _, err := range g.Wait() {
+ if err != nil {
+ storageLogIf(ctx, err)
+ }
+ // let's the rest shutdown
+ }
+ return nil
+}
+
+// Legacy returns 'true' if distribution algo is CRCMOD
+func (z *erasureServerPools) Legacy() (ok bool) {
+ ok = true
+ for _, set := range z.serverPools {
+ ok = ok && set.Legacy()
+ }
+ return ok
+}
+
+func (z *erasureServerPools) BackendInfo() (b madmin.BackendInfo) {
+ b.Type = madmin.Erasure
+
+ scParity := globalStorageClass.GetParityForSC(storageclass.STANDARD)
+ if scParity < 0 {
+ scParity = z.serverPools[0].defaultParityCount
+ }
+ rrSCParity := globalStorageClass.GetParityForSC(storageclass.RRS)
+
+ // Data blocks can vary per pool, but parity is same.
+ for i, setDriveCount := range z.SetDriveCounts() {
+ b.StandardSCData = append(b.StandardSCData, setDriveCount-scParity)
+ b.RRSCData = append(b.RRSCData, setDriveCount-rrSCParity)
+ b.DrivesPerSet = append(b.DrivesPerSet, setDriveCount)
+ b.TotalSets = append(b.TotalSets, z.serverPools[i].setCount)
+ }
+
+ b.StandardSCParity = scParity
+ b.RRSCParity = rrSCParity
+ return b
+}
+
+func (z *erasureServerPools) LocalStorageInfo(ctx context.Context, metrics bool) StorageInfo {
+ var storageInfo StorageInfo
+
+ storageInfos := make([]StorageInfo, len(z.serverPools))
+ g := errgroup.WithNErrs(len(z.serverPools))
+ for index := range z.serverPools {
+ g.Go(func() error {
+ storageInfos[index] = z.serverPools[index].LocalStorageInfo(ctx, metrics)
+ return nil
+ }, index)
+ }
+
+ // Wait for the go routines.
+ g.Wait()
+
+ storageInfo.Backend = z.BackendInfo()
+ for _, lstorageInfo := range storageInfos {
+ storageInfo.Disks = append(storageInfo.Disks, lstorageInfo.Disks...)
+ }
+
+ return storageInfo
+}
+
+func (z *erasureServerPools) StorageInfo(ctx context.Context, metrics bool) StorageInfo {
+ return globalNotificationSys.StorageInfo(ctx, z, metrics)
+}
+
+func (z *erasureServerPools) NSScanner(ctx context.Context, updates chan<- DataUsageInfo, wantCycle uint32, healScanMode madmin.HealScanMode) error {
+ // Updates must be closed before we return.
+ defer xioutil.SafeClose(updates)
+
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+ var results []dataUsageCache
+ var firstErr error
+
+ allBuckets, err := z.ListBuckets(ctx, BucketOptions{})
+ if err != nil {
+ return err
+ }
+
+ if len(allBuckets) == 0 {
+ updates <- DataUsageInfo{} // no buckets found update data usage to reflect latest state
+ return nil
+ }
+ totalResults := 0
+ resultIndex := -1
+ for _, z := range z.serverPools {
+ totalResults += len(z.sets)
+ }
+ results = make([]dataUsageCache, totalResults)
+ // Collect for each set in serverPools.
+ for _, z := range z.serverPools {
+ for _, erObj := range z.sets {
+ resultIndex++
+ wg.Add(1)
+ go func(i int, erObj *erasureObjects) {
+ updates := make(chan dataUsageCache, 1)
+ defer xioutil.SafeClose(updates)
+ // Start update collector.
+ go func() {
+ defer wg.Done()
+ for info := range updates {
+ mu.Lock()
+ results[i] = info
+ mu.Unlock()
+ }
+ }()
+ // Start scanner. Blocks until done.
+ err := erObj.nsScanner(ctx, allBuckets, wantCycle, updates, healScanMode)
+ if err != nil {
+ scannerLogIf(ctx, err)
+ mu.Lock()
+ if firstErr == nil {
+ firstErr = err
+ }
+ // Cancel remaining...
+ cancel()
+ mu.Unlock()
+ return
+ }
+ }(resultIndex, erObj)
+ }
+ }
+ updateCloser := make(chan chan struct{})
+ go func() {
+ updateTicker := time.NewTicker(30 * time.Second)
+ defer updateTicker.Stop()
+ var lastUpdate time.Time
+
+ // We need to merge since we will get the same buckets from each pool.
+ // Therefore to get the exact bucket sizes we must merge before we can convert.
+ var allMerged dataUsageCache
+
+ update := func() {
+ mu.Lock()
+ defer mu.Unlock()
+
+ allMerged = dataUsageCache{Info: dataUsageCacheInfo{Name: dataUsageRoot}}
+ for _, info := range results {
+ if info.Info.LastUpdate.IsZero() {
+ // Not filled yet.
+ return
+ }
+ allMerged.merge(info)
+ }
+ if allMerged.root() != nil && allMerged.Info.LastUpdate.After(lastUpdate) {
+ updates <- allMerged.dui(allMerged.Info.Name, allBuckets)
+ lastUpdate = allMerged.Info.LastUpdate
+ }
+ }
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case v := <-updateCloser:
+ update()
+ xioutil.SafeClose(v)
+ return
+ case <-updateTicker.C:
+ update()
+ }
+ }
+ }()
+
+ wg.Wait()
+ ch := make(chan struct{})
+ select {
+ case updateCloser <- ch:
+ <-ch
+ case <-ctx.Done():
+ mu.Lock()
+ if firstErr == nil {
+ firstErr = ctx.Err()
+ }
+ defer mu.Unlock()
+ }
+ return firstErr
+}
+
+// MakeBucket - creates a new bucket across all serverPools simultaneously
+// even if one of the sets fail to create buckets, we proceed all the successful
+// operations.
+func (z *erasureServerPools) MakeBucket(ctx context.Context, bucket string, opts MakeBucketOptions) error {
+ // Verify if bucket is valid.
+ if !isMinioMetaBucketName(bucket) {
+ if err := s3utils.CheckValidBucketNameStrict(bucket); err != nil {
+ return BucketNameInvalid{Bucket: bucket}
+ }
+
+ if !opts.NoLock {
+ // Lock the bucket name before creating.
+ lk := z.NewNSLock(minioMetaTmpBucket, bucket+".lck")
+ lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ return err
+ }
+
+ ctx = lkctx.Context()
+ defer lk.Unlock(lkctx)
+ }
+ }
+
+ if err := z.s3Peer.MakeBucket(ctx, bucket, opts); err != nil {
+ if _, ok := err.(BucketExists); !ok {
+ // Delete created buckets, ignoring errors.
+ z.DeleteBucket(context.Background(), bucket, DeleteBucketOptions{
+ NoLock: true,
+ NoRecreate: true,
+ })
+ }
+ return err
+ }
+
+ // If it doesn't exist we get a new, so ignore errors
+ meta := newBucketMetadata(bucket)
+ meta.SetCreatedAt(opts.CreatedAt)
+ if opts.LockEnabled {
+ meta.VersioningConfigXML = enabledBucketVersioningConfig
+ meta.ObjectLockConfigXML = enabledBucketObjectLockConfig
+ }
+
+ if opts.VersioningEnabled {
+ meta.VersioningConfigXML = enabledBucketVersioningConfig
+ }
+
+ if err := meta.Save(context.Background(), z); err != nil {
+ return toObjectErr(err, bucket)
+ }
+
+ globalBucketMetadataSys.Set(bucket, meta)
+
+ // Success.
+ return nil
+}
+
+func (z *erasureServerPools) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions) (gr *GetObjectReader, err error) {
+ if err = checkGetObjArgs(ctx, bucket, object); err != nil {
+ return nil, err
+ }
+
+ // This is a special call attempted first to check for SOS-API calls.
+ gr, err = veeamSOSAPIGetObject(ctx, bucket, object, rs, opts)
+ if err == nil {
+ return gr, nil
+ }
+
+ // reset any error to 'nil' and any reader to be 'nil'
+ gr = nil
+ err = nil
+
+ object = encodeDirObject(object)
+
+ if z.SinglePool() {
+ return z.serverPools[0].GetObjectNInfo(ctx, bucket, object, rs, h, opts)
+ }
+
+ var unlockOnDefer bool
+ nsUnlocker := func() {}
+ defer func() {
+ if unlockOnDefer {
+ nsUnlocker()
+ }
+ }()
+
+ // Acquire lock
+ if !opts.NoLock {
+ lock := z.NewNSLock(bucket, object)
+ lkctx, err := lock.GetRLock(ctx, globalOperationTimeout)
+ if err != nil {
+ return nil, err
+ }
+ ctx = lkctx.Context()
+ nsUnlocker = func() { lock.RUnlock(lkctx) }
+ unlockOnDefer = true
+ }
+
+ checkPrecondFn := opts.CheckPrecondFn
+ opts.CheckPrecondFn = nil // do not need to apply pre-conditions at lower layer.
+ opts.NoLock = true // no locks needed at lower levels for getObjectInfo()
+ objInfo, zIdx, err := z.getLatestObjectInfoWithIdx(ctx, bucket, object, opts)
+ if err != nil {
+ if objInfo.DeleteMarker {
+ if opts.VersionID == "" {
+ return &GetObjectReader{
+ ObjInfo: objInfo,
+ }, toObjectErr(errFileNotFound, bucket, object)
+ }
+ // Make sure to return object info to provide extra information.
+ return &GetObjectReader{
+ ObjInfo: objInfo,
+ }, toObjectErr(errMethodNotAllowed, bucket, object)
+ }
+ return nil, err
+ }
+
+ // check preconditions before reading the stream.
+ if checkPrecondFn != nil && checkPrecondFn(objInfo) {
+ return nil, PreConditionFailed{}
+ }
+
+ opts.NoLock = true
+ gr, err = z.serverPools[zIdx].GetObjectNInfo(ctx, bucket, object, rs, h, opts)
+ if err != nil {
+ return nil, err
+ }
+
+ if unlockOnDefer {
+ unlockOnDefer = gr.ObjInfo.Inlined
+ }
+
+ if !unlockOnDefer {
+ return gr.WithCleanupFuncs(nsUnlocker), nil
+ }
+ return gr, nil
+}
+
+// getLatestObjectInfoWithIdx returns the objectInfo of the latest object from multiple pools (this function
+// is present in-case there were duplicate writes to both pools, this function also returns the
+// additional index where the latest object exists, that is used to start the GetObject stream.
+func (z *erasureServerPools) getLatestObjectInfoWithIdx(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, int, error) {
+ object = encodeDirObject(object)
+ results := make([]struct {
+ zIdx int
+ oi ObjectInfo
+ err error
+ }, len(z.serverPools))
+ var wg sync.WaitGroup
+ for i, pool := range z.serverPools {
+ wg.Add(1)
+ go func(i int, pool *erasureSets) {
+ defer wg.Done()
+ results[i].zIdx = i
+ results[i].oi, results[i].err = pool.GetObjectInfo(ctx, bucket, object, opts)
+ }(i, pool)
+ }
+ wg.Wait()
+
+ // Sort the objInfos such that we always serve latest
+ // this is a defensive change to handle any duplicate
+ // content that may have been created, we always serve
+ // the latest object.
+ sort.Slice(results, func(i, j int) bool {
+ a, b := results[i], results[j]
+ if a.oi.ModTime.Equal(b.oi.ModTime) {
+ // On tiebreak, select the lowest pool index.
+ return a.zIdx < b.zIdx
+ }
+ return a.oi.ModTime.After(b.oi.ModTime)
+ })
+
+ for _, res := range results {
+ err := res.err
+ if err == nil {
+ return res.oi, res.zIdx, nil
+ }
+ if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
+ // some errors such as MethodNotAllowed for delete marker
+ // should be returned upwards.
+ return res.oi, res.zIdx, err
+ }
+ // When its a delete marker and versionID is empty
+ // we should simply return the error right away.
+ if res.oi.DeleteMarker && opts.VersionID == "" {
+ return res.oi, res.zIdx, err
+ }
+ }
+
+ object = decodeDirObject(object)
+ if opts.VersionID != "" {
+ return ObjectInfo{}, -1, VersionNotFound{Bucket: bucket, Object: object, VersionID: opts.VersionID}
+ }
+ return ObjectInfo{}, -1, ObjectNotFound{Bucket: bucket, Object: object}
+}
+
+func (z *erasureServerPools) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
+ if err = checkGetObjArgs(ctx, bucket, object); err != nil {
+ return objInfo, err
+ }
+
+ // This is a special call attempted first to check for SOS-API calls.
+ objInfo, err = veeamSOSAPIHeadObject(ctx, bucket, object, opts)
+ if err == nil {
+ return objInfo, nil
+ }
+
+ // reset any error to 'nil', and object info to be empty.
+ err = nil
+ objInfo = ObjectInfo{}
+
+ object = encodeDirObject(object)
+
+ if z.SinglePool() {
+ return z.serverPools[0].GetObjectInfo(ctx, bucket, object, opts)
+ }
+
+ if !opts.NoLock {
+ opts.NoLock = true // avoid taking locks at lower levels for multi-pool setups.
+
+ // Lock the object before reading.
+ lk := z.NewNSLock(bucket, object)
+ lkctx, err := lk.GetRLock(ctx, globalOperationTimeout)
+ if err != nil {
+ return ObjectInfo{}, err
+ }
+ ctx = lkctx.Context()
+ defer lk.RUnlock(lkctx)
+ }
+
+ objInfo, _, err = z.getLatestObjectInfoWithIdx(ctx, bucket, object, opts)
+ return objInfo, err
+}
+
+// PutObject - writes an object to least used erasure pool.
+func (z *erasureServerPools) PutObject(ctx context.Context, bucket string, object string, data *PutObjReader, opts ObjectOptions) (ObjectInfo, error) {
+ // Validate put object input args.
+ if err := checkPutObjectArgs(ctx, bucket, object); err != nil {
+ return ObjectInfo{}, err
+ }
+
+ object = encodeDirObject(object)
+ if z.SinglePool() {
+ _, err := z.getPoolIdx(ctx, bucket, object, data.Size())
+ if err != nil {
+ return ObjectInfo{}, err
+ }
+ return z.serverPools[0].PutObject(ctx, bucket, object, data, opts)
+ }
+
+ idx, err := z.getPoolIdx(ctx, bucket, object, data.Size())
+ if err != nil {
+ return ObjectInfo{}, err
+ }
+
+ if opts.DataMovement && idx == opts.SrcPoolIdx {
+ return ObjectInfo{}, DataMovementOverwriteErr{
+ Bucket: bucket,
+ Object: object,
+ VersionID: opts.VersionID,
+ Err: errDataMovementSrcDstPoolSame,
+ }
+ }
+
+ return z.serverPools[idx].PutObject(ctx, bucket, object, data, opts)
+}
+
+func (z *erasureServerPools) deletePrefix(ctx context.Context, bucket string, prefix string) error {
+ for _, pool := range z.serverPools {
+ if _, err := pool.DeleteObject(ctx, bucket, prefix, ObjectOptions{DeletePrefix: true}); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (z *erasureServerPools) DeleteObject(ctx context.Context, bucket string, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
+ if err = checkDelObjArgs(ctx, bucket, object); err != nil {
+ return objInfo, err
+ }
+
+ if !opts.DeletePrefix { // DeletePrefix handles dir object encoding differently.
+ object = encodeDirObject(object)
+ }
+
+ // Acquire a write lock before deleting the object.
+ lk := z.NewNSLock(bucket, object)
+ lkctx, err := lk.GetLock(ctx, globalDeleteOperationTimeout)
+ if err != nil {
+ return ObjectInfo{}, err
+ }
+ ctx = lkctx.Context()
+ defer lk.Unlock(lkctx)
+
+ if opts.DeletePrefix {
+ return ObjectInfo{}, z.deletePrefix(ctx, bucket, object)
+ }
+
+ gopts := opts
+ gopts.NoLock = true
+
+ pinfo, noReadQuorumPools, err := z.getPoolInfoExistingWithOpts(ctx, bucket, object, gopts)
+ if err != nil {
+ if _, ok := err.(InsufficientReadQuorum); ok {
+ return objInfo, InsufficientWriteQuorum{}
+ }
+ return objInfo, err
+ }
+
+ // Delete marker already present we are not going to create new delete markers.
+ if pinfo.ObjInfo.DeleteMarker && opts.VersionID == "" {
+ pinfo.ObjInfo.Name = decodeDirObject(object)
+ return pinfo.ObjInfo, nil
+ }
+
+ // Datamovement must never be allowed on the same pool.
+ if opts.DataMovement && opts.SrcPoolIdx == pinfo.Index {
+ return pinfo.ObjInfo, DataMovementOverwriteErr{
+ Bucket: bucket,
+ Object: object,
+ VersionID: opts.VersionID,
+ Err: errDataMovementSrcDstPoolSame,
+ }
+ }
+
+ if opts.DataMovement {
+ objInfo, err = z.serverPools[pinfo.Index].DeleteObject(ctx, bucket, object, opts)
+ objInfo.Name = decodeDirObject(object)
+ return objInfo, err
+ }
+
+ // Delete concurrently in all server pools with read quorum error for unversioned objects.
+ if len(noReadQuorumPools) > 0 && !opts.Versioned && !opts.VersionSuspended {
+ return z.deleteObjectFromAllPools(ctx, bucket, object, opts, noReadQuorumPools)
+ }
+
+ // All replication requests needs to go to pool with the object.
+ if opts.ReplicationRequest {
+ objInfo, err = z.serverPools[pinfo.Index].DeleteObject(ctx, bucket, object, opts)
+ objInfo.Name = decodeDirObject(object)
+ return objInfo, err
+ }
+
+ for _, pool := range z.serverPools {
+ objInfo, err := pool.DeleteObject(ctx, bucket, object, opts)
+ if err != nil && !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
+ objInfo.Name = decodeDirObject(object)
+ return objInfo, err
+ }
+ if err == nil {
+ objInfo.Name = decodeDirObject(object)
+ return objInfo, nil
+ }
+ }
+
+ objInfo.Name = decodeDirObject(object)
+ if opts.VersionID != "" {
+ return objInfo, VersionNotFound{Bucket: bucket, Object: object, VersionID: opts.VersionID}
+ }
+ return objInfo, ObjectNotFound{Bucket: bucket, Object: object}
+}
+
+func (z *erasureServerPools) deleteObjectFromAllPools(ctx context.Context, bucket string, object string, opts ObjectOptions, poolIndices []poolErrs) (objInfo ObjectInfo, err error) {
+ derrs := make([]error, len(poolIndices))
+ dobjects := make([]ObjectInfo, len(poolIndices))
+
+ // Delete concurrently in all server pools that reported no error or read quorum error
+ // where the read quorum issue is from metadata inconsistency.
+ var wg sync.WaitGroup
+ for idx, pe := range poolIndices {
+ if v, ok := pe.Err.(InsufficientReadQuorum); ok && v.Type != RQInconsistentMeta {
+ derrs[idx] = InsufficientWriteQuorum{}
+ continue
+ }
+ wg.Add(1)
+ pool := z.serverPools[pe.Index]
+ go func(idx int, pool *erasureSets) {
+ defer wg.Done()
+ dobjects[idx], derrs[idx] = pool.DeleteObject(ctx, bucket, object, opts)
+ }(idx, pool)
+ }
+ wg.Wait()
+
+ // the poolIndices array is pre-sorted in order of latest ModTime, we care only about pool with latest object though
+ // the delete call tries to clean up other pools during DeleteObject call.
+ objInfo = dobjects[0]
+ objInfo.Name = decodeDirObject(object)
+ err = derrs[0]
+ return objInfo, err
+}
+
+func (z *erasureServerPools) DeleteObjects(ctx context.Context, bucket string, objects []ObjectToDelete, opts ObjectOptions) ([]DeletedObject, []error) {
+ derrs := make([]error, len(objects))
+ dobjects := make([]DeletedObject, len(objects))
+ objSets := set.NewStringSet()
+ for i := range derrs {
+ objects[i].ObjectName = encodeDirObject(objects[i].ObjectName)
+
+ derrs[i] = checkDelObjArgs(ctx, bucket, objects[i].ObjectName)
+ objSets.Add(objects[i].ObjectName)
+ }
+
+ // Acquire a bulk write lock across 'objects'
+ multiDeleteLock := z.NewNSLock(bucket, objSets.ToSlice()...)
+ lkctx, err := multiDeleteLock.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ for i := range derrs {
+ derrs[i] = err
+ }
+ return dobjects, derrs
+ }
+ ctx = lkctx.Context()
+ defer multiDeleteLock.Unlock(lkctx)
+
+ dObjectsByPool := make([][]DeletedObject, len(z.serverPools))
+ dErrsByPool := make([][]error, len(z.serverPools))
+
+ eg := errgroup.WithNErrs(len(z.serverPools)).WithConcurrency(len(z.serverPools))
+ for i, pool := range z.serverPools {
+ pool := pool
+ eg.Go(func() error {
+ dObjectsByPool[i], dErrsByPool[i] = pool.DeleteObjects(ctx, bucket, objects, opts)
+ return nil
+ }, i)
+ }
+ eg.Wait() // wait to check all the pools.
+
+ for i := range dobjects {
+ // Iterate over pools
+ for pool := range z.serverPools {
+ if dErrsByPool[pool][i] == nil && dObjectsByPool[pool][i].found {
+ // A fast exit when the object is found and removed
+ dobjects[i] = dObjectsByPool[pool][i]
+ derrs[i] = nil
+ break
+ }
+ if derrs[i] == nil {
+ // No error related to this object is found, assign this pool result
+ // whether it is nil because there is no object found or because of
+ // some other errors such erasure quorum errors.
+ dobjects[i] = dObjectsByPool[pool][i]
+ derrs[i] = dErrsByPool[pool][i]
+ }
+ }
+ }
+
+ for i := range dobjects {
+ dobjects[i].ObjectName = decodeDirObject(dobjects[i].ObjectName)
+ }
+ return dobjects, derrs
+}
+
+func (z *erasureServerPools) CopyObject(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error) {
+ if err := checkCopyObjArgs(ctx, srcBucket, srcObject); err != nil {
+ return ObjectInfo{}, err
+ }
+ if err := checkCopyObjArgs(ctx, dstBucket, dstObject); err != nil {
+ return ObjectInfo{}, err
+ }
+
+ srcObject = encodeDirObject(srcObject)
+ dstObject = encodeDirObject(dstObject)
+
+ cpSrcDstSame := isStringEqual(pathJoin(srcBucket, srcObject), pathJoin(dstBucket, dstObject))
+
+ if !dstOpts.NoLock {
+ ns := z.NewNSLock(dstBucket, dstObject)
+ lkctx, err := ns.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ return ObjectInfo{}, err
+ }
+ ctx = lkctx.Context()
+ defer ns.Unlock(lkctx)
+ dstOpts.NoLock = true
+ }
+
+ poolIdx, err := z.getPoolIdxNoLock(ctx, dstBucket, dstObject, srcInfo.Size)
+ if err != nil {
+ return objInfo, err
+ }
+
+ if cpSrcDstSame && srcInfo.metadataOnly {
+ // Version ID is set for the destination and source == destination version ID.
+ if dstOpts.VersionID != "" && srcOpts.VersionID == dstOpts.VersionID {
+ return z.serverPools[poolIdx].CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts)
+ }
+ // Destination is not versioned and source version ID is empty
+ // perform an in-place update.
+ if !dstOpts.Versioned && srcOpts.VersionID == "" {
+ return z.serverPools[poolIdx].CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts)
+ }
+ // Destination is versioned, source is not destination version,
+ // as a special case look for if the source object is not legacy
+ // from older format, for older format we will rewrite them as
+ // newer using PutObject() - this is an optimization to save space
+ if dstOpts.Versioned && srcOpts.VersionID != dstOpts.VersionID && !srcInfo.Legacy {
+ // CopyObject optimization where we don't create an entire copy
+ // of the content, instead we add a reference.
+ srcInfo.versionOnly = true
+ return z.serverPools[poolIdx].CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts)
+ }
+ }
+
+ putOpts := ObjectOptions{
+ ServerSideEncryption: dstOpts.ServerSideEncryption,
+ UserDefined: srcInfo.UserDefined,
+ Versioned: dstOpts.Versioned,
+ VersionID: dstOpts.VersionID,
+ MTime: dstOpts.MTime,
+ NoLock: true,
+ EncryptFn: dstOpts.EncryptFn,
+ WantChecksum: dstOpts.WantChecksum,
+ WantServerSideChecksumType: dstOpts.WantServerSideChecksumType,
+ }
+
+ return z.serverPools[poolIdx].PutObject(ctx, dstBucket, dstObject, srcInfo.PutObjReader, putOpts)
+}
+
+func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsInfo, error) {
+ return z.listObjectsGeneric(ctx, bucket, prefix, marker, delimiter, maxKeys, true)
+}
+
+func (z *erasureServerPools) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (ListObjectsV2Info, error) {
+ marker := continuationToken
+ if marker == "" {
+ marker = startAfter
+ }
+
+ loi, err := z.listObjectsGeneric(ctx, bucket, prefix, marker, delimiter, maxKeys, false)
+ if err != nil {
+ return ListObjectsV2Info{}, err
+ }
+
+ listObjectsV2Info := ListObjectsV2Info{
+ IsTruncated: loi.IsTruncated,
+ ContinuationToken: continuationToken,
+ NextContinuationToken: loi.NextMarker,
+ Objects: loi.Objects,
+ Prefixes: loi.Prefixes,
+ }
+ return listObjectsV2Info, err
+}
+
+func (z *erasureServerPools) ListObjectVersions(ctx context.Context, bucket, prefix, marker, versionMarker, delimiter string, maxKeys int) (ListObjectVersionsInfo, error) {
+ loi := ListObjectVersionsInfo{}
+ if marker == "" && versionMarker != "" {
+ return loi, NotImplemented{}
+ }
+
+ opts := listPathOptions{
+ Bucket: bucket,
+ Prefix: prefix,
+ Separator: delimiter,
+ Limit: maxKeysPlusOne(maxKeys, marker != ""),
+ Marker: marker,
+ InclDeleted: true,
+ AskDisks: globalAPIConfig.getListQuorum(),
+ Versioned: true,
+ }
+
+ // Shortcut for APN/1.0 Veeam/1.0 Backup/10.0
+ // It requests unique blocks with a specific prefix.
+ // We skip scanning the parent directory for
+ // more objects matching the prefix.
+ if isVeeamClient(ctx) && strings.HasSuffix(prefix, ".blk") {
+ opts.BaseDir = prefix
+ opts.Transient = true
+ }
+
+ // set bucket metadata in opts
+ opts.setBucketMeta(ctx)
+
+ merged, err := z.listPath(ctx, &opts)
+ if err != nil && err != io.EOF {
+ if !isErrBucketNotFound(err) {
+ storageLogOnceIf(ctx, err, "erasure-list-objects-path-"+bucket)
+ }
+ return loi, toObjectErr(err, bucket)
+ }
+ defer merged.truncate(0) // Release when returning
+
+ if contextCanceled(ctx) {
+ return ListObjectVersionsInfo{}, ctx.Err()
+ }
+
+ if versionMarker == "" {
+ o := listPathOptions{Marker: marker}
+ // If we are not looking for a specific version skip it.
+
+ o.parseMarker()
+ merged.forwardPast(o.Marker)
+ }
+ objects := merged.fileInfoVersions(bucket, prefix, delimiter, versionMarker)
+ loi.IsTruncated = err == nil && len(objects) > 0
+ if maxKeys > 0 && len(objects) > maxKeys {
+ objects = objects[:maxKeys]
+ loi.IsTruncated = true
+ }
+ for _, obj := range objects {
+ if obj.IsDir && obj.ModTime.IsZero() && delimiter != "" {
+ // Only add each once.
+ // With slash delimiter we only get the directory once.
+ found := false
+ if delimiter != slashSeparator {
+ for _, p := range loi.Prefixes {
+ if found {
+ break
+ }
+ found = p == obj.Name
+ }
+ }
+ if !found {
+ loi.Prefixes = append(loi.Prefixes, obj.Name)
+ }
+ } else {
+ loi.Objects = append(loi.Objects, obj)
+ }
+ }
+ if loi.IsTruncated {
+ last := objects[len(objects)-1]
+ loi.NextMarker = opts.encodeMarker(last.Name)
+ loi.NextVersionIDMarker = last.VersionID
+ }
+ return loi, nil
+}
+
+func maxKeysPlusOne(maxKeys int, addOne bool) int {
+ if maxKeys < 0 || maxKeys > maxObjectList {
+ maxKeys = maxObjectList
+ }
+ if addOne {
+ maxKeys++
+ }
+ return maxKeys
+}
+
+func (z *erasureServerPools) listObjectsGeneric(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int, v1 bool) (loi ListObjectsInfo, err error) {
+ opts := listPathOptions{
+ V1: v1,
+ Bucket: bucket,
+ Prefix: prefix,
+ Separator: delimiter,
+ Limit: maxKeysPlusOne(maxKeys, marker != ""),
+ Marker: marker,
+ InclDeleted: false,
+ AskDisks: globalAPIConfig.getListQuorum(),
+ }
+ opts.setBucketMeta(ctx)
+ listFn := func(ctx context.Context, opts listPathOptions, limitTo int) (ListObjectsInfo, error) {
+ var loi ListObjectsInfo
+ merged, err := z.listPath(ctx, &opts)
+ if err != nil && err != io.EOF {
+ if !isErrBucketNotFound(err) {
+ storageLogOnceIf(ctx, err, "erasure-list-objects-path-"+bucket)
+ }
+ return loi, toObjectErr(err, bucket)
+ }
+ merged.forwardPast(opts.Marker)
+ defer merged.truncate(0) // Release when returning
+
+ if contextCanceled(ctx) {
+ return ListObjectsInfo{}, ctx.Err()
+ }
+
+ // Default is recursive, if delimiter is set then list non recursive.
+ objects := merged.fileInfos(bucket, prefix, delimiter)
+ loi.IsTruncated = err == nil && len(objects) > 0
+ if limitTo > 0 && len(objects) > limitTo {
+ objects = objects[:limitTo]
+ loi.IsTruncated = true
+ }
+ for _, obj := range objects {
+ if obj.IsDir && obj.ModTime.IsZero() && delimiter != "" {
+ // Only add each once.
+ // With slash delimiter we only get the directory once.
+ found := false
+ if delimiter != slashSeparator {
+ for _, p := range loi.Prefixes {
+ if found {
+ break
+ }
+ found = p == obj.Name
+ }
+ }
+ if !found {
+ loi.Prefixes = append(loi.Prefixes, obj.Name)
+ }
+ } else {
+ loi.Objects = append(loi.Objects, obj)
+ }
+ }
+ if loi.IsTruncated {
+ last := objects[len(objects)-1]
+ loi.NextMarker = last.Name
+ }
+
+ if loi.IsTruncated && merged.lastSkippedEntry > loi.NextMarker {
+ // An object hidden by ILM was found during a truncated listing. Set the next marker
+ // as the last skipped entry if it is lexically higher loi.NextMarker as an optimization
+ loi.NextMarker = merged.lastSkippedEntry
+ }
+
+ if loi.NextMarker != "" {
+ loi.NextMarker = opts.encodeMarker(loi.NextMarker)
+ }
+ return loi, nil
+ }
+ ri := logger.GetReqInfo(ctx)
+ hadoop := ri != nil && strings.Contains(ri.UserAgent, "Hadoop ") && strings.Contains(ri.UserAgent, "scala/")
+ matches := func() bool {
+ if prefix == "" {
+ return false
+ }
+ // List of standard files supported by s3a
+ // that involves a List() on a directory
+ // where directory is actually an object on
+ // namespace.
+ for _, k := range []string{
+ "_SUCCESS/",
+ ".parquet/",
+ ".csv/",
+ ".json/",
+ ".avro/",
+ ".orc/",
+ ".txt/",
+ // Add any other files in future
+ } {
+ if strings.HasSuffix(prefix, k) {
+ return true
+ }
+ }
+ return false
+ }
+
+ if hadoop && delimiter == SlashSeparator && maxKeys == 2 && marker == "" {
+ // Optimization for Spark/Hadoop workload where spark sends a garbage
+ // request of this kind
+ //
+ // GET /testbucket/?list-type=2&delimiter=%2F&max-keys=2&prefix=parquet%2F_SUCCESS%2F&fetch-owner=false
+ //
+ // Here spark is expecting that the List() return empty instead, so from MinIO's point
+ // of view if we simply do a GetObjectInfo() on this prefix by treating it as an object
+ // We save a lot of calls over the network.
+ //
+ // This happens repeatedly for all objects that are created concurrently() avoiding this
+ // as a List() call is an important performance improvement.
+ //
+ // Spark based s3a committers are a big enough use-case to have this optimization.
+ //
+ // A sample code to see the improvements is as follows, this sample code is
+ // simply a read on JSON from MinIO and write it back as "parquet".
+ //
+ // import org.apache.spark.sql.SparkSession
+ // import org.apache.spark.sql.types.{IntegerType, StringType, StructField, StructType}
+ // object SparkJSONRead {
+ // def main(args: Array[String]): Unit = {
+ // val spark:SparkSession = SparkSession.builder()
+ // .appName("SparkByExample")
+ // .master("local[1]").getOrCreate()
+ //
+ // spark.sparkContext.setLogLevel("ERROR")
+ // spark.sparkContext.hadoopConfiguration.set("fs.s3a.endpoint", "http://minio-lb:9000")
+ // spark.sparkContext.hadoopConfiguration.set("fs.s3a.path.style.access", "true")
+ // spark.sparkContext.hadoopConfiguration.set("fs.s3a.access.key", "minioadmin")
+ // spark.sparkContext.hadoopConfiguration.set("fs.s3a.secret.key", "minioadmin")
+ //
+ // val df = spark.read.json("s3a://testbucket/s3.json")
+ //
+ // df.write.parquet("s3a://testbucket/parquet/")
+ // }
+ // }
+ if matches() {
+ objInfo, err := z.GetObjectInfo(ctx, bucket, path.Dir(prefix), ObjectOptions{NoLock: true})
+ if err == nil || objInfo.IsLatest && objInfo.DeleteMarker {
+ if opts.Lifecycle != nil {
+ evt := evalActionFromLifecycle(ctx, *opts.Lifecycle, opts.Retention, opts.Replication.Config, objInfo)
+ if evt.Action.Delete() {
+ globalExpiryState.enqueueByDays(objInfo, evt, lcEventSrc_s3ListObjects)
+ if !evt.Action.DeleteRestored() {
+ // Skip entry if ILM action was DeleteVersionAction or DeleteAction
+ return loi, nil
+ }
+ }
+ }
+ return loi, nil
+ }
+ if isErrBucketNotFound(err) {
+ return loi, err
+ }
+ if contextCanceled(ctx) {
+ return ListObjectsInfo{}, ctx.Err()
+ }
+ }
+ // Hadoop makes the max-keys=2 listing call just to find if the directory is empty or not, or in the case
+ // of an object to check for object existence. For versioned buckets, MinIO's non-recursive
+ // call will report top level prefixes in deleted state, whereas spark/hadoop interpret this as non-empty
+ // and throw a 404 exception. This is especially a problem for spark jobs overwriting the same partition
+ // repeatedly. This workaround recursively lists the top 3 entries including delete markers to reflect the
+ // correct state of the directory in the list results.
+ if strings.HasSuffix(opts.Prefix, SlashSeparator) {
+ li, err := listFn(ctx, opts, maxKeys)
+ if err != nil {
+ return loi, err
+ }
+ if len(li.Objects) == 0 {
+ prefixes := li.Prefixes[:0]
+ for _, prefix := range li.Prefixes {
+ objInfo, _ := z.GetObjectInfo(ctx, bucket, pathJoin(prefix, "_SUCCESS"), ObjectOptions{NoLock: true})
+ if objInfo.IsLatest && objInfo.DeleteMarker {
+ continue
+ }
+ prefixes = append(prefixes, prefix)
+ }
+ if len(prefixes) > 0 {
+ objInfo, _ := z.GetObjectInfo(ctx, bucket, pathJoin(opts.Prefix, "_SUCCESS"), ObjectOptions{NoLock: true})
+ if objInfo.IsLatest && objInfo.DeleteMarker {
+ return loi, nil
+ }
+ }
+ li.Prefixes = prefixes
+ }
+ return li, nil
+ }
+ }
+
+ if len(prefix) > 0 && maxKeys == 1 && marker == "" {
+ // Optimization for certain applications like
+ // - Cohesity
+ // - Actifio, Splunk etc.
+ // which send ListObjects requests where the actual object
+ // itself is the prefix and max-keys=1 in such scenarios
+ // we can simply verify locally if such an object exists
+ // to avoid the need for ListObjects().
+ objInfo, err := z.GetObjectInfo(ctx, bucket, prefix, ObjectOptions{NoLock: true})
+ if err == nil {
+ if opts.Lifecycle != nil {
+ evt := evalActionFromLifecycle(ctx, *opts.Lifecycle, opts.Retention, opts.Replication.Config, objInfo)
+ if evt.Action.Delete() {
+ globalExpiryState.enqueueByDays(objInfo, evt, lcEventSrc_s3ListObjects)
+ if !evt.Action.DeleteRestored() {
+ // Skip entry if ILM action was DeleteVersionAction or DeleteAction
+ return loi, nil
+ }
+ }
+ }
+ loi.Objects = append(loi.Objects, objInfo)
+ return loi, nil
+ }
+ if isErrBucketNotFound(err) {
+ return ListObjectsInfo{}, err
+ }
+ if contextCanceled(ctx) {
+ return ListObjectsInfo{}, ctx.Err()
+ }
+ }
+ return listFn(ctx, opts, maxKeys)
+}
+
+func (z *erasureServerPools) ListMultipartUploads(ctx context.Context, bucket, prefix, keyMarker, uploadIDMarker, delimiter string, maxUploads int) (ListMultipartsInfo, error) {
+ if err := checkListMultipartArgs(ctx, bucket, prefix, keyMarker, uploadIDMarker, delimiter); err != nil {
+ return ListMultipartsInfo{}, err
+ }
+
+ poolResult := ListMultipartsInfo{}
+ poolResult.MaxUploads = maxUploads
+ poolResult.KeyMarker = keyMarker
+ poolResult.Prefix = prefix
+ poolResult.Delimiter = delimiter
+
+ // if no prefix provided, return the list from cache
+ if prefix == "" {
+ if _, err := z.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ return ListMultipartsInfo{}, toObjectErr(err, bucket)
+ }
+
+ z.mpCache.Range(func(_ string, mp MultipartInfo) bool {
+ if mp.Bucket == bucket {
+ poolResult.Uploads = append(poolResult.Uploads, mp)
+ }
+ return true
+ })
+ sort.Slice(poolResult.Uploads, func(i int, j int) bool {
+ return poolResult.Uploads[i].Initiated.Before(poolResult.Uploads[j].Initiated)
+ })
+ return poolResult, nil
+ }
+
+ if z.SinglePool() {
+ return z.serverPools[0].ListMultipartUploads(ctx, bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
+ }
+
+ for idx, pool := range z.serverPools {
+ if z.IsSuspended(idx) {
+ continue
+ }
+ result, err := pool.ListMultipartUploads(ctx, bucket, prefix, keyMarker, uploadIDMarker,
+ delimiter, maxUploads)
+ if err != nil {
+ return result, err
+ }
+ poolResult.Uploads = append(poolResult.Uploads, result.Uploads...)
+ }
+
+ return poolResult, nil
+}
+
+// Initiate a new multipart upload on a hashedSet based on object name.
+func (z *erasureServerPools) NewMultipartUpload(ctx context.Context, bucket, object string, opts ObjectOptions) (mp *NewMultipartUploadResult, err error) {
+ if err := checkNewMultipartArgs(ctx, bucket, object); err != nil {
+ return nil, err
+ }
+
+ defer func() {
+ if err == nil && mp != nil {
+ z.mpCache.Store(mp.UploadID, MultipartInfo{
+ Bucket: bucket,
+ Object: object,
+ UploadID: mp.UploadID,
+ Initiated: time.Now(),
+ })
+ }
+ }()
+
+ if z.SinglePool() {
+ return z.serverPools[0].NewMultipartUpload(ctx, bucket, object, opts)
+ }
+
+ for idx, pool := range z.serverPools {
+ if z.IsSuspended(idx) || z.IsPoolRebalancing(idx) {
+ continue
+ }
+
+ result, err := pool.ListMultipartUploads(ctx, bucket, object, "", "", "", maxUploadsList)
+ if err != nil {
+ return nil, err
+ }
+ // If there is a multipart upload with the same bucket/object name,
+ // create the new multipart in the same pool, this will avoid
+ // creating two multiparts uploads in two different pools
+ if len(result.Uploads) != 0 {
+ return z.serverPools[idx].NewMultipartUpload(ctx, bucket, object, opts)
+ }
+ }
+
+ // any parallel writes on the object will block for this poolIdx
+ // to return since this holds a read lock on the namespace.
+ idx, err := z.getPoolIdx(ctx, bucket, object, -1)
+ if err != nil {
+ return nil, err
+ }
+
+ if opts.DataMovement && idx == opts.SrcPoolIdx {
+ return nil, DataMovementOverwriteErr{
+ Bucket: bucket,
+ Object: object,
+ VersionID: opts.VersionID,
+ Err: errDataMovementSrcDstPoolSame,
+ }
+ }
+
+ return z.serverPools[idx].NewMultipartUpload(ctx, bucket, object, opts)
+}
+
+// Copies a part of an object from source hashedSet to destination hashedSet.
+func (z *erasureServerPools) CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, uploadID string, partID int, startOffset int64, length int64, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (PartInfo, error) {
+ if err := checkNewMultipartArgs(ctx, srcBucket, srcObject); err != nil {
+ return PartInfo{}, err
+ }
+
+ return z.PutObjectPart(ctx, destBucket, destObject, uploadID, partID,
+ srcInfo.PutObjReader, dstOpts)
+}
+
+// PutObjectPart - writes part of an object to hashedSet based on the object name.
+func (z *erasureServerPools) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, data *PutObjReader, opts ObjectOptions) (PartInfo, error) {
+ if err := checkPutObjectPartArgs(ctx, bucket, object, uploadID); err != nil {
+ return PartInfo{}, err
+ }
+
+ if z.SinglePool() {
+ _, err := z.getPoolIdx(ctx, bucket, object, data.Size())
+ if err != nil {
+ return PartInfo{}, err
+ }
+ return z.serverPools[0].PutObjectPart(ctx, bucket, object, uploadID, partID, data, opts)
+ }
+
+ for idx, pool := range z.serverPools {
+ if z.IsSuspended(idx) {
+ continue
+ }
+ pi, err := pool.PutObjectPart(ctx, bucket, object, uploadID, partID, data, opts)
+ if err == nil {
+ return pi, nil
+ }
+ if _, ok := err.(InvalidUploadID); ok {
+ // Look for information on the next pool
+ continue
+ }
+ // Any other unhandled errors such as quorum return.
+ return PartInfo{}, err
+ }
+
+ return PartInfo{}, InvalidUploadID{
+ Bucket: bucket,
+ Object: object,
+ UploadID: uploadID,
+ }
+}
+
+func (z *erasureServerPools) GetMultipartInfo(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) (MultipartInfo, error) {
+ if err := checkListPartsArgs(ctx, bucket, object, uploadID); err != nil {
+ 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() {
+ return z.serverPools[0].GetMultipartInfo(ctx, bucket, object, uploadID, opts)
+ }
+
+ for idx, pool := range z.serverPools {
+ if z.IsSuspended(idx) {
+ continue
+ }
+ mi, err := pool.GetMultipartInfo(ctx, bucket, object, uploadID, opts)
+ if err == nil {
+ return mi, nil
+ }
+ if _, ok := err.(InvalidUploadID); ok {
+ // upload id not found, continue to the next pool.
+ continue
+ }
+ // any other unhandled error return right here.
+ return MultipartInfo{}, err
+ }
+
+ return MultipartInfo{}, InvalidUploadID{
+ Bucket: bucket,
+ Object: object,
+ UploadID: uploadID,
+ }
+}
+
+// ListObjectParts - lists all uploaded parts to an object in hashedSet.
+func (z *erasureServerPools) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int, opts ObjectOptions) (ListPartsInfo, error) {
+ if err := checkListPartsArgs(ctx, bucket, object, uploadID); err != nil {
+ 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() {
+ return z.serverPools[0].ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts)
+ }
+
+ for idx, pool := range z.serverPools {
+ if z.IsSuspended(idx) {
+ continue
+ }
+ result, err := pool.ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts)
+ if err == nil {
+ return result, nil
+ }
+ if _, ok := err.(InvalidUploadID); ok {
+ continue
+ }
+ return ListPartsInfo{}, err
+ }
+
+ return ListPartsInfo{}, InvalidUploadID{
+ Bucket: bucket,
+ Object: object,
+ UploadID: uploadID,
+ }
+}
+
+// Aborts an in-progress multipart operation on hashedSet based on the object name.
+func (z *erasureServerPools) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) (err error) {
+ if err := checkAbortMultipartArgs(ctx, bucket, object, uploadID); err != nil {
+ return err
+ }
+
+ defer func() {
+ if err == nil {
+ z.mpCache.Delete(uploadID)
+ globalNotificationSys.DeleteUploadID(ctx, uploadID)
+ }
+ }()
+
+ 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() {
+ return z.serverPools[0].AbortMultipartUpload(ctx, bucket, object, uploadID, opts)
+ }
+
+ for idx, pool := range z.serverPools {
+ if z.IsSuspended(idx) {
+ continue
+ }
+ err := pool.AbortMultipartUpload(ctx, bucket, object, uploadID, opts)
+ if err == nil {
+ return nil
+ }
+ if _, ok := err.(InvalidUploadID); ok {
+ // upload id not found move to next pool
+ continue
+ }
+ return err
+ }
+ return InvalidUploadID{
+ Bucket: bucket,
+ Object: object,
+ UploadID: uploadID,
+ }
+}
+
+// CompleteMultipartUpload - completes a pending multipart transaction, on hashedSet based on object name.
+func (z *erasureServerPools) CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart, opts ObjectOptions) (objInfo ObjectInfo, err error) {
+ if err = checkCompleteMultipartArgs(ctx, bucket, object, uploadID); err != nil {
+ return objInfo, err
+ }
+
+ defer func() {
+ if err == nil {
+ z.mpCache.Delete(uploadID)
+ globalNotificationSys.DeleteUploadID(ctx, uploadID)
+ }
+ }()
+
+ // 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() {
+ return z.serverPools[0].CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts)
+ }
+
+ for idx, pool := range z.serverPools {
+ if z.IsSuspended(idx) {
+ continue
+ }
+ objInfo, err = pool.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts)
+ if err == nil {
+ return objInfo, nil
+ }
+ if _, ok := err.(InvalidUploadID); ok {
+ // upload id not found move to next pool
+ continue
+ }
+ return objInfo, err
+ }
+
+ return objInfo, InvalidUploadID{
+ Bucket: bucket,
+ Object: object,
+ UploadID: uploadID,
+ }
+}
+
+// GetBucketInfo - returns bucket info from one of the erasure coded serverPools.
+func (z *erasureServerPools) GetBucketInfo(ctx context.Context, bucket string, opts BucketOptions) (bucketInfo BucketInfo, err error) {
+ bucketInfo, err = z.s3Peer.GetBucketInfo(ctx, bucket, opts)
+ if err != nil {
+ return bucketInfo, toObjectErr(err, bucket)
+ }
+
+ meta, err := globalBucketMetadataSys.Get(bucket)
+ if err == nil {
+ bucketInfo.Created = meta.Created
+ bucketInfo.Versioning = meta.Versioning()
+ bucketInfo.ObjectLocking = meta.ObjectLocking()
+ }
+ return bucketInfo, nil
+}
+
+// ClearUploadID deletes given uploadID from cache
+func (z *erasureServerPools) ClearUploadID(uploadID string) error {
+ z.mpCache.Delete(uploadID)
+ return nil
+}
+
+// DeleteBucket - deletes a bucket on all serverPools simultaneously,
+// even if one of the serverPools fail to delete buckets, we proceed to
+// undo a successful operation.
+func (z *erasureServerPools) DeleteBucket(ctx context.Context, bucket string, opts DeleteBucketOptions) error {
+ if isMinioMetaBucketName(bucket) {
+ return BucketNameInvalid{Bucket: bucket}
+ }
+
+ // Verify if bucket is valid.
+ if err := s3utils.CheckValidBucketName(bucket); err != nil {
+ return BucketNameInvalid{Bucket: bucket}
+ }
+
+ if !opts.NoLock {
+ // Lock the bucket name before creating.
+ lk := z.NewNSLock(minioMetaTmpBucket, bucket+".lck")
+ lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ return err
+ }
+ ctx = lkctx.Context()
+ defer lk.Unlock(lkctx)
+ }
+
+ if !opts.Force {
+ results := make(chan itemOrErr[ObjectInfo])
+
+ ctx, cancel := context.WithTimeout(ctx, time.Minute)
+ defer cancel()
+ err := z.Walk(ctx, bucket, "", results, WalkOptions{Limit: 1})
+ if err != nil {
+ s3LogIf(ctx, fmt.Errorf("unable to verify if the bucket %s is empty: %w", bucket, err))
+ return toObjectErr(err, bucket)
+ }
+
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case r, found := <-results:
+ if found {
+ if r.Err != nil {
+ s3LogIf(ctx, fmt.Errorf("unable to verify if the bucket %s is empty: %w", bucket, r.Err))
+ return toObjectErr(r.Err, bucket)
+ }
+ return toObjectErr(errVolumeNotEmpty, bucket)
+ }
+ }
+
+ // Always pass force to the lower level
+ opts.Force = true
+ }
+
+ err := z.s3Peer.DeleteBucket(ctx, bucket, opts)
+ if err == nil || isErrBucketNotFound(err) {
+ // If site replication is configured, hold on to deleted bucket state until sites sync
+ if opts.SRDeleteOp == MarkDelete {
+ z.s3Peer.MakeBucket(context.Background(), pathJoin(minioMetaBucket, bucketMetaPrefix, deletedBucketsPrefix, bucket), MakeBucketOptions{})
+ }
+ }
+
+ if err == nil {
+ // Purge the entire bucket metadata entirely.
+ z.deleteAll(context.Background(), minioMetaBucket, pathJoin(bucketMetaPrefix, bucket))
+ }
+
+ return toObjectErr(err, bucket)
+}
+
+// deleteAll will rename bucket+prefix unconditionally across all disks to
+// minioMetaTmpDeletedBucket + unique uuid,
+// Note that set distribution is ignored so it should only be used in cases where
+// data is not distributed across sets. Errors are logged but individual
+// disk failures are not returned.
+func (z *erasureServerPools) deleteAll(ctx context.Context, bucket, prefix string) {
+ for _, servers := range z.serverPools {
+ for _, set := range servers.sets {
+ set.deleteAll(ctx, bucket, prefix)
+ }
+ }
+}
+
+var listBucketsCache = cachevalue.New[[]BucketInfo]()
+
+// List all buckets from one of the serverPools, we are not doing merge
+// sort here just for simplification. As per design it is assumed
+// that all buckets are present on all serverPools.
+func (z *erasureServerPools) ListBuckets(ctx context.Context, opts BucketOptions) (buckets []BucketInfo, err error) {
+ if opts.Cached {
+ listBucketsCache.InitOnce(time.Second,
+ cachevalue.Opts{ReturnLastGood: true, NoWait: true},
+ func(ctx context.Context) ([]BucketInfo, error) {
+ ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
+ defer cancel()
+
+ buckets, err = z.s3Peer.ListBuckets(ctx, opts)
+ if err != nil {
+ return nil, err
+ }
+ if !opts.NoMetadata {
+ for i := range buckets {
+ createdAt, err := globalBucketMetadataSys.CreatedAt(buckets[i].Name)
+ if err == nil {
+ buckets[i].Created = createdAt
+ }
+ }
+ }
+ return buckets, nil
+ },
+ )
+
+ return listBucketsCache.GetWithCtx(ctx)
+ }
+
+ buckets, err = z.s3Peer.ListBuckets(ctx, opts)
+ if err != nil {
+ return nil, err
+ }
+
+ if !opts.NoMetadata {
+ for i := range buckets {
+ createdAt, err := globalBucketMetadataSys.CreatedAt(buckets[i].Name)
+ if err == nil {
+ buckets[i].Created = createdAt
+ }
+ }
+ }
+ return buckets, nil
+}
+
+func (z *erasureServerPools) HealFormat(ctx context.Context, dryRun bool) (madmin.HealResultItem, error) {
+ // Acquire lock on format.json
+ formatLock := z.NewNSLock(minioMetaBucket, formatConfigFile)
+ lkctx, err := formatLock.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ return madmin.HealResultItem{}, err
+ }
+ ctx = lkctx.Context()
+ defer formatLock.Unlock(lkctx)
+
+ r := madmin.HealResultItem{
+ Type: madmin.HealItemMetadata,
+ Detail: "disk-format",
+ }
+
+ var countNoHeal int
+ for _, pool := range z.serverPools {
+ result, err := pool.HealFormat(ctx, dryRun)
+ if err != nil && !errors.Is(err, errNoHealRequired) {
+ healingLogOnceIf(ctx, err, "erasure-heal-format")
+ continue
+ }
+ // Count errNoHealRequired across all serverPools,
+ // to return appropriate error to the caller
+ if errors.Is(err, errNoHealRequired) {
+ countNoHeal++
+ }
+ r.DiskCount += result.DiskCount
+ r.SetCount += result.SetCount
+ r.Before.Drives = append(r.Before.Drives, result.Before.Drives...)
+ r.After.Drives = append(r.After.Drives, result.After.Drives...)
+ }
+
+ // No heal returned by all serverPools, return errNoHealRequired
+ if countNoHeal == len(z.serverPools) {
+ return r, errNoHealRequired
+ }
+
+ return r, nil
+}
+
+func (z *erasureServerPools) HealBucket(ctx context.Context, bucket string, opts madmin.HealOpts) (madmin.HealResultItem, error) {
+ // .metadata.bin healing is not needed here, it is automatically healed via read() call.
+ return z.s3Peer.HealBucket(ctx, bucket, opts)
+}
+
+// Walk a bucket, optionally prefix recursively, until we have returned
+// all the contents of the provided bucket+prefix.
+func (z *erasureServerPools) Walk(ctx context.Context, bucket, prefix string, results chan<- itemOrErr[ObjectInfo], opts WalkOptions) error {
+ if err := checkListObjsArgs(ctx, bucket, prefix, ""); err != nil {
+ xioutil.SafeClose(results)
+ return err
+ }
+ parentCtx := ctx
+ ctx, cancelCause := context.WithCancelCause(ctx)
+ var entries []chan metaCacheEntry
+
+ for poolIdx, erasureSet := range z.serverPools {
+ for setIdx, set := range erasureSet.sets {
+ listOut := make(chan metaCacheEntry, 1)
+ entries = append(entries, listOut)
+ disks, infos, _ := set.getOnlineDisksWithHealingAndInfo(true)
+ if len(disks) == 0 {
+ xioutil.SafeClose(results)
+ err := fmt.Errorf("Walk: no online disks found in (set:%d pool:%d) %w", setIdx, poolIdx, errErasureReadQuorum)
+ cancelCause(err)
+ return err
+ }
+ go func() {
+ defer xioutil.SafeClose(listOut)
+ send := func(e metaCacheEntry) {
+ if e.isDir() {
+ // Ignore directories.
+ return
+ }
+ select {
+ case listOut <- e:
+ case <-ctx.Done():
+ }
+ }
+
+ askDisks := getListQuorum(opts.AskDisks, set.setDriveCount)
+ if askDisks == -1 {
+ newDisks := getQuorumDisks(disks, infos, (len(disks)+1)/2)
+ if newDisks != nil {
+ // If we found disks signature in quorum, we proceed to list
+ // from a single drive, shuffling of the drives is subsequently.
+ disks = newDisks
+ askDisks = 1
+ } else {
+ // If we did not find suitable disks, perform strict quorum listing
+ // as no disk agrees on quorum anymore.
+ askDisks = getListQuorum("strict", set.setDriveCount)
+ }
+ }
+
+ // Special case: ask all disks if the drive count is 4
+ if set.setDriveCount == 4 || askDisks > len(disks) {
+ askDisks = len(disks) // use all available drives
+ }
+
+ var fallbackDisks []StorageAPI
+ if askDisks > 0 && len(disks) > askDisks {
+ rand.Shuffle(len(disks), func(i, j int) {
+ disks[i], disks[j] = disks[j], disks[i]
+ })
+ fallbackDisks = disks[askDisks:]
+ disks = disks[:askDisks]
+ }
+
+ requestedVersions := 0
+ if opts.LatestOnly {
+ requestedVersions = 1
+ }
+
+ // However many we ask, versions must exist on ~50%
+ listingQuorum := (askDisks + 1) / 2
+
+ // How to resolve partial results.
+ resolver := metadataResolutionParams{
+ dirQuorum: listingQuorum,
+ objQuorum: listingQuorum,
+ bucket: bucket,
+ requestedVersions: requestedVersions,
+ }
+
+ path := baseDirFromPrefix(prefix)
+ filterPrefix := strings.Trim(strings.TrimPrefix(prefix, path), slashSeparator)
+ if path == prefix {
+ filterPrefix = ""
+ }
+
+ lopts := listPathRawOptions{
+ disks: disks,
+ fallbackDisks: fallbackDisks,
+ bucket: bucket,
+ path: path,
+ filterPrefix: filterPrefix,
+ recursive: true,
+ forwardTo: opts.Marker,
+ perDiskLimit: opts.Limit,
+ minDisks: listingQuorum,
+ reportNotFound: false,
+ agreed: send,
+ partial: func(entries metaCacheEntries, _ []error) {
+ entry, ok := entries.resolve(&resolver)
+ if ok {
+ send(*entry)
+ }
+ },
+ finished: nil,
+ }
+
+ if err := listPathRaw(ctx, lopts); err != nil {
+ cancelCause(fmt.Errorf("listPathRaw returned %w: opts(%#v)", err, lopts))
+ return
+ }
+ }()
+ }
+ }
+
+ // Convert and filter merged entries.
+ merged := make(chan metaCacheEntry, 100)
+ vcfg, _ := globalBucketVersioningSys.Get(bucket)
+ errCh := make(chan error, 1)
+ go func() {
+ sentErr := false
+ sendErr := func(err error) {
+ if !sentErr {
+ select {
+ case results <- itemOrErr[ObjectInfo]{Err: err}:
+ sentErr = true
+ case <-parentCtx.Done():
+ }
+ }
+ }
+ defer func() {
+ select {
+ case <-ctx.Done():
+ sendErr(ctx.Err())
+ default:
+ }
+ xioutil.SafeClose(results)
+ cancelCause(nil)
+ }()
+ send := func(oi ObjectInfo) bool {
+ select {
+ case results <- itemOrErr[ObjectInfo]{Item: oi}:
+ return true
+ case <-ctx.Done():
+ sendErr(context.Cause(ctx))
+ return false
+ }
+ }
+ for entry := range merged {
+ if opts.LatestOnly {
+ fi, err := entry.fileInfo(bucket)
+ if err != nil {
+ sendErr(err)
+ return
+ }
+ if opts.Filter != nil {
+ if opts.Filter(fi) {
+ if !send(fi.ToObjectInfo(bucket, fi.Name, vcfg != nil && vcfg.Versioned(fi.Name))) {
+ return
+ }
+ }
+ } else {
+ if !send(fi.ToObjectInfo(bucket, fi.Name, vcfg != nil && vcfg.Versioned(fi.Name))) {
+ return
+ }
+ }
+ continue
+ }
+ fivs, err := entry.fileInfoVersions(bucket)
+ if err != nil {
+ sendErr(err)
+ return
+ }
+
+ // Note: entry.fileInfoVersions returns versions sorted in reverse chronological order based on ModTime
+ if opts.VersionsSort == WalkVersionsSortAsc {
+ versionsSorter(fivs.Versions).reverse()
+ }
+
+ for _, version := range fivs.Versions {
+ if opts.Filter != nil {
+ if opts.Filter(version) {
+ if !send(version.ToObjectInfo(bucket, version.Name, vcfg != nil && vcfg.Versioned(version.Name))) {
+ return
+ }
+ }
+ } else {
+ if !send(version.ToObjectInfo(bucket, version.Name, vcfg != nil && vcfg.Versioned(version.Name))) {
+ return
+ }
+ }
+ }
+ }
+ if err := <-errCh; err != nil {
+ sendErr(err)
+ }
+ }()
+ go func() {
+ defer close(errCh)
+ // Merge all entries from all disks.
+ // We leave quorum at 1, since entries are already resolved to have the desired quorum.
+ // mergeEntryChannels will close 'merged' channel upon completion or cancellation.
+ errCh <- mergeEntryChannels(ctx, entries, merged, 1)
+ }()
+
+ return nil
+}
+
+// HealObjectFn closure function heals the object.
+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 {
+ healEntry := func(bucket string, entry metaCacheEntry, scanMode madmin.HealScanMode) error {
+ if entry.isDir() {
+ return nil
+ }
+ // We might land at .metacache, .trash, .multipart
+ // no need to heal them skip, only when bucket
+ // is '.minio.sys'
+ if bucket == minioMetaBucket {
+ if wildcard.Match("buckets/*/.metacache/*", entry.name) {
+ return nil
+ }
+ if wildcard.Match("tmp/*", entry.name) {
+ return nil
+ }
+ if wildcard.Match("multipart/*", entry.name) {
+ return nil
+ }
+ if wildcard.Match("tmp-old/*", entry.name) {
+ return nil
+ }
+ }
+ fivs, err := entry.fileInfoVersions(bucket)
+ if err != nil {
+ return healObjectFn(bucket, entry.name, "", scanMode)
+ }
+ if opts.Remove && !opts.DryRun {
+ err := z.CheckAbandonedParts(ctx, bucket, entry.name, opts)
+ if err != nil {
+ healingLogIf(ctx, fmt.Errorf("unable to check object %s/%s for abandoned data: %w", bucket, entry.name, err))
+ }
+ }
+ for _, version := range fivs.Versions {
+ err := healObjectFn(bucket, version.Name, version.VersionID, scanMode)
+ if err != nil && !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
+ return err
+ }
+ }
+
+ return nil
+ }
+
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ poolErrs := make([][]error, len(z.serverPools))
+ for idx, erasureSet := range z.serverPools {
+ if opts.Pool != nil && *opts.Pool != idx {
+ continue
+ }
+ if z.IsSuspended(idx) {
+ continue
+ }
+ errs := make([]error, len(erasureSet.sets))
+ wk, _ := workers.New(3)
+ for idx, set := range erasureSet.sets {
+ if opts.Set != nil && *opts.Set != idx {
+ continue
+ }
+ wk.Take()
+ go func(idx int, set *erasureObjects) {
+ defer wk.Give()
+
+ errs[idx] = set.listAndHeal(ctx, bucket, prefix, opts.Recursive, opts.ScanMode, healEntry)
+ }(idx, set)
+ }
+ wk.Wait()
+ poolErrs[idx] = errs
+ }
+ for _, errs := range poolErrs {
+ for _, err := range errs {
+ if err == nil {
+ continue
+ }
+ return err
+ }
+ }
+ return nil
+}
+
+func (z *erasureServerPools) HealObject(ctx context.Context, bucket, object, versionID string, opts madmin.HealOpts) (madmin.HealResultItem, error) {
+ object = encodeDirObject(object)
+
+ errs := make([]error, len(z.serverPools))
+ results := make([]madmin.HealResultItem, len(z.serverPools))
+ var wg sync.WaitGroup
+ for idx, pool := range z.serverPools {
+ if z.IsSuspended(idx) {
+ continue
+ }
+ wg.Add(1)
+ go func(idx int, pool *erasureSets) {
+ defer wg.Done()
+ result, err := pool.HealObject(ctx, bucket, object, versionID, opts)
+ result.Object = decodeDirObject(result.Object)
+ errs[idx] = err
+ results[idx] = result
+ }(idx, pool)
+ }
+ wg.Wait()
+
+ // Return the first nil error
+ for idx, err := range errs {
+ if err == nil {
+ return results[idx], nil
+ }
+ }
+
+ // No pool returned a nil error, return the first non 'not found' error
+ for idx, err := range errs {
+ if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
+ return results[idx], err
+ }
+ }
+
+ hr := madmin.HealResultItem{
+ Type: madmin.HealItemObject,
+ Bucket: bucket,
+ Object: object,
+ VersionID: versionID,
+ }
+
+ // At this stage, all errors are 'not found'
+ if versionID != "" {
+ return hr, VersionNotFound{
+ Bucket: bucket,
+ Object: object,
+ VersionID: versionID,
+ }
+ }
+ return hr, ObjectNotFound{
+ Bucket: bucket,
+ Object: object,
+ }
+}
+
+func (z *erasureServerPools) getPoolAndSet(id string) (poolIdx, setIdx, diskIdx int, err error) {
+ for poolIdx := range z.serverPools {
+ format := z.serverPools[poolIdx].format
+ for setIdx, set := range format.Erasure.Sets {
+ for i, diskID := range set {
+ if diskID == id {
+ return poolIdx, setIdx, i, nil
+ }
+ }
+ }
+ }
+ return -1, -1, -1, fmt.Errorf("DriveID(%s) %w", id, errDiskNotFound)
+}
+
+const (
+ vmware = "VMWare"
+)
+
+// HealthOptions takes input options to return specific information
+type HealthOptions struct {
+ Maintenance bool
+ DeploymentType string
+ NoLogging bool
+}
+
+// HealthResult returns the current state of the system, also
+// additionally with any specific heuristic information which
+// was queried
+type HealthResult struct {
+ Healthy bool
+ HealthyRead bool
+ HealingDrives int
+ ESHealth []struct {
+ Maintenance bool
+ PoolID, SetID int
+ Healthy bool
+ HealthyRead bool
+ HealthyDrives int
+ HealingDrives int
+ ReadQuorum int
+ WriteQuorum int
+ }
+ WriteQuorum int
+ ReadQuorum int
+ UsingDefaults bool
+}
+
+func (hr HealthResult) String() string {
+ var str strings.Builder
+ for i, es := range hr.ESHealth {
+ str.WriteString("(Pool: ")
+ str.WriteString(strconv.Itoa(es.PoolID))
+ str.WriteString(" Set: ")
+ str.WriteString(strconv.Itoa(es.SetID))
+ str.WriteString(" Healthy: ")
+ str.WriteString(strconv.FormatBool(es.Healthy))
+ if i == 0 {
+ str.WriteString(")")
+ } else {
+ str.WriteString(") | ")
+ }
+ }
+ return str.String()
+}
+
+// Health - returns current status of the object layer health,
+// provides if write access exists across sets, additionally
+// can be used to query scenarios if health may be lost
+// if this node is taken down by an external orchestrator.
+func (z *erasureServerPools) Health(ctx context.Context, opts HealthOptions) HealthResult {
+ reqInfo := (&logger.ReqInfo{}).AppendTags("maintenance", strconv.FormatBool(opts.Maintenance))
+
+ type setInfo struct {
+ online int
+ healing int
+ }
+
+ var drivesHealing int
+
+ erasureSetUpCount := make([][]setInfo, len(z.serverPools))
+ for i := range z.serverPools {
+ erasureSetUpCount[i] = make([]setInfo, len(z.serverPools[i].sets))
+ }
+
+ storageInfo := z.StorageInfo(ctx, false)
+
+ for _, disk := range storageInfo.Disks {
+ if opts.Maintenance {
+ globalLocalDrivesMu.RLock()
+ _, ok := globalLocalDrivesMap[disk.Endpoint]
+ globalLocalDrivesMu.RUnlock()
+ if ok {
+ continue
+ }
+ }
+
+ if disk.PoolIndex > -1 && disk.SetIndex > -1 {
+ if disk.State == madmin.DriveStateOk {
+ si := erasureSetUpCount[disk.PoolIndex][disk.SetIndex]
+ si.online++
+ if disk.Healing {
+ si.healing++
+ drivesHealing++
+ }
+ erasureSetUpCount[disk.PoolIndex][disk.SetIndex] = si
+ }
+ }
+ }
+
+ b := z.BackendInfo()
+ poolReadQuorums := make([]int, len(b.StandardSCData))
+ poolWriteQuorums := make([]int, len(b.StandardSCData))
+ for i, data := range b.StandardSCData {
+ poolReadQuorums[i] = data
+ poolWriteQuorums[i] = data
+ if data == b.StandardSCParity {
+ poolWriteQuorums[i] = data + 1
+ }
+ }
+
+ // Check if disks are healing on in-case of VMware vsphere deployments.
+ if opts.Maintenance && opts.DeploymentType == vmware {
+ if drivesHealing > 0 {
+ healingLogIf(logger.SetReqInfo(ctx, reqInfo), fmt.Errorf("Total drives to be healed %d", drivesHealing))
+ }
+ }
+
+ var usingDefaults bool
+ if globalStorageClass.GetParityForSC(storageclass.STANDARD) < 0 {
+ usingDefaults = true
+ }
+
+ var maximumWriteQuorum int
+ for _, writeQuorum := range poolWriteQuorums {
+ if maximumWriteQuorum == 0 {
+ maximumWriteQuorum = writeQuorum
+ }
+ if writeQuorum > maximumWriteQuorum {
+ maximumWriteQuorum = writeQuorum
+ }
+ }
+
+ var maximumReadQuorum int
+ for _, readQuorum := range poolReadQuorums {
+ if maximumReadQuorum == 0 {
+ maximumReadQuorum = readQuorum
+ }
+ if readQuorum > maximumReadQuorum {
+ maximumReadQuorum = readQuorum
+ }
+ }
+
+ result := HealthResult{
+ Healthy: true,
+ HealthyRead: true,
+ WriteQuorum: maximumWriteQuorum,
+ ReadQuorum: maximumReadQuorum,
+ UsingDefaults: usingDefaults, // indicates if config was not initialized and we are using defaults on this node.
+ }
+
+ for poolIdx := range erasureSetUpCount {
+ for setIdx := range erasureSetUpCount[poolIdx] {
+ result.ESHealth = append(result.ESHealth, struct {
+ Maintenance bool
+ PoolID, SetID int
+ Healthy bool
+ HealthyRead bool
+ HealthyDrives, HealingDrives int
+ ReadQuorum, WriteQuorum int
+ }{
+ Maintenance: opts.Maintenance,
+ SetID: setIdx,
+ PoolID: poolIdx,
+ Healthy: erasureSetUpCount[poolIdx][setIdx].online >= poolWriteQuorums[poolIdx],
+ HealthyRead: erasureSetUpCount[poolIdx][setIdx].online >= poolReadQuorums[poolIdx],
+ HealthyDrives: erasureSetUpCount[poolIdx][setIdx].online,
+ HealingDrives: erasureSetUpCount[poolIdx][setIdx].healing,
+ ReadQuorum: poolReadQuorums[poolIdx],
+ WriteQuorum: poolWriteQuorums[poolIdx],
+ })
+
+ healthy := erasureSetUpCount[poolIdx][setIdx].online >= poolWriteQuorums[poolIdx]
+ if !healthy && !opts.NoLogging {
+ storageLogIf(logger.SetReqInfo(ctx, reqInfo),
+ fmt.Errorf("Write quorum could not be established on pool: %d, set: %d, expected write quorum: %d, drives-online: %d",
+ poolIdx, setIdx, poolWriteQuorums[poolIdx], erasureSetUpCount[poolIdx][setIdx].online), logger.FatalKind)
+ }
+ result.Healthy = result.Healthy && healthy
+
+ healthyRead := erasureSetUpCount[poolIdx][setIdx].online >= poolReadQuorums[poolIdx]
+ if !healthyRead && !opts.NoLogging {
+ storageLogIf(logger.SetReqInfo(ctx, reqInfo),
+ fmt.Errorf("Read quorum could not be established on pool: %d, set: %d, expected read quorum: %d, drives-online: %d",
+ poolIdx, setIdx, poolReadQuorums[poolIdx], erasureSetUpCount[poolIdx][setIdx].online))
+ }
+ result.HealthyRead = result.HealthyRead && healthyRead
+ }
+ }
+
+ if opts.Maintenance {
+ result.Healthy = result.Healthy && drivesHealing == 0
+ result.HealthyRead = result.HealthyRead && drivesHealing == 0
+ result.HealingDrives = drivesHealing
+ }
+
+ return result
+}
+
+// PutObjectMetadata - replace or add tags to an existing object
+func (z *erasureServerPools) PutObjectMetadata(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
+ object = encodeDirObject(object)
+ if z.SinglePool() {
+ 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.NoLock = true
+ // We don't know the size here set 1GiB at least.
+ idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
+ if err != nil {
+ return ObjectInfo{}, err
+ }
+
+ return z.serverPools[idx].PutObjectMetadata(ctx, bucket, object, opts)
+}
+
+// PutObjectTags - replace or add tags to an existing object
+func (z *erasureServerPools) PutObjectTags(ctx context.Context, bucket, object string, tags string, opts ObjectOptions) (ObjectInfo, error) {
+ object = encodeDirObject(object)
+ if z.SinglePool() {
+ 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.NoLock = true
+
+ // We don't know the size here set 1GiB at least.
+ idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
+ if err != nil {
+ return ObjectInfo{}, err
+ }
+
+ return z.serverPools[idx].PutObjectTags(ctx, bucket, object, tags, opts)
+}
+
+// DeleteObjectTags - delete object tags from an existing object
+func (z *erasureServerPools) DeleteObjectTags(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
+ object = encodeDirObject(object)
+ if z.SinglePool() {
+ return z.serverPools[0].DeleteObjectTags(ctx, bucket, object, opts)
+ }
+
+ 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)
+ if err != nil {
+ return ObjectInfo{}, err
+ }
+
+ return z.serverPools[idx].DeleteObjectTags(ctx, bucket, object, opts)
+}
+
+// GetObjectTags - get object tags from an existing object
+func (z *erasureServerPools) GetObjectTags(ctx context.Context, bucket, object string, opts ObjectOptions) (*tags.Tags, error) {
+ object = encodeDirObject(object)
+ if z.SinglePool() {
+ return z.serverPools[0].GetObjectTags(ctx, bucket, object, opts)
+ }
+
+ oi, _, err := z.getLatestObjectInfoWithIdx(ctx, bucket, object, opts)
+ if err != nil {
+ return nil, err
+ }
+
+ return tags.ParseObjectTags(oi.UserTags)
+}
+
+// TransitionObject - transition object content to target tier.
+func (z *erasureServerPools) TransitionObject(ctx context.Context, bucket, object string, opts ObjectOptions) error {
+ object = encodeDirObject(object)
+ if z.SinglePool() {
+ 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.
+ opts.SkipDecommissioned = true
+ opts.NoLock = true
+ idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
+ if err != nil {
+ return err
+ }
+
+ return z.serverPools[idx].TransitionObject(ctx, bucket, object, opts)
+}
+
+// RestoreTransitionedObject - restore transitioned object content locally on this cluster.
+func (z *erasureServerPools) RestoreTransitionedObject(ctx context.Context, bucket, object string, opts ObjectOptions) error {
+ object = encodeDirObject(object)
+ if z.SinglePool() {
+ 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.
+ opts.SkipDecommissioned = true
+ opts.NoLock = true
+ idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
+ if err != nil {
+ return err
+ }
+
+ return z.serverPools[idx].RestoreTransitionedObject(ctx, bucket, object, opts)
+}
+
+func (z *erasureServerPools) CheckAbandonedParts(ctx context.Context, bucket, object string, opts madmin.HealOpts) error {
+ object = encodeDirObject(object)
+ if z.SinglePool() {
+ return z.serverPools[0].CheckAbandonedParts(ctx, bucket, object, opts)
+ }
+ errs := make([]error, len(z.serverPools))
+ var wg sync.WaitGroup
+ for idx, pool := range z.serverPools {
+ if z.IsSuspended(idx) {
+ continue
+ }
+ wg.Add(1)
+ go func(idx int, pool *erasureSets) {
+ defer wg.Done()
+ err := pool.CheckAbandonedParts(ctx, bucket, object, opts)
+ if err != nil && !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
+ errs[idx] = err
+ }
+ }(idx, pool)
+ }
+ wg.Wait()
+ for _, err := range errs {
+ return err
+ }
+ return nil
+}
+
+// DecomTieredObject - moves tiered object to another pool during decommissioning.
+func (z *erasureServerPools) DecomTieredObject(ctx context.Context, bucket, object string, fi FileInfo, opts ObjectOptions) error {
+ object = encodeDirObject(object)
+ if z.SinglePool() {
+ return fmt.Errorf("error decommissioning %s/%s", bucket, object)
+ }
+ if !opts.NoLock {
+ ns := z.NewNSLock(bucket, object)
+ lkctx, err := ns.GetLock(ctx, globalOperationTimeout)
+ if err != nil {
+ return err
+ }
+ ctx = lkctx.Context()
+ defer ns.Unlock(lkctx)
+ opts.NoLock = true
+ }
+ idx, err := z.getPoolIdxNoLock(ctx, bucket, object, fi.Size)
+ if err != nil {
+ return err
+ }
+
+ if opts.DataMovement && idx == opts.SrcPoolIdx {
+ return DataMovementOverwriteErr{
+ Bucket: bucket,
+ Object: object,
+ VersionID: opts.VersionID,
+ Err: errDataMovementSrcDstPoolSame,
+ }
+ }
+
+ return z.serverPools[idx].DecomTieredObject(ctx, bucket, object, fi, opts)
+}
diff --git a/cmd/erasure-sets.go b/cmd/erasure-sets.go
new file mode 100644
index 00000000..9ceb8085
--- /dev/null
+++ b/cmd/erasure-sets.go
@@ -0,0 +1,1193 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "hash/crc32"
+ "math/rand"
+ "net/http"
+ "reflect"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/dchest/siphash"
+ "github.com/google/uuid"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio-go/v7/pkg/set"
+ "github.com/minio/minio-go/v7/pkg/tags"
+ "github.com/minio/minio/internal/dsync"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/console"
+ "github.com/minio/pkg/v3/sync/errgroup"
+ "github.com/puzpuzpuz/xsync/v3"
+)
+
+// setsDsyncLockers is encapsulated type for Close()
+type setsDsyncLockers [][]dsync.NetLocker
+
+// erasureSets implements ObjectLayer combining a static list of erasure coded
+// object sets. NOTE: There is no dynamic scaling allowed or intended in
+// current design.
+type erasureSets struct {
+ sets []*erasureObjects
+
+ // Reference format.
+ format *formatErasureV3
+
+ // erasureDisks mutex to lock erasureDisks.
+ erasureDisksMu sync.RWMutex
+
+ // Re-ordered list of disks per set.
+ erasureDisks [][]StorageAPI
+
+ // Distributed locker clients.
+ erasureLockers setsDsyncLockers
+
+ // Distributed lock owner (constant per running instance).
+ erasureLockOwner string
+
+ // List of endpoints provided on the command line.
+ endpoints PoolEndpoints
+
+ // String version of all the endpoints, an optimization
+ // to avoid url.String() conversion taking CPU on
+ // large disk setups.
+ endpointStrings []string
+
+ // Total number of sets and the number of disks per set.
+ setCount, setDriveCount int
+ defaultParityCount int
+
+ poolIndex int
+
+ // Distribution algorithm of choice.
+ distributionAlgo string
+ deploymentID [16]byte
+
+ lastConnectDisksOpTime time.Time
+}
+
+var staleUploadsCleanupIntervalChangedCh = make(chan struct{})
+
+func (s *erasureSets) getDiskMap() map[Endpoint]StorageAPI {
+ diskMap := make(map[Endpoint]StorageAPI)
+
+ s.erasureDisksMu.RLock()
+ defer s.erasureDisksMu.RUnlock()
+
+ for i := range s.setCount {
+ for j := 0; j < s.setDriveCount; j++ {
+ disk := s.erasureDisks[i][j]
+ if disk == OfflineDisk {
+ continue
+ }
+ if !disk.IsOnline() {
+ continue
+ }
+ diskMap[disk.Endpoint()] = disk
+ }
+ }
+ return diskMap
+}
+
+// Initializes a new StorageAPI from the endpoint argument, returns
+// StorageAPI and also `format` which exists on the disk.
+func connectEndpoint(endpoint Endpoint) (StorageAPI, *formatErasureV3, error) {
+ disk, err := newStorageAPI(endpoint, storageOpts{
+ cleanUp: false,
+ healthCheck: false,
+ })
+ if err != nil {
+ return nil, nil, err
+ }
+
+ format, err := loadFormatErasure(disk, false)
+ if err != nil {
+ disk.Close()
+ return nil, nil, fmt.Errorf("Drive: %s returned %w", disk, err) // make sure to '%w' to wrap the error
+ }
+
+ disk.Close()
+ disk, err = newStorageAPI(endpoint, storageOpts{
+ cleanUp: true,
+ healthCheck: true,
+ })
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return disk, format, nil
+}
+
+// findDiskIndex - returns the i,j'th position of the input `diskID` against the reference
+// format, after successful validation.
+// - i'th position is the set index
+// - j'th position is the disk index in the current set
+func findDiskIndexByDiskID(refFormat *formatErasureV3, diskID string) (int, int, error) {
+ if diskID == "" {
+ return -1, -1, errDiskNotFound
+ }
+ if diskID == offlineDiskUUID {
+ return -1, -1, fmt.Errorf("DriveID: %s is offline", diskID)
+ }
+ for i := range len(refFormat.Erasure.Sets) {
+ for j := 0; j < len(refFormat.Erasure.Sets[0]); j++ {
+ if refFormat.Erasure.Sets[i][j] == diskID {
+ return i, j, nil
+ }
+ }
+ }
+
+ return -1, -1, fmt.Errorf("DriveID: %s not found", diskID)
+}
+
+// findDiskIndex - returns the i,j'th position of the input `format` against the reference
+// format, after successful validation.
+// - i'th position is the set index
+// - j'th position is the disk index in the current set
+func findDiskIndex(refFormat, format *formatErasureV3) (int, int, error) {
+ if err := formatErasureV3Check(refFormat, format); err != nil {
+ return 0, 0, err
+ }
+
+ if format.Erasure.This == offlineDiskUUID {
+ return -1, -1, fmt.Errorf("DriveID: %s is offline", format.Erasure.This)
+ }
+
+ for i := range len(refFormat.Erasure.Sets) {
+ for j := 0; j < len(refFormat.Erasure.Sets[0]); j++ {
+ if refFormat.Erasure.Sets[i][j] == format.Erasure.This {
+ return i, j, nil
+ }
+ }
+ }
+
+ return -1, -1, fmt.Errorf("DriveID: %s not found", format.Erasure.This)
+}
+
+// Legacy returns 'true' if distribution algo is CRCMOD
+func (s *erasureSets) Legacy() (ok bool) {
+ return s.distributionAlgo == formatErasureVersionV2DistributionAlgoV1
+}
+
+// connectDisks - attempt to connect all the endpoints, loads format
+// and re-arranges the disks in proper position.
+func (s *erasureSets) connectDisks(log bool) {
+ defer func() {
+ s.lastConnectDisksOpTime = time.Now()
+ }()
+
+ var wg sync.WaitGroup
+ diskMap := s.getDiskMap()
+ for _, endpoint := range s.endpoints.Endpoints {
+ cdisk := diskMap[endpoint]
+ if cdisk != nil && cdisk.IsOnline() {
+ if s.lastConnectDisksOpTime.IsZero() {
+ continue
+ }
+
+ // An online-disk means its a valid disk but it may be a re-connected disk
+ // we verify that here based on LastConn(), however we make sure to avoid
+ // putting it back into the s.erasureDisks by re-placing the disk again.
+ _, setIndex, _ := cdisk.GetDiskLoc()
+ if setIndex != -1 {
+ continue
+ }
+ }
+ if cdisk != nil {
+ // Close previous offline disk.
+ cdisk.Close()
+ }
+
+ wg.Add(1)
+ go func(endpoint Endpoint) {
+ defer wg.Done()
+ disk, format, err := connectEndpoint(endpoint)
+ if err != nil {
+ if endpoint.IsLocal && errors.Is(err, errUnformattedDisk) {
+ globalBackgroundHealState.pushHealLocalDisks(endpoint)
+ } else if !errors.Is(err, errDriveIsRoot) {
+ if log {
+ printEndpointError(endpoint, err, true)
+ }
+ }
+ return
+ }
+ if disk.IsLocal() {
+ h := disk.Healing()
+ if h != nil && !h.Finished {
+ globalBackgroundHealState.pushHealLocalDisks(disk.Endpoint())
+ }
+ }
+ s.erasureDisksMu.Lock()
+ setIndex, diskIndex, err := findDiskIndex(s.format, format)
+ if err != nil {
+ printEndpointError(endpoint, err, false)
+ disk.Close()
+ s.erasureDisksMu.Unlock()
+ return
+ }
+
+ if currentDisk := s.erasureDisks[setIndex][diskIndex]; currentDisk != nil {
+ if !reflect.DeepEqual(currentDisk.Endpoint(), disk.Endpoint()) {
+ err = fmt.Errorf("Detected unexpected drive ordering refusing to use the drive: expecting %s, found %s, refusing to use the drive",
+ currentDisk.Endpoint(), disk.Endpoint())
+ printEndpointError(endpoint, err, false)
+ disk.Close()
+ s.erasureDisksMu.Unlock()
+ return
+ }
+ s.erasureDisks[setIndex][diskIndex].Close()
+ }
+
+ disk.SetDiskID(format.Erasure.This)
+ s.erasureDisks[setIndex][diskIndex] = disk
+
+ if disk.IsLocal() {
+ globalLocalDrivesMu.Lock()
+ if globalIsDistErasure {
+ globalLocalSetDrives[s.poolIndex][setIndex][diskIndex] = disk
+ }
+ globalLocalDrivesMap[disk.Endpoint().String()] = disk
+ globalLocalDrivesMu.Unlock()
+ }
+ s.erasureDisksMu.Unlock()
+ }(endpoint)
+ }
+
+ wg.Wait()
+}
+
+// monitorAndConnectEndpoints this is a monitoring loop to keep track of disconnected
+// endpoints by reconnecting them and making sure to place them into right position in
+// the set topology, this monitoring happens at a given monitoring interval.
+func (s *erasureSets) monitorAndConnectEndpoints(ctx context.Context, monitorInterval time.Duration) {
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+
+ time.Sleep(time.Duration(r.Float64() * float64(time.Second)))
+
+ // Pre-emptively connect the disks if possible.
+ s.connectDisks(false)
+
+ monitor := time.NewTimer(monitorInterval)
+ defer monitor.Stop()
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-monitor.C:
+ if serverDebugLog {
+ console.Debugln("running drive monitoring")
+ }
+
+ s.connectDisks(true)
+
+ // Reset the timer for next interval
+ monitor.Reset(monitorInterval)
+ }
+ }
+}
+
+func (s *erasureSets) GetLockers(setIndex int) func() ([]dsync.NetLocker, string) {
+ return func() ([]dsync.NetLocker, string) {
+ lockers := make([]dsync.NetLocker, len(s.erasureLockers[setIndex]))
+ copy(lockers, s.erasureLockers[setIndex])
+ return lockers, s.erasureLockOwner
+ }
+}
+
+func (s *erasureSets) GetEndpointStrings(setIndex int) func() []string {
+ return func() []string {
+ eps := make([]string, s.setDriveCount)
+ copy(eps, s.endpointStrings[setIndex*s.setDriveCount:setIndex*s.setDriveCount+s.setDriveCount])
+ return eps
+ }
+}
+
+func (s *erasureSets) GetEndpoints(setIndex int) func() []Endpoint {
+ return func() []Endpoint {
+ eps := make([]Endpoint, s.setDriveCount)
+ copy(eps, s.endpoints.Endpoints[setIndex*s.setDriveCount:setIndex*s.setDriveCount+s.setDriveCount])
+ return eps
+ }
+}
+
+// GetDisks returns a closure for a given set, which provides list of disks per set.
+func (s *erasureSets) GetDisks(setIndex int) func() []StorageAPI {
+ return func() []StorageAPI {
+ s.erasureDisksMu.RLock()
+ defer s.erasureDisksMu.RUnlock()
+ disks := make([]StorageAPI, s.setDriveCount)
+ copy(disks, s.erasureDisks[setIndex])
+ return disks
+ }
+}
+
+// defaultMonitorConnectEndpointInterval is the interval to monitor endpoint connections.
+// Must be bigger than defaultMonitorNewDiskInterval.
+const defaultMonitorConnectEndpointInterval = defaultMonitorNewDiskInterval + time.Second*5
+
+// Initialize new set of erasure coded sets.
+func newErasureSets(ctx context.Context, endpoints PoolEndpoints, storageDisks []StorageAPI, format *formatErasureV3, defaultParityCount, poolIdx int) (*erasureSets, error) {
+ setCount := len(format.Erasure.Sets)
+ setDriveCount := len(format.Erasure.Sets[0])
+
+ endpointStrings := make([]string, len(endpoints.Endpoints))
+ for i, endpoint := range endpoints.Endpoints {
+ endpointStrings[i] = endpoint.String()
+ }
+
+ // Initialize the erasure sets instance.
+ s := &erasureSets{
+ sets: make([]*erasureObjects, setCount),
+ erasureDisks: make([][]StorageAPI, setCount),
+ erasureLockers: make([][]dsync.NetLocker, setCount),
+ erasureLockOwner: globalLocalNodeName,
+ endpoints: endpoints,
+ endpointStrings: endpointStrings,
+ setCount: setCount,
+ setDriveCount: setDriveCount,
+ defaultParityCount: defaultParityCount,
+ format: format,
+ distributionAlgo: format.Erasure.DistributionAlgo,
+ deploymentID: uuid.MustParse(format.ID),
+ poolIndex: poolIdx,
+ }
+
+ mutex := newNSLock(globalIsDistErasure)
+
+ for i := range setCount {
+ s.erasureDisks[i] = make([]StorageAPI, setDriveCount)
+ }
+
+ erasureLockers := map[string]dsync.NetLocker{}
+ for _, endpoint := range endpoints.Endpoints {
+ if _, ok := erasureLockers[endpoint.Host]; !ok {
+ erasureLockers[endpoint.Host] = newLockAPI(endpoint)
+ }
+ }
+
+ var wg sync.WaitGroup
+ var lk sync.Mutex
+ for i := range setCount {
+ lockerEpSet := set.NewStringSet()
+ for j := range setDriveCount {
+ wg.Add(1)
+ go func(i int, endpoint Endpoint) {
+ defer wg.Done()
+
+ lk.Lock()
+ // Only add lockers only one per endpoint and per erasure set.
+ if locker, ok := erasureLockers[endpoint.Host]; ok && !lockerEpSet.Contains(endpoint.Host) {
+ lockerEpSet.Add(endpoint.Host)
+ s.erasureLockers[i] = append(s.erasureLockers[i], locker)
+ }
+ lk.Unlock()
+ }(i, endpoints.Endpoints[i*setDriveCount+j])
+ }
+ }
+ wg.Wait()
+
+ for i := range setCount {
+ wg.Add(1)
+ go func(i int) {
+ defer wg.Done()
+
+ var innerWg sync.WaitGroup
+ for j := range setDriveCount {
+ disk := storageDisks[i*setDriveCount+j]
+ if disk == nil {
+ continue
+ }
+
+ if disk.IsLocal() && globalIsDistErasure {
+ globalLocalDrivesMu.RLock()
+ ldisk := globalLocalSetDrives[poolIdx][i][j]
+ if ldisk == nil {
+ globalLocalDrivesMu.RUnlock()
+ continue
+ }
+ disk.Close()
+ disk = ldisk
+ globalLocalDrivesMu.RUnlock()
+ }
+
+ innerWg.Add(1)
+ go func(disk StorageAPI, i, j int) {
+ defer innerWg.Done()
+ diskID, err := disk.GetDiskID()
+ if err != nil {
+ if !errors.Is(err, errUnformattedDisk) {
+ bootLogIf(ctx, err)
+ }
+ return
+ }
+ if diskID == "" {
+ return
+ }
+ s.erasureDisks[i][j] = disk
+ }(disk, i, j)
+ }
+
+ innerWg.Wait()
+
+ // Initialize erasure objects for a given set.
+ s.sets[i] = &erasureObjects{
+ setIndex: i,
+ poolIndex: poolIdx,
+ setDriveCount: setDriveCount,
+ defaultParityCount: defaultParityCount,
+ getDisks: s.GetDisks(i),
+ getLockers: s.GetLockers(i),
+ getEndpoints: s.GetEndpoints(i),
+ getEndpointStrings: s.GetEndpointStrings(i),
+ nsMutex: mutex,
+ }
+ }(i)
+ }
+
+ wg.Wait()
+
+ // start cleanup stale uploads go-routine.
+ go s.cleanupStaleUploads(ctx)
+
+ // start cleanup of deleted objects.
+ go s.cleanupDeletedObjects(ctx)
+
+ // Start the disk monitoring and connect routine.
+ if !globalIsTesting {
+ go s.monitorAndConnectEndpoints(ctx, defaultMonitorConnectEndpointInterval)
+ }
+
+ return s, nil
+}
+
+// cleanup ".trash/" folder every 5m minutes with sufficient sleep cycles, between each
+// deletes a dynamic sleeper is used with a factor of 10 ratio with max delay between
+// deletes to be 2 seconds.
+func (s *erasureSets) cleanupDeletedObjects(ctx context.Context) {
+ timer := time.NewTimer(globalAPIConfig.getDeleteCleanupInterval())
+ defer timer.Stop()
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-timer.C:
+ var wg sync.WaitGroup
+ for _, set := range s.sets {
+ wg.Add(1)
+ go func(set *erasureObjects) {
+ defer wg.Done()
+ if set == nil {
+ return
+ }
+ set.cleanupDeletedObjects(ctx)
+ }(set)
+ }
+ wg.Wait()
+
+ // Reset for the next interval
+ timer.Reset(globalAPIConfig.getDeleteCleanupInterval())
+ }
+ }
+}
+
+func (s *erasureSets) cleanupStaleUploads(ctx context.Context) {
+ timer := time.NewTimer(globalAPIConfig.getStaleUploadsCleanupInterval())
+ defer timer.Stop()
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-timer.C:
+ var wg sync.WaitGroup
+ for _, set := range s.sets {
+ wg.Add(1)
+ go func(set *erasureObjects) {
+ defer wg.Done()
+ if set == nil {
+ return
+ }
+ set.cleanupStaleUploads(ctx)
+ }(set)
+ }
+ wg.Wait()
+ case <-staleUploadsCleanupIntervalChangedCh:
+ }
+
+ // Reset for the next interval
+ timer.Reset(globalAPIConfig.getStaleUploadsCleanupInterval())
+ }
+}
+
+type auditObjectOp struct {
+ Name string `json:"name"`
+ Pool int `json:"poolId"`
+ Set int `json:"setId"`
+}
+
+func (op auditObjectOp) String() string {
+ // Flatten the auditObjectOp
+ return fmt.Sprintf("name=%s,pool=%d,set=%d", op.Name, op.Pool, op.Set)
+}
+
+// Add erasure set information to the current context
+func auditObjectErasureSet(ctx context.Context, api, object string, set *erasureObjects) {
+ if len(logger.AuditTargets()) == 0 {
+ return
+ }
+
+ op := auditObjectOp{
+ Name: decodeDirObject(object),
+ Pool: set.poolIndex + 1,
+ Set: set.setIndex + 1,
+ }
+
+ logger.GetReqInfo(ctx).AppendTags(api, op.String())
+}
+
+// NewNSLock - initialize a new namespace RWLocker instance.
+func (s *erasureSets) NewNSLock(bucket string, objects ...string) RWLocker {
+ return s.sets[0].NewNSLock(bucket, objects...)
+}
+
+// SetDriveCount returns the current drives per set.
+func (s *erasureSets) SetDriveCount() int {
+ return s.setDriveCount
+}
+
+// ParityCount returns the default parity count used while erasure
+// coding objects
+func (s *erasureSets) ParityCount() int {
+ return s.defaultParityCount
+}
+
+// StorageInfo - combines output of StorageInfo across all erasure coded object sets.
+func (s *erasureSets) StorageInfo(ctx context.Context) StorageInfo {
+ var storageInfo madmin.StorageInfo
+
+ storageInfos := make([]madmin.StorageInfo, len(s.sets))
+
+ g := errgroup.WithNErrs(len(s.sets))
+ for index := range s.sets {
+ g.Go(func() error {
+ storageInfos[index] = s.sets[index].StorageInfo(ctx)
+ return nil
+ }, index)
+ }
+
+ // Wait for the go routines.
+ g.Wait()
+
+ for _, lstorageInfo := range storageInfos {
+ storageInfo.Disks = append(storageInfo.Disks, lstorageInfo.Disks...)
+ }
+
+ return storageInfo
+}
+
+// StorageInfo - combines output of StorageInfo across all erasure coded object sets.
+func (s *erasureSets) LocalStorageInfo(ctx context.Context, metrics bool) StorageInfo {
+ var storageInfo StorageInfo
+
+ storageInfos := make([]StorageInfo, len(s.sets))
+
+ g := errgroup.WithNErrs(len(s.sets))
+ for index := range s.sets {
+ g.Go(func() error {
+ storageInfos[index] = s.sets[index].LocalStorageInfo(ctx, metrics)
+ return nil
+ }, index)
+ }
+
+ // Wait for the go routines.
+ g.Wait()
+
+ for _, lstorageInfo := range storageInfos {
+ storageInfo.Disks = append(storageInfo.Disks, lstorageInfo.Disks...)
+ }
+
+ return storageInfo
+}
+
+// Shutdown shutsdown all erasure coded sets in parallel
+// returns error upon first error.
+func (s *erasureSets) Shutdown(ctx context.Context) error {
+ g := errgroup.WithNErrs(len(s.sets))
+
+ for index := range s.sets {
+ g.Go(func() error {
+ return s.sets[index].Shutdown(ctx)
+ }, index)
+ }
+
+ for _, err := range g.Wait() {
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// hashes the key returning an integer based on the input algorithm.
+// This function currently supports
+// - CRCMOD
+// - SIPMOD
+// - all new algos.
+func sipHashMod(key string, cardinality int, id [16]byte) int {
+ if cardinality <= 0 {
+ return -1
+ }
+ // use the faster version as per siphash docs
+ // https://github.com/dchest/siphash#usage
+ k0, k1 := binary.LittleEndian.Uint64(id[0:8]), binary.LittleEndian.Uint64(id[8:16])
+ sum64 := siphash.Hash(k0, k1, []byte(key))
+ return int(sum64 % uint64(cardinality))
+}
+
+func crcHashMod(key string, cardinality int) int {
+ if cardinality <= 0 {
+ return -1
+ }
+ keyCrc := crc32.Checksum([]byte(key), crc32.IEEETable)
+ return int(keyCrc % uint32(cardinality))
+}
+
+func hashKey(algo string, key string, cardinality int, id [16]byte) int {
+ switch algo {
+ case formatErasureVersionV2DistributionAlgoV1:
+ return crcHashMod(key, cardinality)
+ case formatErasureVersionV3DistributionAlgoV2, formatErasureVersionV3DistributionAlgoV3:
+ return sipHashMod(key, cardinality, id)
+ default:
+ // Unknown algorithm returns -1, also if cardinality is lesser than 0.
+ return -1
+ }
+}
+
+// Returns always a same erasure coded set for a given input.
+func (s *erasureSets) getHashedSetIndex(input string) int {
+ return hashKey(s.distributionAlgo, input, len(s.sets), s.deploymentID)
+}
+
+// Returns always a same erasure coded set for a given input.
+func (s *erasureSets) getHashedSet(input string) (set *erasureObjects) {
+ return s.sets[s.getHashedSetIndex(input)]
+}
+
+// listDeletedBuckets lists deleted buckets from all disks.
+func listDeletedBuckets(ctx context.Context, storageDisks []StorageAPI, delBuckets *xsync.MapOf[string, VolInfo], readQuorum int) error {
+ g := errgroup.WithNErrs(len(storageDisks))
+ for index := range storageDisks {
+ g.Go(func() error {
+ if storageDisks[index] == nil {
+ // we ignore disk not found errors
+ return nil
+ }
+ volsInfo, err := storageDisks[index].ListDir(ctx, "", minioMetaBucket, pathJoin(bucketMetaPrefix, deletedBucketsPrefix), -1)
+ if err != nil {
+ if errors.Is(err, errFileNotFound) {
+ return nil
+ }
+ return err
+ }
+ for _, volName := range volsInfo {
+ vi, err := storageDisks[index].StatVol(ctx, pathJoin(minioMetaBucket, bucketMetaPrefix, deletedBucketsPrefix, volName))
+ if err == nil {
+ vi.Name = strings.TrimSuffix(volName, SlashSeparator)
+ delBuckets.Store(volName, vi)
+ }
+ }
+ return nil
+ }, index)
+ }
+ return reduceReadQuorumErrs(ctx, g.Wait(), bucketMetadataOpIgnoredErrs, readQuorum)
+}
+
+// --- Object Operations ---
+
+// GetObjectNInfo - returns object info and locked object ReadCloser
+func (s *erasureSets) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions) (gr *GetObjectReader, err error) {
+ set := s.getHashedSet(object)
+ return set.GetObjectNInfo(ctx, bucket, object, rs, h, opts)
+}
+
+// PutObject - writes an object to hashedSet based on the object name.
+func (s *erasureSets) PutObject(ctx context.Context, bucket string, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) {
+ set := s.getHashedSet(object)
+ return set.PutObject(ctx, bucket, object, data, opts)
+}
+
+// GetObjectInfo - reads object metadata from the hashedSet based on the object name.
+func (s *erasureSets) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
+ set := s.getHashedSet(object)
+ return set.GetObjectInfo(ctx, bucket, object, opts)
+}
+
+func (s *erasureSets) deletePrefix(ctx context.Context, bucket string, prefix string) error {
+ var wg sync.WaitGroup
+ wg.Add(len(s.sets))
+ for _, s := range s.sets {
+ go func(s *erasureObjects) {
+ defer wg.Done()
+ // This is a force delete, no reason to throw errors.
+ s.DeleteObject(ctx, bucket, prefix, ObjectOptions{DeletePrefix: true})
+ }(s)
+ }
+ wg.Wait()
+ return nil
+}
+
+// DeleteObject - deletes an object from the hashedSet based on the object name.
+func (s *erasureSets) DeleteObject(ctx context.Context, bucket string, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
+ if opts.DeletePrefix && !opts.DeletePrefixObject {
+ err := s.deletePrefix(ctx, bucket, object)
+ return ObjectInfo{}, err
+ }
+ set := s.getHashedSet(object)
+ return set.DeleteObject(ctx, bucket, object, opts)
+}
+
+// DeleteObjects - bulk delete of objects
+// Bulk delete is only possible within one set. For that purpose
+// objects are group by set first, and then bulk delete is invoked
+// for each set, the error response of each delete will be returned
+func (s *erasureSets) DeleteObjects(ctx context.Context, bucket string, objects []ObjectToDelete, opts ObjectOptions) ([]DeletedObject, []error) {
+ type delObj struct {
+ // Set index associated to this object
+ setIndex int
+ // Original index from the list of arguments
+ // where this object is passed
+ origIndex int
+ // object to delete
+ object ObjectToDelete
+ }
+
+ // Transform []delObj to the list of object names
+ toNames := func(delObjs []delObj) []ObjectToDelete {
+ objs := make([]ObjectToDelete, len(delObjs))
+ for i, obj := range delObjs {
+ objs[i] = obj.object
+ }
+ return objs
+ }
+
+ // The result of delete operation on all passed objects
+ delErrs := make([]error, len(objects))
+
+ // The result of delete objects
+ delObjects := make([]DeletedObject, len(objects))
+
+ // A map between a set and its associated objects
+ objSetMap := make(map[int][]delObj)
+
+ // Group objects by set index
+ for i, object := range objects {
+ index := s.getHashedSetIndex(object.ObjectName)
+ objSetMap[index] = append(objSetMap[index], delObj{setIndex: index, origIndex: i, object: object})
+ }
+
+ // Invoke bulk delete on objects per set and save
+ // the result of the delete operation
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+ wg.Add(len(objSetMap))
+ for setIdx, objsGroup := range objSetMap {
+ go func(set *erasureObjects, group []delObj) {
+ defer wg.Done()
+ dobjects, errs := set.DeleteObjects(ctx, bucket, toNames(group), opts)
+ mu.Lock()
+ defer mu.Unlock()
+ for i, obj := range group {
+ delErrs[obj.origIndex] = errs[i]
+ delObjects[obj.origIndex] = dobjects[i]
+ }
+ }(s.sets[setIdx], objsGroup)
+ }
+ wg.Wait()
+
+ return delObjects, delErrs
+}
+
+// CopyObject - copies objects from one hashedSet to another hashedSet, on server side.
+func (s *erasureSets) CopyObject(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error) {
+ srcSet := s.getHashedSet(srcObject)
+ dstSet := s.getHashedSet(dstObject)
+
+ cpSrcDstSame := srcSet == dstSet
+ // Check if this request is only metadata update.
+ if cpSrcDstSame && srcInfo.metadataOnly {
+ // Version ID is set for the destination and source == destination version ID.
+ // perform an in-place update.
+ if dstOpts.VersionID != "" && srcOpts.VersionID == dstOpts.VersionID {
+ srcInfo.Reader.Close() // We are not interested in the reader stream at this point close it.
+ return srcSet.CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts)
+ }
+ // Destination is not versioned and source version ID is empty
+ // perform an in-place update.
+ if !dstOpts.Versioned && srcOpts.VersionID == "" {
+ srcInfo.Reader.Close() // We are not interested in the reader stream at this point close it.
+ return srcSet.CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts)
+ }
+ // CopyObject optimization where we don't create an entire copy
+ // of the content, instead we add a reference, we disallow legacy
+ // objects to be self referenced in this manner so make sure
+ // that we actually create a new dataDir for legacy objects.
+ if dstOpts.Versioned && srcOpts.VersionID != dstOpts.VersionID && !srcInfo.Legacy {
+ srcInfo.versionOnly = true
+ srcInfo.Reader.Close() // We are not interested in the reader stream at this point close it.
+ return srcSet.CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts)
+ }
+ }
+
+ putOpts := ObjectOptions{
+ ServerSideEncryption: dstOpts.ServerSideEncryption,
+ UserDefined: srcInfo.UserDefined,
+ Versioned: dstOpts.Versioned,
+ VersionID: dstOpts.VersionID,
+ MTime: dstOpts.MTime,
+ EncryptFn: dstOpts.EncryptFn,
+ WantChecksum: dstOpts.WantChecksum,
+ WantServerSideChecksumType: dstOpts.WantServerSideChecksumType,
+ }
+
+ return dstSet.putObject(ctx, dstBucket, dstObject, srcInfo.PutObjReader, putOpts)
+}
+
+func (s *erasureSets) ListMultipartUploads(ctx context.Context, bucket, prefix, keyMarker, uploadIDMarker, delimiter string, maxUploads int) (result ListMultipartsInfo, err error) {
+ // In list multipart uploads we are going to treat input prefix as the object,
+ // this means that we are not supporting directory navigation.
+ set := s.getHashedSet(prefix)
+ return set.ListMultipartUploads(ctx, bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
+}
+
+// Initiate a new multipart upload on a hashedSet based on object name.
+func (s *erasureSets) NewMultipartUpload(ctx context.Context, bucket, object string, opts ObjectOptions) (res *NewMultipartUploadResult, err error) {
+ set := s.getHashedSet(object)
+ return set.NewMultipartUpload(ctx, bucket, object, opts)
+}
+
+// PutObjectPart - writes part of an object to hashedSet based on the object name.
+func (s *erasureSets) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, data *PutObjReader, opts ObjectOptions) (info PartInfo, err error) {
+ set := s.getHashedSet(object)
+ return set.PutObjectPart(ctx, bucket, object, uploadID, partID, data, opts)
+}
+
+// GetMultipartInfo - return multipart metadata info uploaded at hashedSet.
+func (s *erasureSets) GetMultipartInfo(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) (result MultipartInfo, err error) {
+ set := s.getHashedSet(object)
+ return set.GetMultipartInfo(ctx, bucket, object, uploadID, opts)
+}
+
+// ListObjectParts - lists all uploaded parts to an object in hashedSet.
+func (s *erasureSets) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int, opts ObjectOptions) (result ListPartsInfo, err error) {
+ set := s.getHashedSet(object)
+ return set.ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts)
+}
+
+// Aborts an in-progress multipart operation on hashedSet based on the object name.
+func (s *erasureSets) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) error {
+ set := s.getHashedSet(object)
+ return set.AbortMultipartUpload(ctx, bucket, object, uploadID, opts)
+}
+
+// CompleteMultipartUpload - completes a pending multipart transaction, on hashedSet based on object name.
+func (s *erasureSets) CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart, opts ObjectOptions) (objInfo ObjectInfo, err error) {
+ set := s.getHashedSet(object)
+ return set.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts)
+}
+
+/*
+
+All disks online
+-----------------
+- All Unformatted - format all and return success.
+- Some Unformatted - format all and return success.
+- Any JBOD inconsistent - return failure
+- Some are corrupt (missing format.json) - return failure
+- Any unrecognized disks - return failure
+
+Some disks are offline and we have quorum.
+-----------------
+- Some unformatted - format all and return success,
+ treat disks offline as corrupted.
+- Any JBOD inconsistent - return failure
+- Some are corrupt (missing format.json)
+- Any unrecognized disks - return failure
+
+No read quorum
+-----------------
+failure for all cases.
+
+// Pseudo code for managing `format.json`.
+
+// Generic checks.
+if (no quorum) return error
+if (any disk is corrupt) return error // Always error
+if (jbod inconsistent) return error // Always error.
+if (disks not recognized) // Always error.
+
+// Specific checks.
+if (all disks online)
+ if (all disks return format.json)
+ if (jbod consistent)
+ if (all disks recognized)
+ return
+ else
+ if (all disks return format.json not found)
+ return error
+ else (some disks return format.json not found)
+ (heal format)
+ return
+ fi
+ fi
+else
+ if (some disks return format.json not found)
+ // Offline disks are marked as dead.
+ (heal format) // Offline disks should be marked as dead.
+ return success
+ fi
+fi
+*/
+
+func formatsToDrivesInfo(endpoints Endpoints, formats []*formatErasureV3, sErrs []error) (beforeDrives []madmin.HealDriveInfo) {
+ beforeDrives = make([]madmin.HealDriveInfo, len(endpoints))
+ // Existing formats are available (i.e. ok), so save it in
+ // result, also populate disks to be healed.
+ for i, format := range formats {
+ drive := endpoints.GetString(i)
+ state := madmin.DriveStateCorrupt
+ switch {
+ case format != nil:
+ state = madmin.DriveStateOk
+ case sErrs[i] == errUnformattedDisk:
+ state = madmin.DriveStateMissing
+ case sErrs[i] == errDiskNotFound:
+ state = madmin.DriveStateOffline
+ }
+ beforeDrives[i] = madmin.HealDriveInfo{
+ UUID: func() string {
+ if format != nil {
+ return format.Erasure.This
+ }
+ return ""
+ }(),
+ Endpoint: drive,
+ State: state,
+ }
+ }
+
+ return beforeDrives
+}
+
+// HealFormat - heals missing `format.json` on fresh unformatted disks.
+func (s *erasureSets) HealFormat(ctx context.Context, dryRun bool) (res madmin.HealResultItem, err error) {
+ storageDisks, _ := initStorageDisksWithErrors(s.endpoints.Endpoints, storageOpts{
+ cleanUp: false,
+ healthCheck: false,
+ })
+
+ defer func(storageDisks []StorageAPI) {
+ if err != nil {
+ closeStorageDisks(storageDisks...)
+ }
+ }(storageDisks)
+
+ formats, sErrs := loadFormatErasureAll(storageDisks, true)
+ if err = checkFormatErasureValues(formats, storageDisks, s.setDriveCount); err != nil {
+ return madmin.HealResultItem{}, err
+ }
+
+ refFormat, err := getFormatErasureInQuorum(formats)
+ if err != nil {
+ return res, err
+ }
+
+ // Prepare heal-result
+ res = madmin.HealResultItem{
+ Type: madmin.HealItemMetadata,
+ Detail: "disk-format",
+ DiskCount: s.setCount * s.setDriveCount,
+ SetCount: s.setCount,
+ }
+
+ // Fetch all the drive info status.
+ beforeDrives := formatsToDrivesInfo(s.endpoints.Endpoints, formats, sErrs)
+
+ res.After.Drives = make([]madmin.HealDriveInfo, len(beforeDrives))
+ res.Before.Drives = make([]madmin.HealDriveInfo, len(beforeDrives))
+ // Copy "after" drive state too from before.
+ for k, v := range beforeDrives {
+ res.Before.Drives[k] = v
+ res.After.Drives[k] = v
+ }
+
+ if countErrs(sErrs, errUnformattedDisk) == 0 {
+ return res, errNoHealRequired
+ }
+
+ if !reflect.DeepEqual(s.format, refFormat) {
+ // Format is corrupted and unrecognized by the running instance.
+ healingLogIf(ctx, fmt.Errorf("Unable to heal the newly replaced drives due to format.json inconsistencies, please engage MinIO support for further assistance: %w",
+ errCorruptedFormat))
+ return res, errCorruptedFormat
+ }
+
+ formatOpID := mustGetUUID()
+
+ // Initialize a new set of set formats which will be written to disk.
+ newFormatSets, currentDisksInfo := newHealFormatSets(refFormat, s.setCount, s.setDriveCount, formats, sErrs)
+
+ if !dryRun {
+ tmpNewFormats := make([]*formatErasureV3, s.setCount*s.setDriveCount)
+ for i := range newFormatSets {
+ for j := range newFormatSets[i] {
+ if newFormatSets[i][j] == nil {
+ continue
+ }
+ res.After.Drives[i*s.setDriveCount+j].UUID = newFormatSets[i][j].Erasure.This
+ res.After.Drives[i*s.setDriveCount+j].State = madmin.DriveStateOk
+ tmpNewFormats[i*s.setDriveCount+j] = newFormatSets[i][j]
+ }
+ }
+
+ // Save new formats `format.json` on unformatted disks.
+ for index, format := range tmpNewFormats {
+ if storageDisks[index] == nil || format == nil {
+ continue
+ }
+ if err := saveFormatErasure(storageDisks[index], format, formatOpID); err != nil {
+ healingLogIf(ctx, fmt.Errorf("Drive %s failed to write updated 'format.json': %v", storageDisks[index], err))
+ storageDisks[index].Close()
+ tmpNewFormats[index] = nil // this disk failed to write new format
+ }
+ }
+
+ s.erasureDisksMu.Lock()
+
+ for index, format := range tmpNewFormats {
+ if format == nil {
+ continue
+ }
+
+ m, n, err := findDiskIndexByDiskID(refFormat, format.Erasure.This)
+ if err != nil {
+ healingLogIf(ctx, err)
+ continue
+ }
+
+ if s.erasureDisks[m][n] != nil {
+ s.erasureDisks[m][n].Close()
+ }
+
+ if disk := storageDisks[index]; disk != nil {
+ if disk.IsLocal() {
+ xldisk, ok := disk.(*xlStorageDiskIDCheck)
+ if ok {
+ _, commonDeletes := calcCommonWritesDeletes(currentDisksInfo[m], (s.setDriveCount+1)/2)
+ xldisk.totalDeletes.Store(commonDeletes)
+ xldisk.storage.setDeleteAttribute(commonDeletes)
+
+ if globalDriveMonitoring {
+ go xldisk.monitorDiskWritable(xldisk.diskCtx)
+ }
+ }
+ } else {
+ disk.Close() // Close the remote storage client, re-initialize with healthchecks.
+ disk, err = newStorageRESTClient(disk.Endpoint(), true, globalGrid.Load())
+ if err != nil {
+ continue
+ }
+ }
+
+ s.erasureDisks[m][n] = disk
+
+ if disk.IsLocal() {
+ globalLocalDrivesMu.Lock()
+ if globalIsDistErasure {
+ globalLocalSetDrives[s.poolIndex][m][n] = disk
+ }
+ globalLocalDrivesMap[disk.Endpoint().String()] = disk
+ globalLocalDrivesMu.Unlock()
+ }
+ }
+ }
+
+ s.erasureDisksMu.Unlock()
+ }
+
+ return res, nil
+}
+
+// HealObject - heals inconsistent object on a hashedSet based on object name.
+func (s *erasureSets) HealObject(ctx context.Context, bucket, object, versionID string, opts madmin.HealOpts) (madmin.HealResultItem, error) {
+ return s.getHashedSet(object).HealObject(ctx, bucket, object, versionID, opts)
+}
+
+// PutObjectMetadata - replace or add metadata to an existing object/version
+func (s *erasureSets) PutObjectMetadata(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
+ er := s.getHashedSet(object)
+ return er.PutObjectMetadata(ctx, bucket, object, opts)
+}
+
+// DecomTieredObject - moves tiered object to another pool during decommissioning.
+func (s *erasureSets) DecomTieredObject(ctx context.Context, bucket, object string, fi FileInfo, opts ObjectOptions) error {
+ er := s.getHashedSet(object)
+ return er.DecomTieredObject(ctx, bucket, object, fi, opts)
+}
+
+// PutObjectTags - replace or add tags to an existing object
+func (s *erasureSets) PutObjectTags(ctx context.Context, bucket, object string, tags string, opts ObjectOptions) (ObjectInfo, error) {
+ er := s.getHashedSet(object)
+ return er.PutObjectTags(ctx, bucket, object, tags, opts)
+}
+
+// DeleteObjectTags - delete object tags from an existing object
+func (s *erasureSets) DeleteObjectTags(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
+ er := s.getHashedSet(object)
+ return er.DeleteObjectTags(ctx, bucket, object, opts)
+}
+
+// GetObjectTags - get object tags from an existing object
+func (s *erasureSets) GetObjectTags(ctx context.Context, bucket, object string, opts ObjectOptions) (*tags.Tags, error) {
+ er := s.getHashedSet(object)
+ return er.GetObjectTags(ctx, bucket, object, opts)
+}
+
+// TransitionObject - transition object content to target tier.
+func (s *erasureSets) TransitionObject(ctx context.Context, bucket, object string, opts ObjectOptions) error {
+ return s.getHashedSet(object).TransitionObject(ctx, bucket, object, opts)
+}
+
+// RestoreTransitionedObject - restore transitioned object content locally on this cluster.
+func (s *erasureSets) RestoreTransitionedObject(ctx context.Context, bucket, object string, opts ObjectOptions) error {
+ return s.getHashedSet(object).RestoreTransitionedObject(ctx, bucket, object, opts)
+}
+
+// CheckAbandonedParts - check object for abandoned parts.
+func (s *erasureSets) CheckAbandonedParts(ctx context.Context, bucket, object string, opts madmin.HealOpts) error {
+ return s.getHashedSet(object).checkAbandonedParts(ctx, bucket, object, opts)
+}
diff --git a/cmd/erasure-sets_test.go b/cmd/erasure-sets_test.go
new file mode 100644
index 00000000..63311d55
--- /dev/null
+++ b/cmd/erasure-sets_test.go
@@ -0,0 +1,237 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/google/uuid"
+)
+
+var testUUID = uuid.MustParse("f5c58c61-7175-4018-ab5e-a94fe9c2de4e")
+
+func BenchmarkCrcHash(b *testing.B) {
+ cases := []struct {
+ key int
+ }{
+ {16},
+ {64},
+ {128},
+ {256},
+ {512},
+ {1024},
+ }
+ for _, testCase := range cases {
+ key := randString(testCase.key)
+ b.Run("", func(b *testing.B) {
+ b.SetBytes(1024)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for b.Loop() {
+ crcHashMod(key, 16)
+ }
+ })
+ }
+}
+
+func BenchmarkSipHash(b *testing.B) {
+ cases := []struct {
+ key int
+ }{
+ {16},
+ {64},
+ {128},
+ {256},
+ {512},
+ {1024},
+ }
+ for _, testCase := range cases {
+ key := randString(testCase.key)
+ b.Run("", func(b *testing.B) {
+ b.SetBytes(1024)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for b.Loop() {
+ sipHashMod(key, 16, testUUID)
+ }
+ })
+ }
+}
+
+// TestSipHashMod - test sip hash.
+func TestSipHashMod(t *testing.T) {
+ testCases := []struct {
+ objectName string
+ sipHash int
+ }{
+ // cases which should pass the test.
+ // passing in valid object name.
+ {"object", 37},
+ {"The Shining Script .pdf", 38},
+ {"Cost Benefit Analysis (2009-2010).pptx", 59},
+ {"117Gn8rfHL2ACARPAhaFd0AGzic9pUbIA/5OCn5A", 35},
+ {"SHØRT", 49},
+ {"There are far too many object names, and far too few bucket names!", 8},
+ {"a/b/c/", 159},
+ {"/a/b/c", 96},
+ {string([]byte{0xff, 0xfe, 0xfd}), 147},
+ }
+
+ // Tests hashing order to be consistent.
+ for i, testCase := range testCases {
+ if sipHashElement := hashKey("SIPMOD", testCase.objectName, 200, testUUID); sipHashElement != testCase.sipHash {
+ t.Errorf("Test case %d: Expected \"%v\" but failed \"%v\"", i+1, testCase.sipHash, sipHashElement)
+ }
+ }
+
+ if sipHashElement := hashKey("SIPMOD", "This will fail", -1, testUUID); sipHashElement != -1 {
+ t.Errorf("Test: Expected \"-1\" but got \"%v\"", sipHashElement)
+ }
+
+ if sipHashElement := hashKey("SIPMOD", "This will fail", 0, testUUID); sipHashElement != -1 {
+ t.Errorf("Test: Expected \"-1\" but got \"%v\"", sipHashElement)
+ }
+
+ if sipHashElement := hashKey("UNKNOWN", "This will fail", 0, testUUID); sipHashElement != -1 {
+ t.Errorf("Test: Expected \"-1\" but got \"%v\"", sipHashElement)
+ }
+}
+
+// TestCrcHashMod - test crc hash.
+func TestCrcHashMod(t *testing.T) {
+ testCases := []struct {
+ objectName string
+ crcHash int
+ }{
+ // cases which should pass the test.
+ // passing in valid object name.
+ {"object", 28},
+ {"The Shining Script .pdf", 142},
+ {"Cost Benefit Analysis (2009-2010).pptx", 133},
+ {"117Gn8rfHL2ACARPAhaFd0AGzic9pUbIA/5OCn5A", 185},
+ {"SHØRT", 97},
+ {"There are far too many object names, and far too few bucket names!", 101},
+ {"a/b/c/", 193},
+ {"/a/b/c", 116},
+ {string([]byte{0xff, 0xfe, 0xfd}), 61},
+ }
+
+ // Tests hashing order to be consistent.
+ for i, testCase := range testCases {
+ if crcHashElement := hashKey("CRCMOD", testCase.objectName, 200, testUUID); crcHashElement != testCase.crcHash {
+ t.Errorf("Test case %d: Expected \"%v\" but failed \"%v\"", i+1, testCase.crcHash, crcHashElement)
+ }
+ }
+
+ if crcHashElement := hashKey("CRCMOD", "This will fail", -1, testUUID); crcHashElement != -1 {
+ t.Errorf("Test: Expected \"-1\" but got \"%v\"", crcHashElement)
+ }
+
+ if crcHashElement := hashKey("CRCMOD", "This will fail", 0, testUUID); crcHashElement != -1 {
+ t.Errorf("Test: Expected \"-1\" but got \"%v\"", crcHashElement)
+ }
+
+ if crcHashElement := hashKey("UNKNOWN", "This will fail", 0, testUUID); crcHashElement != -1 {
+ t.Errorf("Test: Expected \"-1\" but got \"%v\"", crcHashElement)
+ }
+}
+
+// TestNewErasure - tests initialization of all input disks
+// and constructs a valid `Erasure` object
+func TestNewErasureSets(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ nDisks := 16 // Maximum disks.
+ var erasureDisks []string
+ for range nDisks {
+ // Do not attempt to create this path, the test validates
+ // so that newErasureSets initializes non existing paths
+ // and successfully returns initialized object layer.
+ disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
+ erasureDisks = append(erasureDisks, disk)
+ defer os.RemoveAll(disk)
+ }
+
+ endpoints := mustGetNewEndpoints(0, 16, erasureDisks...)
+ _, _, err := waitForFormatErasure(true, endpoints, 1, 0, 16, "")
+ if err != errInvalidArgument {
+ t.Fatalf("Expecting error, got %s", err)
+ }
+
+ _, _, err = waitForFormatErasure(true, nil, 1, 1, 16, "")
+ if err != errInvalidArgument {
+ t.Fatalf("Expecting error, got %s", err)
+ }
+
+ // Initializes all erasure disks
+ storageDisks, format, err := waitForFormatErasure(true, endpoints, 1, 1, 16, "")
+ if err != nil {
+ t.Fatalf("Unable to format drives for erasure, %s", err)
+ }
+
+ ep := PoolEndpoints{Endpoints: endpoints}
+
+ parity, err := ecDrivesNoConfig(16)
+ if err != nil {
+ t.Fatalf("Unexpected error during EC drive config: %v", err)
+ }
+ if _, err := newErasureSets(ctx, ep, storageDisks, format, parity, 0); err != nil {
+ t.Fatalf("Unable to initialize erasure")
+ }
+}
+
+// TestHashedLayer - tests the hashed layer which will be returned
+// consistently for a given object name.
+func TestHashedLayer(t *testing.T) {
+ // Test distribution with 16 sets.
+ var objs [16]*erasureObjects
+ for i := range objs {
+ objs[i] = &erasureObjects{}
+ }
+
+ sets := &erasureSets{sets: objs[:], distributionAlgo: "CRCMOD"}
+
+ testCases := []struct {
+ objectName string
+ expectedObj *erasureObjects
+ }{
+ // cases which should pass the test.
+ // passing in valid object name.
+ {"object", objs[12]},
+ {"The Shining Script .pdf", objs[14]},
+ {"Cost Benefit Analysis (2009-2010).pptx", objs[13]},
+ {"117Gn8rfHL2ACARPAhaFd0AGzic9pUbIA/5OCn5A", objs[1]},
+ {"SHØRT", objs[9]},
+ {"There are far too many object names, and far too few bucket names!", objs[13]},
+ {"a/b/c/", objs[1]},
+ {"/a/b/c", objs[4]},
+ {string([]byte{0xff, 0xfe, 0xfd}), objs[13]},
+ }
+
+ // Tests hashing order to be consistent.
+ for i, testCase := range testCases {
+ gotObj := sets.getHashedSet(testCase.objectName)
+ if gotObj != testCase.expectedObj {
+ t.Errorf("Test case %d: Expected \"%#v\" but failed \"%#v\"", i+1, testCase.expectedObj, gotObj)
+ }
+ }
+}
diff --git a/cmd/erasure-utils.go b/cmd/erasure-utils.go
new file mode 100644
index 00000000..35385fed
--- /dev/null
+++ b/cmd/erasure-utils.go
@@ -0,0 +1,118 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/base64"
+ "fmt"
+ "io"
+ "strings"
+
+ "github.com/klauspost/reedsolomon"
+)
+
+// getDataBlockLen - get length of data blocks from encoded blocks.
+func getDataBlockLen(enBlocks [][]byte, dataBlocks int) int {
+ size := 0
+ // Figure out the data block length.
+ for _, block := range enBlocks[:dataBlocks] {
+ size += len(block)
+ }
+ return size
+}
+
+// Writes all the data blocks from encoded blocks until requested
+// outSize length. Provides a way to skip bytes until the offset.
+func writeDataBlocks(ctx context.Context, dst io.Writer, enBlocks [][]byte, dataBlocks int, offset int64, length int64) (int64, error) {
+ // Offset and out size cannot be negative.
+ if offset < 0 || length < 0 {
+ return 0, errUnexpected
+ }
+
+ // Do we have enough blocks?
+ if len(enBlocks) < dataBlocks {
+ return 0, reedsolomon.ErrTooFewShards
+ }
+
+ // Do we have enough data?
+ if int64(getDataBlockLen(enBlocks, dataBlocks)) < length {
+ return 0, reedsolomon.ErrShortData
+ }
+
+ // Counter to decrement total left to write.
+ write := length
+
+ // Counter to increment total written.
+ var totalWritten int64
+
+ // Write all data blocks to dst.
+ for _, block := range enBlocks[:dataBlocks] {
+ // Skip blocks until we have reached our offset.
+ if offset >= int64(len(block)) {
+ // Decrement offset.
+ offset -= int64(len(block))
+ continue
+ }
+
+ // Skip until offset.
+ block = block[offset:]
+
+ // Reset the offset for next iteration to read everything
+ // from subsequent blocks.
+ offset = 0
+
+ // We have written all the blocks, write the last remaining block.
+ if write < int64(len(block)) {
+ n, err := dst.Write(block[:write])
+ if err != nil {
+ return 0, err
+ }
+ totalWritten += int64(n)
+ break
+ }
+
+ // Copy the block.
+ n, err := dst.Write(block)
+ if err != nil {
+ return 0, err
+ }
+
+ // Decrement output size.
+ write -= int64(n)
+
+ // Increment written.
+ totalWritten += int64(n)
+ }
+
+ // Success.
+ return totalWritten, nil
+}
+
+// returns deploymentID from uploadID
+func getDeplIDFromUpload(uploadID string) (string, error) {
+ uploadBytes, err := base64.RawURLEncoding.DecodeString(uploadID)
+ if err != nil {
+ return "", fmt.Errorf("error parsing uploadID %s (%w)", uploadID, err)
+ }
+ slc := strings.SplitN(string(uploadBytes), ".", 2)
+ if len(slc) != 2 {
+ return "", fmt.Errorf("uploadID %s has incorrect format", uploadID)
+ }
+ return slc[0], nil
+}
diff --git a/cmd/erasure.go b/cmd/erasure.go
new file mode 100644
index 00000000..4e6674c3
--- /dev/null
+++ b/cmd/erasure.go
@@ -0,0 +1,572 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "maps"
+ "math/rand"
+ "os"
+ "runtime"
+ "sort"
+ "sync"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/dsync"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/pkg/v3/sync/errgroup"
+)
+
+// list all errors that can be ignore in a bucket operation.
+var bucketOpIgnoredErrs = append(baseIgnoredErrs, errDiskAccessDenied, errUnformattedDisk)
+
+// list all errors that can be ignored in a bucket metadata operation.
+var bucketMetadataOpIgnoredErrs = append(bucketOpIgnoredErrs, errVolumeNotFound)
+
+// OfflineDisk represents an unavailable disk.
+var OfflineDisk StorageAPI // zero value is nil
+
+// erasureObjects - Implements ER object layer.
+type erasureObjects struct {
+ setDriveCount int
+ defaultParityCount int
+
+ setIndex int
+ poolIndex int
+
+ // getDisks returns list of storageAPIs.
+ getDisks func() []StorageAPI
+
+ // getLockers returns list of remote and local lockers.
+ getLockers func() ([]dsync.NetLocker, string)
+
+ // getEndpoints returns list of endpoint belonging this set.
+ // some may be local and some remote.
+ getEndpoints func() []Endpoint
+
+ // getEndpoints returns list of endpoint strings belonging this set.
+ // some may be local and some remote.
+ getEndpointStrings func() []string
+
+ // Locker mutex map.
+ nsMutex *nsLockMap
+}
+
+// NewNSLock - initialize a new namespace RWLocker instance.
+func (er erasureObjects) NewNSLock(bucket string, objects ...string) RWLocker {
+ return er.nsMutex.NewNSLock(er.getLockers, bucket, objects...)
+}
+
+// Shutdown function for object storage interface.
+func (er erasureObjects) Shutdown(ctx context.Context) error {
+ // Add any object layer shutdown activities here.
+ closeStorageDisks(er.getDisks()...)
+ return nil
+}
+
+// defaultWQuorum write quorum based on setDriveCount and defaultParityCount
+func (er erasureObjects) defaultWQuorum() int {
+ dataCount := er.setDriveCount - er.defaultParityCount
+ if dataCount == er.defaultParityCount {
+ return dataCount + 1
+ }
+ return dataCount
+}
+
+// defaultRQuorum read quorum based on setDriveCount and defaultParityCount
+func (er erasureObjects) defaultRQuorum() int {
+ return er.setDriveCount - er.defaultParityCount
+}
+
+func diskErrToDriveState(err error) (state string) {
+ switch {
+ case errors.Is(err, errDiskNotFound) || errors.Is(err, context.DeadlineExceeded):
+ state = madmin.DriveStateOffline
+ case errors.Is(err, errCorruptedFormat) || errors.Is(err, errCorruptedBackend):
+ state = madmin.DriveStateCorrupt
+ case errors.Is(err, errUnformattedDisk):
+ state = madmin.DriveStateUnformatted
+ case errors.Is(err, errDiskAccessDenied):
+ state = madmin.DriveStatePermission
+ case errors.Is(err, errFaultyDisk):
+ state = madmin.DriveStateFaulty
+ case errors.Is(err, errDriveIsRoot):
+ state = madmin.DriveStateRootMount
+ case err == nil:
+ state = madmin.DriveStateOk
+ default:
+ state = fmt.Sprintf("%s (cause: %s)", madmin.DriveStateUnknown, err)
+ }
+
+ return state
+}
+
+func getOnlineOfflineDisksStats(disksInfo []madmin.Disk) (onlineDisks, offlineDisks madmin.BackendDisks) {
+ onlineDisks = make(madmin.BackendDisks)
+ offlineDisks = make(madmin.BackendDisks)
+
+ for _, disk := range disksInfo {
+ ep := disk.Endpoint
+ if _, ok := offlineDisks[ep]; !ok {
+ offlineDisks[ep] = 0
+ }
+ if _, ok := onlineDisks[ep]; !ok {
+ onlineDisks[ep] = 0
+ }
+ }
+
+ // Wait for the routines.
+ for _, disk := range disksInfo {
+ ep := disk.Endpoint
+ state := disk.State
+ if state != madmin.DriveStateOk && state != madmin.DriveStateUnformatted {
+ offlineDisks[ep]++
+ continue
+ }
+ onlineDisks[ep]++
+ }
+
+ rootDiskCount := 0
+ for _, di := range disksInfo {
+ if di.RootDisk {
+ rootDiskCount++
+ }
+ }
+
+ // Count offline disks as well to ensure consistent
+ // reportability of offline drives on local setups.
+ if len(disksInfo) == (rootDiskCount + offlineDisks.Sum()) {
+ // Success.
+ return onlineDisks, offlineDisks
+ }
+
+ // Root disk should be considered offline
+ for i := range disksInfo {
+ ep := disksInfo[i].Endpoint
+ if disksInfo[i].RootDisk {
+ offlineDisks[ep]++
+ onlineDisks[ep]--
+ }
+ }
+
+ return onlineDisks, offlineDisks
+}
+
+// getDisksInfo - fetch disks info across all other storage API.
+func getDisksInfo(disks []StorageAPI, endpoints []Endpoint, metrics bool) (disksInfo []madmin.Disk) {
+ disksInfo = make([]madmin.Disk, len(disks))
+
+ g := errgroup.WithNErrs(len(disks))
+ for index := range disks {
+ g.Go(func() error {
+ di := madmin.Disk{
+ Endpoint: endpoints[index].String(),
+ PoolIndex: endpoints[index].PoolIdx,
+ SetIndex: endpoints[index].SetIdx,
+ DiskIndex: endpoints[index].DiskIdx,
+ Local: endpoints[index].IsLocal,
+ }
+ if disks[index] == OfflineDisk {
+ di.State = diskErrToDriveState(errDiskNotFound)
+ disksInfo[index] = di
+ return nil
+ }
+ info, err := disks[index].DiskInfo(context.TODO(), DiskInfoOptions{Metrics: metrics})
+ di.DrivePath = info.MountPath
+ di.TotalSpace = info.Total
+ di.UsedSpace = info.Used
+ di.AvailableSpace = info.Free
+ di.UUID = info.ID
+ di.Major = info.Major
+ di.Minor = info.Minor
+ di.RootDisk = info.RootDisk
+ di.Healing = info.Healing
+ di.Scanning = info.Scanning
+ di.State = diskErrToDriveState(err)
+ di.FreeInodes = info.FreeInodes
+ di.UsedInodes = info.UsedInodes
+ if hi := disks[index].Healing(); hi != nil {
+ hd := hi.toHealingDisk()
+ di.HealInfo = &hd
+ }
+ di.Metrics = &madmin.DiskMetrics{
+ LastMinute: make(map[string]madmin.TimedAction, len(info.Metrics.LastMinute)),
+ APICalls: make(map[string]uint64, len(info.Metrics.APICalls)),
+ TotalErrorsAvailability: info.Metrics.TotalErrorsAvailability,
+ TotalErrorsTimeout: info.Metrics.TotalErrorsTimeout,
+ TotalWaiting: info.Metrics.TotalWaiting,
+ }
+ for k, v := range info.Metrics.LastMinute {
+ if v.N > 0 {
+ di.Metrics.LastMinute[k] = v.asTimedAction()
+ }
+ }
+ maps.Copy(di.Metrics.APICalls, info.Metrics.APICalls)
+ if info.Total > 0 {
+ di.Utilization = float64(info.Used / info.Total * 100)
+ }
+ disksInfo[index] = di
+ return nil
+ }, index)
+ }
+
+ g.Wait()
+ return disksInfo
+}
+
+// Get an aggregated storage info across all disks.
+func getStorageInfo(disks []StorageAPI, endpoints []Endpoint, metrics bool) StorageInfo {
+ disksInfo := getDisksInfo(disks, endpoints, metrics)
+
+ // Sort so that the first element is the smallest.
+ sort.Slice(disksInfo, func(i, j int) bool {
+ return disksInfo[i].TotalSpace < disksInfo[j].TotalSpace
+ })
+
+ storageInfo := StorageInfo{
+ Disks: disksInfo,
+ }
+
+ storageInfo.Backend.Type = madmin.Erasure
+ return storageInfo
+}
+
+// StorageInfo - returns underlying storage statistics.
+func (er erasureObjects) StorageInfo(ctx context.Context) StorageInfo {
+ disks := er.getDisks()
+ endpoints := er.getEndpoints()
+ return getStorageInfo(disks, endpoints, true)
+}
+
+// LocalStorageInfo - returns underlying local storage statistics.
+func (er erasureObjects) LocalStorageInfo(ctx context.Context, metrics bool) StorageInfo {
+ disks := er.getDisks()
+ endpoints := er.getEndpoints()
+
+ var localDisks []StorageAPI
+ var localEndpoints []Endpoint
+
+ for i, endpoint := range endpoints {
+ if endpoint.IsLocal {
+ localDisks = append(localDisks, disks[i])
+ localEndpoints = append(localEndpoints, endpoint)
+ }
+ }
+
+ return getStorageInfo(localDisks, localEndpoints, metrics)
+}
+
+// getOnlineDisksWithHealingAndInfo - returns online disks and overall healing status.
+// Disks are ordered in the following groups:
+// - Non-scanning disks
+// - Non-healing disks
+// - Healing disks (if inclHealing is true)
+func (er erasureObjects) getOnlineDisksWithHealingAndInfo(inclHealing bool) (newDisks []StorageAPI, newInfos []DiskInfo, healing int) {
+ var wg sync.WaitGroup
+ disks := er.getDisks()
+ infos := make([]DiskInfo, len(disks))
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+ for _, i := range r.Perm(len(disks)) {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+
+ disk := disks[i]
+ if disk == nil {
+ infos[i].Error = errDiskNotFound.Error()
+ return
+ }
+
+ di, err := disk.DiskInfo(context.Background(), DiskInfoOptions{})
+ infos[i] = di
+ if err != nil {
+ // - Do not consume disks which are not reachable
+ // unformatted or simply not accessible for some reason.
+ infos[i].Error = err.Error()
+ }
+ }()
+ }
+ wg.Wait()
+
+ var scanningDisks, healingDisks []StorageAPI
+ var scanningInfos, healingInfos []DiskInfo
+
+ for i, info := range infos {
+ // Check if one of the drives in the set is being healed.
+ // this information is used by scanner to skip healing
+ // this erasure set while it calculates the usage.
+ if info.Error != "" || disks[i] == nil {
+ continue
+ }
+ if info.Healing {
+ healing++
+ if inclHealing {
+ healingDisks = append(healingDisks, disks[i])
+ healingInfos = append(healingInfos, infos[i])
+ }
+ continue
+ }
+
+ if !info.Scanning {
+ newDisks = append(newDisks, disks[i])
+ newInfos = append(newInfos, infos[i])
+ } else {
+ scanningDisks = append(scanningDisks, disks[i])
+ scanningInfos = append(scanningInfos, infos[i])
+ }
+ }
+
+ // Prefer non-scanning disks over disks which are currently being scanned.
+ newDisks = append(newDisks, scanningDisks...)
+ newInfos = append(newInfos, scanningInfos...)
+
+ /// Then add healing disks.
+ newDisks = append(newDisks, healingDisks...)
+ newInfos = append(newInfos, healingInfos...)
+
+ return newDisks, newInfos, healing
+}
+
+func (er erasureObjects) getOnlineDisksWithHealing(inclHealing bool) ([]StorageAPI, bool) {
+ newDisks, _, healing := er.getOnlineDisksWithHealingAndInfo(inclHealing)
+ return newDisks, healing > 0
+}
+
+// Clean-up previously deleted objects. from .minio.sys/tmp/.trash/
+func (er erasureObjects) cleanupDeletedObjects(ctx context.Context) {
+ var wg sync.WaitGroup
+ for _, disk := range er.getLocalDisks() {
+ if disk == nil {
+ continue
+ }
+ wg.Add(1)
+ go func(disk StorageAPI) {
+ defer wg.Done()
+ drivePath := disk.Endpoint().Path
+ readDirFn(pathJoin(drivePath, minioMetaTmpDeletedBucket), func(ddir string, typ os.FileMode) error {
+ w := xioutil.NewDeadlineWorker(globalDriveConfig.GetMaxTimeout())
+ return w.Run(func() error {
+ wait := deleteCleanupSleeper.Timer(ctx)
+ removeAll(pathJoin(drivePath, minioMetaTmpDeletedBucket, ddir))
+ wait()
+ return nil
+ })
+ })
+ }(disk)
+ }
+ wg.Wait()
+}
+
+// nsScanner will start scanning buckets and send updated totals as they are traversed.
+// Updates are sent on a regular basis and the caller *must* consume them.
+func (er erasureObjects) nsScanner(ctx context.Context, buckets []BucketInfo, wantCycle uint32, updates chan<- dataUsageCache, healScanMode madmin.HealScanMode) error {
+ if len(buckets) == 0 {
+ return nil
+ }
+
+ // Collect disks we can use.
+ disks, healing := er.getOnlineDisksWithHealing(false)
+ if len(disks) == 0 {
+ scannerLogIf(ctx, errors.New("data-scanner: all drives are offline or being healed, skipping scanner cycle"))
+ return nil
+ }
+
+ // Load bucket totals
+ oldCache := dataUsageCache{}
+ if err := oldCache.load(ctx, er, dataUsageCacheName); err != nil {
+ return err
+ }
+
+ // New cache..
+ cache := dataUsageCache{
+ Info: dataUsageCacheInfo{
+ Name: dataUsageRoot,
+ NextCycle: oldCache.Info.NextCycle,
+ },
+ Cache: make(map[string]dataUsageEntry, len(oldCache.Cache)),
+ }
+
+ // Put all buckets into channel.
+ bucketCh := make(chan BucketInfo, len(buckets))
+
+ // Shuffle buckets to ensure total randomness of buckets, being scanned.
+ // Otherwise same set of buckets get scanned across erasure sets always.
+ // at any given point in time. This allows different buckets to be scanned
+ // in different order per erasure set, this wider spread is needed when
+ // there are lots of buckets with different order of objects in them.
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+ permutes := r.Perm(len(buckets))
+ // Add new buckets first
+ for _, idx := range permutes {
+ b := buckets[idx]
+ if e := oldCache.find(b.Name); e == nil {
+ bucketCh <- b
+ }
+ }
+ for _, idx := range permutes {
+ b := buckets[idx]
+ if e := oldCache.find(b.Name); e != nil {
+ cache.replace(b.Name, dataUsageRoot, *e)
+ bucketCh <- b
+ }
+ }
+ xioutil.SafeClose(bucketCh)
+
+ bucketResults := make(chan dataUsageEntryInfo, len(disks))
+
+ // Start async collector/saver.
+ // This goroutine owns the cache.
+ var saverWg sync.WaitGroup
+ saverWg.Add(1)
+ go func() {
+ // Add jitter to the update time so multiple sets don't sync up.
+ updateTime := 30*time.Second + time.Duration(float64(10*time.Second)*rand.Float64())
+ t := time.NewTicker(updateTime)
+ defer t.Stop()
+ defer saverWg.Done()
+ var lastSave time.Time
+
+ for {
+ select {
+ case <-t.C:
+ if cache.Info.LastUpdate.Equal(lastSave) {
+ continue
+ }
+ scannerLogOnceIf(ctx, cache.save(ctx, er, dataUsageCacheName), "nsscanner-cache-update")
+ updates <- cache.clone()
+
+ lastSave = cache.Info.LastUpdate
+ case v, ok := <-bucketResults:
+ if !ok {
+ // Save final state...
+ cache.Info.NextCycle = wantCycle
+ cache.Info.LastUpdate = time.Now()
+ scannerLogOnceIf(ctx, cache.save(ctx, er, dataUsageCacheName), "nsscanner-channel-closed")
+ updates <- cache.clone()
+ return
+ }
+ cache.replace(v.Name, v.Parent, v.Entry)
+ cache.Info.LastUpdate = time.Now()
+ }
+ }
+ }()
+
+ // Restrict parallelism for disk usage scanner
+ // upto GOMAXPROCS if GOMAXPROCS is < len(disks)
+ maxProcs := runtime.GOMAXPROCS(0)
+ if maxProcs < len(disks) {
+ disks = disks[:maxProcs]
+ }
+
+ // Start one scanner per disk
+ var wg sync.WaitGroup
+ wg.Add(len(disks))
+
+ for i := range disks {
+ go func(i int) {
+ defer wg.Done()
+ disk := disks[i]
+
+ for bucket := range bucketCh {
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ }
+
+ // Load cache for bucket
+ cacheName := pathJoin(bucket.Name, dataUsageCacheName)
+ cache := dataUsageCache{}
+ scannerLogIf(ctx, cache.load(ctx, er, cacheName))
+ if cache.Info.Name == "" {
+ cache.Info.Name = bucket.Name
+ }
+ cache.Info.SkipHealing = healing
+ cache.Info.NextCycle = wantCycle
+ if cache.Info.Name != bucket.Name {
+ cache.Info = dataUsageCacheInfo{
+ Name: bucket.Name,
+ LastUpdate: time.Time{},
+ NextCycle: wantCycle,
+ }
+ }
+ // Collect updates.
+ updates := make(chan dataUsageEntry, 1)
+ var wg sync.WaitGroup
+ wg.Add(1)
+ go func(name string) {
+ defer wg.Done()
+ for update := range updates {
+ select {
+ case <-ctx.Done():
+ case bucketResults <- dataUsageEntryInfo{
+ Name: name,
+ Parent: dataUsageRoot,
+ Entry: update,
+ }:
+ }
+ }
+ }(cache.Info.Name)
+ // Calc usage
+ before := cache.Info.LastUpdate
+ var err error
+ cache, err = disk.NSScanner(ctx, cache, updates, healScanMode, nil)
+ if err != nil {
+ if !cache.Info.LastUpdate.IsZero() && cache.Info.LastUpdate.After(before) {
+ scannerLogIf(ctx, cache.save(ctx, er, cacheName))
+ } else {
+ scannerLogIf(ctx, err)
+ }
+ // This ensures that we don't close
+ // bucketResults channel while the
+ // updates-collector goroutine still
+ // holds a reference to this.
+ wg.Wait()
+ continue
+ }
+
+ wg.Wait()
+ // Flatten for upstream, but save full state.
+ var root dataUsageEntry
+ if r := cache.root(); r != nil {
+ root = cache.flatten(*r)
+ }
+ select {
+ case <-ctx.Done():
+ return
+ case bucketResults <- dataUsageEntryInfo{
+ Name: cache.Info.Name,
+ Parent: dataUsageRoot,
+ Entry: root,
+ }:
+ }
+
+ // Save cache
+ scannerLogIf(ctx, cache.save(ctx, er, cacheName))
+ }
+ }(i)
+ }
+ wg.Wait()
+ xioutil.SafeClose(bucketResults)
+ saverWg.Wait()
+
+ return nil
+}
diff --git a/cmd/erasure_test.go b/cmd/erasure_test.go
new file mode 100644
index 00000000..078771fd
--- /dev/null
+++ b/cmd/erasure_test.go
@@ -0,0 +1,135 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "crypto/rand"
+ "io"
+ "testing"
+)
+
+var erasureEncodeDecodeTests = []struct {
+ dataBlocks, parityBlocks int
+ missingData, missingParity int
+ reconstructParity bool
+ shouldFail bool
+}{
+ {dataBlocks: 2, parityBlocks: 2, missingData: 0, missingParity: 0, reconstructParity: true, shouldFail: false},
+ {dataBlocks: 3, parityBlocks: 3, missingData: 1, missingParity: 0, reconstructParity: true, shouldFail: false},
+ {dataBlocks: 4, parityBlocks: 4, missingData: 2, missingParity: 0, reconstructParity: false, shouldFail: false},
+ {dataBlocks: 5, parityBlocks: 5, missingData: 0, missingParity: 1, reconstructParity: true, shouldFail: false},
+ {dataBlocks: 6, parityBlocks: 6, missingData: 0, missingParity: 2, reconstructParity: true, shouldFail: false},
+ {dataBlocks: 7, parityBlocks: 7, missingData: 1, missingParity: 1, reconstructParity: false, shouldFail: false},
+ {dataBlocks: 8, parityBlocks: 8, missingData: 3, missingParity: 2, reconstructParity: false, shouldFail: false},
+ {dataBlocks: 2, parityBlocks: 2, missingData: 2, missingParity: 1, reconstructParity: true, shouldFail: true},
+ {dataBlocks: 4, parityBlocks: 2, missingData: 2, missingParity: 2, reconstructParity: false, shouldFail: true},
+ {dataBlocks: 8, parityBlocks: 4, missingData: 2, missingParity: 2, reconstructParity: false, shouldFail: false},
+}
+
+func TestErasureEncodeDecode(t *testing.T) {
+ data := make([]byte, 256)
+ if _, err := io.ReadFull(rand.Reader, data); err != nil {
+ t.Fatalf("Failed to read random data: %v", err)
+ }
+ for i, test := range erasureEncodeDecodeTests {
+ buffer := make([]byte, len(data), 2*len(data))
+ copy(buffer, data)
+
+ erasure, err := NewErasure(t.Context(), test.dataBlocks, test.parityBlocks, blockSizeV2)
+ if err != nil {
+ t.Fatalf("Test %d: failed to create erasure: %v", i, err)
+ }
+ encoded, err := erasure.EncodeData(t.Context(), buffer)
+ if err != nil {
+ t.Fatalf("Test %d: failed to encode data: %v", i, err)
+ }
+
+ for j := range encoded[:test.missingData] {
+ encoded[j] = nil
+ }
+ for j := test.dataBlocks; j < test.dataBlocks+test.missingParity; j++ {
+ encoded[j] = nil
+ }
+
+ if test.reconstructParity {
+ err = erasure.DecodeDataAndParityBlocks(t.Context(), encoded)
+ } else {
+ err = erasure.DecodeDataBlocks(encoded)
+ }
+
+ if err == nil && test.shouldFail {
+ t.Errorf("Test %d: test should fail but it passed", i)
+ }
+ if err != nil && !test.shouldFail {
+ t.Errorf("Test %d: test should pass but it failed: %v", i, err)
+ }
+
+ decoded := encoded
+ if !test.shouldFail {
+ if test.reconstructParity {
+ for j := range decoded {
+ if decoded[j] == nil {
+ t.Errorf("Test %d: failed to reconstruct shard %d", i, j)
+ }
+ }
+ } else {
+ for j := range decoded[:test.dataBlocks] {
+ if decoded[j] == nil {
+ t.Errorf("Test %d: failed to reconstruct data shard %d", i, j)
+ }
+ }
+ }
+
+ decodedData := new(bytes.Buffer)
+ if _, err = writeDataBlocks(t.Context(), decodedData, decoded, test.dataBlocks, 0, int64(len(data))); err != nil {
+ t.Errorf("Test %d: failed to write data blocks: %v", i, err)
+ }
+ if !bytes.Equal(decodedData.Bytes(), data) {
+ t.Errorf("Test %d: Decoded data does not match original data: got: %v want: %v", i, decodedData.Bytes(), data)
+ }
+ }
+ }
+}
+
+// Setup for erasureCreateFile and erasureReadFile tests.
+type erasureTestSetup struct {
+ dataBlocks int
+ parityBlocks int
+ blockSize int64
+ diskPaths []string
+ disks []StorageAPI
+}
+
+// Returns an initialized setup for erasure tests.
+func newErasureTestSetup(tb testing.TB, dataBlocks int, parityBlocks int, blockSize int64) (*erasureTestSetup, error) {
+ diskPaths := make([]string, dataBlocks+parityBlocks)
+ disks := make([]StorageAPI, len(diskPaths))
+ var err error
+ for i := range diskPaths {
+ disks[i], diskPaths[i], err = newXLStorageTestSetup(tb)
+ if err != nil {
+ return nil, err
+ }
+ err = disks[i].MakeVol(tb.Context(), "testbucket")
+ if err != nil {
+ return nil, err
+ }
+ }
+ return &erasureTestSetup{dataBlocks, parityBlocks, blockSize, diskPaths, disks}, nil
+}
diff --git a/cmd/etcd.go b/cmd/etcd.go
new file mode 100644
index 00000000..e3cdebb7
--- /dev/null
+++ b/cmd/etcd.go
@@ -0,0 +1,91 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ etcd "go.etcd.io/etcd/client/v3"
+)
+
+var errEtcdUnreachable = errors.New("etcd is unreachable, please check your endpoints")
+
+func etcdErrToErr(err error, etcdEndpoints []string) error {
+ if err == nil {
+ return nil
+ }
+ switch err {
+ case context.DeadlineExceeded:
+ return fmt.Errorf("%w %s", errEtcdUnreachable, etcdEndpoints)
+ default:
+ return fmt.Errorf("unexpected error %w from etcd, please check your endpoints %s", err, etcdEndpoints)
+ }
+}
+
+func saveKeyEtcdWithTTL(ctx context.Context, client *etcd.Client, key string, data []byte, ttl int64) error {
+ timeoutCtx, cancel := context.WithTimeout(ctx, defaultContextTimeout)
+ defer cancel()
+ lease, err := client.Grant(timeoutCtx, ttl)
+ if err != nil {
+ return etcdErrToErr(err, client.Endpoints())
+ }
+ _, err = client.Put(timeoutCtx, key, string(data), etcd.WithLease(lease.ID))
+ etcdLogIf(ctx, err)
+ return etcdErrToErr(err, client.Endpoints())
+}
+
+func saveKeyEtcd(ctx context.Context, client *etcd.Client, key string, data []byte, opts ...options) error {
+ timeoutCtx, cancel := context.WithTimeout(ctx, defaultContextTimeout)
+ defer cancel()
+ if len(opts) > 0 {
+ return saveKeyEtcdWithTTL(ctx, client, key, data, opts[0].ttl)
+ }
+ _, err := client.Put(timeoutCtx, key, string(data))
+ etcdLogIf(ctx, err)
+ return etcdErrToErr(err, client.Endpoints())
+}
+
+func deleteKeyEtcd(ctx context.Context, client *etcd.Client, key string) error {
+ timeoutCtx, cancel := context.WithTimeout(ctx, defaultContextTimeout)
+ defer cancel()
+
+ _, err := client.Delete(timeoutCtx, key)
+ etcdLogIf(ctx, err)
+ return etcdErrToErr(err, client.Endpoints())
+}
+
+func readKeyEtcd(ctx context.Context, client *etcd.Client, key string) ([]byte, error) {
+ timeoutCtx, cancel := context.WithTimeout(ctx, defaultContextTimeout)
+ defer cancel()
+ resp, err := client.Get(timeoutCtx, key)
+ if err != nil {
+ etcdLogOnceIf(ctx, err, "etcd-retrieve-keys")
+ return nil, etcdErrToErr(err, client.Endpoints())
+ }
+ if resp.Count == 0 {
+ return nil, errConfigNotFound
+ }
+ for _, ev := range resp.Kvs {
+ if string(ev.Key) == key {
+ return ev.Value, nil
+ }
+ }
+ return nil, errConfigNotFound
+}
diff --git a/cmd/event-notification.go b/cmd/event-notification.go
new file mode 100644
index 00000000..ceda47ef
--- /dev/null
+++ b/cmd/event-notification.go
@@ -0,0 +1,260 @@
+// Copyright (c) 2015-2022 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "net/url"
+ "runtime"
+ "strings"
+ "sync"
+
+ "github.com/minio/minio/internal/crypto"
+ "github.com/minio/minio/internal/event"
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/minio/internal/pubsub"
+ "github.com/minio/pkg/v3/policy"
+)
+
+// EventNotifier - notifies external systems about events in MinIO.
+type EventNotifier struct {
+ sync.RWMutex
+ targetList *event.TargetList
+ bucketRulesMap map[string]event.RulesMap
+}
+
+// NewEventNotifier - creates new event notification object.
+func NewEventNotifier(ctx context.Context) *EventNotifier {
+ // targetList/bucketRulesMap/bucketRemoteTargetRulesMap are populated by NotificationSys.InitBucketTargets()
+ return &EventNotifier{
+ targetList: event.NewTargetList(ctx),
+ bucketRulesMap: make(map[string]event.RulesMap),
+ }
+}
+
+// GetARNList - returns available ARNs.
+func (evnot *EventNotifier) GetARNList() []string {
+ arns := []string{}
+ if evnot == nil {
+ return arns
+ }
+ region := globalSite.Region()
+ for targetID := range evnot.targetList.TargetMap() {
+ // httpclient target is part of ListenNotification
+ // which doesn't need to be listed as part of the ARN list
+ // This list is only meant for external targets, filter
+ // this out pro-actively.
+ if !strings.HasPrefix(targetID.ID, "httpclient+") {
+ arns = append(arns, targetID.ToARN(region).String())
+ }
+ }
+
+ return arns
+}
+
+// Loads notification policies for all buckets into EventNotifier.
+func (evnot *EventNotifier) set(bucket string, meta BucketMetadata) {
+ config := meta.notificationConfig
+ if config == nil {
+ return
+ }
+ region := globalSite.Region()
+ config.SetRegion(region)
+ if err := config.Validate(region, globalEventNotifier.targetList); err != nil {
+ if _, ok := err.(*event.ErrARNNotFound); !ok {
+ internalLogIf(GlobalContext, err)
+ }
+ }
+ evnot.AddRulesMap(bucket, config.ToRulesMap())
+}
+
+// Targets returns all the registered targets
+func (evnot *EventNotifier) Targets() []event.Target {
+ return evnot.targetList.Targets()
+}
+
+// InitBucketTargets - initializes event notification system from notification.xml of all buckets.
+func (evnot *EventNotifier) InitBucketTargets(ctx context.Context, objAPI ObjectLayer) error {
+ if objAPI == nil {
+ return errServerNotInitialized
+ }
+
+ if err := evnot.targetList.Add(globalNotifyTargetList.Targets()...); err != nil {
+ return err
+ }
+ evnot.targetList = evnot.targetList.Init(runtime.GOMAXPROCS(0)) // TODO: make this configurable (y4m4)
+ return nil
+}
+
+// AddRulesMap - adds rules map for bucket name.
+func (evnot *EventNotifier) AddRulesMap(bucketName string, rulesMap event.RulesMap) {
+ evnot.Lock()
+ defer evnot.Unlock()
+
+ rulesMap = rulesMap.Clone()
+
+ // Do not add for an empty rulesMap.
+ if len(rulesMap) == 0 {
+ delete(evnot.bucketRulesMap, bucketName)
+ } else {
+ evnot.bucketRulesMap[bucketName] = rulesMap
+ }
+}
+
+// RemoveNotification - removes all notification configuration for bucket name.
+func (evnot *EventNotifier) RemoveNotification(bucketName string) {
+ evnot.Lock()
+ defer evnot.Unlock()
+
+ delete(evnot.bucketRulesMap, bucketName)
+}
+
+// RemoveAllBucketTargets - closes and removes all notification targets.
+func (evnot *EventNotifier) RemoveAllBucketTargets() {
+ evnot.Lock()
+ defer evnot.Unlock()
+
+ targetIDSet := event.NewTargetIDSet()
+ for k := range evnot.targetList.TargetMap() {
+ targetIDSet[k] = struct{}{}
+ }
+ evnot.targetList.Remove(targetIDSet)
+}
+
+// Send - sends the event to all registered notification targets
+func (evnot *EventNotifier) Send(args eventArgs) {
+ evnot.RLock()
+ targetIDSet := evnot.bucketRulesMap[args.BucketName].Match(args.EventName, args.Object.Name)
+ evnot.RUnlock()
+
+ if len(targetIDSet) == 0 {
+ return
+ }
+
+ // If MINIO_API_SYNC_EVENTS is set, send events synchronously.
+ evnot.targetList.Send(args.ToEvent(true), targetIDSet, globalAPIConfig.isSyncEventsEnabled())
+}
+
+type eventArgs struct {
+ EventName event.Name
+ BucketName string
+ Object ObjectInfo
+ ReqParams map[string]string
+ RespElements map[string]string
+ Host string
+ UserAgent string
+}
+
+// ToEvent - converts to notification event.
+func (args eventArgs) ToEvent(escape bool) event.Event {
+ eventTime := UTCNow()
+ uniqueID := fmt.Sprintf("%X", eventTime.UnixNano())
+ if !args.Object.ModTime.IsZero() {
+ uniqueID = fmt.Sprintf("%X", args.Object.ModTime.UnixNano())
+ }
+
+ respElements := map[string]string{
+ "x-amz-request-id": args.RespElements["requestId"],
+ "x-amz-id-2": args.RespElements["nodeId"],
+ "x-minio-origin-endpoint": func() string {
+ if globalMinioEndpoint != "" {
+ return globalMinioEndpoint
+ }
+ return getAPIEndpoints()[0]
+ }(), // MinIO specific custom elements.
+ }
+
+ // Add deployment as part of response elements.
+ respElements["x-minio-deployment-id"] = globalDeploymentID()
+ if args.RespElements["content-length"] != "" {
+ respElements["content-length"] = args.RespElements["content-length"]
+ }
+
+ keyName := args.Object.Name
+ if escape {
+ keyName = url.QueryEscape(args.Object.Name)
+ }
+
+ newEvent := event.Event{
+ EventVersion: "2.0",
+ EventSource: "minio:s3",
+ AwsRegion: args.ReqParams["region"],
+ EventTime: eventTime.Format(event.AMZTimeFormat),
+ EventName: args.EventName,
+ UserIdentity: event.Identity{PrincipalID: args.ReqParams["principalId"]},
+ RequestParameters: args.ReqParams,
+ ResponseElements: respElements,
+ S3: event.Metadata{
+ SchemaVersion: "1.0",
+ ConfigurationID: "Config",
+ Bucket: event.Bucket{
+ Name: args.BucketName,
+ OwnerIdentity: event.Identity{PrincipalID: args.ReqParams["principalId"]},
+ ARN: policy.ResourceARNPrefix + args.BucketName,
+ },
+ Object: event.Object{
+ Key: keyName,
+ VersionID: args.Object.VersionID,
+ Sequencer: uniqueID,
+ },
+ },
+ Source: event.Source{
+ Host: args.Host,
+ UserAgent: args.UserAgent,
+ },
+ }
+
+ isRemovedEvent := args.EventName == event.ObjectRemovedDelete ||
+ args.EventName == event.ObjectRemovedDeleteMarkerCreated ||
+ args.EventName == event.ObjectRemovedNoOP
+
+ if !isRemovedEvent {
+ newEvent.S3.Object.ETag = args.Object.ETag
+ newEvent.S3.Object.Size = args.Object.Size
+ newEvent.S3.Object.ContentType = args.Object.ContentType
+ newEvent.S3.Object.UserMetadata = make(map[string]string, len(args.Object.UserDefined))
+ for k, v := range args.Object.UserDefined {
+ if stringsHasPrefixFold(strings.ToLower(k), ReservedMetadataPrefixLower) {
+ continue
+ }
+ newEvent.S3.Object.UserMetadata[k] = v
+ }
+ }
+
+ return newEvent
+}
+
+func sendEvent(args eventArgs) {
+ // avoid generating a notification for REPLICA creation event.
+ if _, ok := args.ReqParams[xhttp.MinIOSourceReplicationRequest]; ok {
+ return
+ }
+
+ args.Object.Size, _ = args.Object.GetActualSize()
+
+ // remove sensitive encryption entries in metadata.
+ crypto.RemoveSensitiveEntries(args.Object.UserDefined)
+ crypto.RemoveInternalEntries(args.Object.UserDefined)
+
+ if globalHTTPListen.NumSubscribers(pubsub.MaskFromMaskable(args.EventName)) > 0 {
+ globalHTTPListen.Publish(args.ToEvent(false))
+ }
+
+ globalEventNotifier.Send(args)
+}
diff --git a/cmd/fmt-gen.go b/cmd/fmt-gen.go
new file mode 100644
index 00000000..c616c4dc
--- /dev/null
+++ b/cmd/fmt-gen.go
@@ -0,0 +1,119 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "encoding/json"
+ "log"
+ "os"
+ "path/filepath"
+
+ "github.com/klauspost/compress/zip"
+ "github.com/minio/cli"
+)
+
+var fmtGenFlags = []cli.Flag{
+ cli.IntFlag{
+ Name: "parity",
+ Usage: "specify erasure code parity",
+ },
+ cli.StringFlag{
+ Name: "deployment-id",
+ Usage: "deployment-id of the MinIO cluster for which format.json is needed",
+ },
+ cli.StringFlag{
+ Name: "address",
+ Value: ":" + GlobalMinioDefaultPort,
+ Usage: "bind to a specific ADDRESS:PORT, ADDRESS can be an IP or hostname",
+ EnvVar: "MINIO_ADDRESS",
+ },
+}
+
+var fmtGenCmd = cli.Command{
+ Name: "fmt-gen",
+ Usage: "Generate format.json files for an erasure server pool",
+ Flags: append(fmtGenFlags, GlobalFlags...),
+ Action: fmtGenMain,
+ Hidden: true,
+ CustomHelpTemplate: `NAME:
+ {{.HelpName}} - {{.Usage}}
+
+USAGE:
+ {{.HelpName}} {{if .VisibleFlags}}[FLAGS] {{end}}DIR1 [DIR2..]
+ {{.HelpName}} {{if .VisibleFlags}}[FLAGS] {{end}}DIR{1...64}
+ {{.HelpName}} {{if .VisibleFlags}}[FLAGS] {{end}}DIR{1...64} DIR{65...128}
+
+DIR:
+ DIR points to a directory on a filesystem. When you want to combine
+ multiple drives into a single large system, pass one directory per
+ filesystem separated by space. You may also use a '...' convention
+ to abbreviate the directory arguments. Remote directories in a
+ distributed setup are encoded as HTTP(s) URIs.
+{{if .VisibleFlags}}
+FLAGS:
+ {{range .VisibleFlags}}{{.}}
+ {{end}}{{end}}
+EXAMPLES:
+ 1. Generate format.json.zip containing format.json files for all drives in a distributed MinIO server pool of 32 nodes with 32 drives each.
+ {{.Prompt}} {{.HelpName}} http://node{1...32}.example.com/mnt/export{1...32}
+
+`,
+}
+
+func fmtGenMain(ctxt *cli.Context) {
+ deploymentID := ctxt.String("deployment-id")
+ err := buildServerCtxt(ctxt, &globalServerCtxt)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ handleCommonArgs(globalServerCtxt)
+ pools, _, err := createServerEndpoints(globalMinioAddr, globalServerCtxt.Layout.pools, globalServerCtxt.Layout.legacy)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ zipFile, err := os.Create("format.json.zip")
+ if err != nil {
+ log.Fatalf("failed to create format.json.zip: %v", err)
+ }
+ defer zipFile.Close()
+ fmtZipW := zip.NewWriter(zipFile)
+ defer fmtZipW.Close()
+ for _, pool := range pools { // for each pool
+ setCount, setDriveCount := pool.SetCount, pool.DrivesPerSet
+ format := newFormatErasureV3(setCount, setDriveCount)
+ format.ID = deploymentID
+ for i := range setCount { // for each erasure set
+ for j := range setDriveCount {
+ newFormat := format.Clone()
+ newFormat.Erasure.This = format.Erasure.Sets[i][j]
+ if deploymentID != "" {
+ newFormat.ID = deploymentID
+ }
+ drive := pool.Endpoints[i*setDriveCount+j]
+ fmtBytes, err := json.Marshal(newFormat)
+ if err != nil {
+ //nolint:gocritic
+ log.Fatalf("failed to marshal format.json for %s: %v", drive.String(), err)
+ }
+ fmtJSON := filepath.Join(drive.Host, drive.Path, minioMetaBucket, "format.json")
+ embedFileInZip(fmtZipW, fmtJSON, fmtBytes, 0o600)
+ }
+ }
+ }
+}
diff --git a/cmd/format-erasure.go b/cmd/format-erasure.go
new file mode 100644
index 00000000..6ff6d9e9
--- /dev/null
+++ b/cmd/format-erasure.go
@@ -0,0 +1,720 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/fs"
+ "os"
+ "sync"
+
+ "github.com/dustin/go-humanize"
+ "github.com/minio/minio/internal/color"
+ "github.com/minio/minio/internal/config"
+ "github.com/minio/minio/internal/config/storageclass"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/sync/errgroup"
+)
+
+const (
+ // Represents Erasure backend.
+ formatBackendErasure = "xl"
+
+ // Represents Erasure backend - single drive
+ formatBackendErasureSingle = "xl-single"
+
+ // formatErasureV1.Erasure.Version - version '1'.
+ formatErasureVersionV1 = "1"
+
+ // formatErasureV2.Erasure.Version - version '2'.
+ formatErasureVersionV2 = "2"
+
+ // formatErasureV3.Erasure.Version - version '3'.
+ formatErasureVersionV3 = "3"
+
+ // Distribution algorithm used, legacy
+ formatErasureVersionV2DistributionAlgoV1 = "CRCMOD"
+
+ // Distributed algorithm used, with N/2 default parity
+ formatErasureVersionV3DistributionAlgoV2 = "SIPMOD"
+
+ // Distributed algorithm used, with EC:4 default parity
+ formatErasureVersionV3DistributionAlgoV3 = "SIPMOD+PARITY"
+)
+
+// Offline disk UUID represents an offline disk.
+const offlineDiskUUID = "ffffffff-ffff-ffff-ffff-ffffffffffff"
+
+// Used to detect the version of "xl" format.
+type formatErasureVersionDetect struct {
+ Erasure struct {
+ Version string `json:"version"`
+ } `json:"xl"`
+}
+
+// Represents the V1 backend disk structure version
+// under `.minio.sys` and actual data namespace.
+// formatErasureV1 - structure holds format config version '1'.
+type formatErasureV1 struct {
+ formatMetaV1
+ Erasure struct {
+ Version string `json:"version"` // Version of 'xl' format.
+ Disk string `json:"drive"` // Disk field carries assigned disk uuid.
+ // JBOD field carries the input disk order generated the first
+ // time when fresh disks were supplied.
+ JBOD []string `json:"jbod"`
+ } `json:"xl"` // Erasure field holds xl format.
+}
+
+// Represents the V2 backend disk structure version
+// under `.minio.sys` and actual data namespace.
+// formatErasureV2 - structure holds format config version '2'.
+// The V2 format to support "large bucket" support where a bucket
+// can span multiple erasure sets.
+type formatErasureV2 struct {
+ formatMetaV1
+ Erasure struct {
+ Version string `json:"version"` // Version of 'xl' format.
+ This string `json:"this"` // This field carries assigned disk uuid.
+ // Sets field carries the input disk order generated the first
+ // time when fresh disks were supplied, it is a two dimensional
+ // array second dimension represents list of disks used per set.
+ Sets [][]string `json:"sets"`
+ // Distribution algorithm represents the hashing algorithm
+ // to pick the right set index for an object.
+ DistributionAlgo string `json:"distributionAlgo"`
+ } `json:"xl"`
+}
+
+// formatErasureV3 struct is same as formatErasureV2 struct except that formatErasureV3.Erasure.Version is "3" indicating
+// the simplified multipart backend which is a flat hierarchy now.
+// In .minio.sys/multipart we have:
+// sha256(bucket/object)/uploadID/[xl.meta, part.1, part.2 ....]
+type formatErasureV3 struct {
+ formatMetaV1
+ Erasure struct {
+ Version string `json:"version"` // Version of 'xl' format.
+ This string `json:"this"` // This field carries assigned disk uuid.
+ // Sets field carries the input disk order generated the first
+ // time when fresh disks were supplied, it is a two dimensional
+ // array second dimension represents list of disks used per set.
+ Sets [][]string `json:"sets"`
+ // Distribution algorithm represents the hashing algorithm
+ // to pick the right set index for an object.
+ DistributionAlgo string `json:"distributionAlgo"`
+ } `json:"xl"`
+ Info DiskInfo `json:"-"`
+}
+
+func (f *formatErasureV3) Drives() (drives int) {
+ for _, set := range f.Erasure.Sets {
+ drives += len(set)
+ }
+ return drives
+}
+
+func (f *formatErasureV3) Clone() *formatErasureV3 {
+ b, err := json.Marshal(f)
+ if err != nil {
+ panic(err)
+ }
+ var dst formatErasureV3
+ if err = json.Unmarshal(b, &dst); err != nil {
+ panic(err)
+ }
+ return &dst
+}
+
+// Returns formatErasure.Erasure.Version
+func newFormatErasureV3(numSets int, setLen int) *formatErasureV3 {
+ format := &formatErasureV3{}
+ format.Version = formatMetaVersionV1
+ format.Format = formatBackendErasure
+ if setLen == 1 {
+ format.Format = formatBackendErasureSingle
+ }
+ format.ID = mustGetUUID()
+ format.Erasure.Version = formatErasureVersionV3
+ format.Erasure.DistributionAlgo = formatErasureVersionV3DistributionAlgoV3
+ format.Erasure.Sets = make([][]string, numSets)
+
+ for i := range numSets {
+ format.Erasure.Sets[i] = make([]string, setLen)
+ for j := range setLen {
+ format.Erasure.Sets[i][j] = mustGetUUID()
+ }
+ }
+ return format
+}
+
+// Returns format Erasure version after reading `format.json`, returns
+// successfully the version only if the backend is Erasure.
+func formatGetBackendErasureVersion(b []byte) (string, error) {
+ meta := &formatMetaV1{}
+ if err := json.Unmarshal(b, meta); err != nil {
+ return "", err
+ }
+ if meta.Version != formatMetaVersionV1 {
+ return "", fmt.Errorf(`format.Version expected: %s, got: %s`, formatMetaVersionV1, meta.Version)
+ }
+ if meta.Format != formatBackendErasure && meta.Format != formatBackendErasureSingle {
+ return "", fmt.Errorf(`found backend type %s, expected %s or %s - to migrate to a supported backend visit https://docs.min.io/community/minio-object-store/operations/deployments/baremetal-migrate-fs-gateway.html`, meta.Format, formatBackendErasure, formatBackendErasureSingle)
+ }
+ // Erasure backend found, proceed to detect version.
+ format := &formatErasureVersionDetect{}
+ if err := json.Unmarshal(b, format); err != nil {
+ return "", err
+ }
+ return format.Erasure.Version, nil
+}
+
+// Migrates all previous versions to latest version of `format.json`,
+// this code calls migration in sequence, such as V1 is migrated to V2
+// first before it V2 migrates to V3.n
+func formatErasureMigrate(export string) ([]byte, fs.FileInfo, error) {
+ formatPath := pathJoin(export, minioMetaBucket, formatConfigFile)
+ formatData, formatFi, err := xioutil.ReadFileWithFileInfo(formatPath)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ version, err := formatGetBackendErasureVersion(formatData)
+ if err != nil {
+ return nil, nil, fmt.Errorf("Drive %s: %w", export, err)
+ }
+
+ migrate := func(formatPath string, formatData []byte) ([]byte, fs.FileInfo, error) {
+ if err = os.WriteFile(formatPath, formatData, 0o666); err != nil {
+ return nil, nil, err
+ }
+ formatFi, err := Lstat(formatPath)
+ if err != nil {
+ return nil, nil, err
+ }
+ return formatData, formatFi, nil
+ }
+
+ switch version {
+ case formatErasureVersionV1:
+ formatData, err = formatErasureMigrateV1ToV2(formatData, version)
+ if err != nil {
+ return nil, nil, fmt.Errorf("Drive %s: %w", export, err)
+ }
+ // Migrate successful v1 => v2, proceed to v2 => v3
+ version = formatErasureVersionV2
+ fallthrough
+ case formatErasureVersionV2:
+ formatData, err = formatErasureMigrateV2ToV3(formatData, export, version)
+ if err != nil {
+ return nil, nil, fmt.Errorf("Drive %s: %w", export, err)
+ }
+ // Migrate successful v2 => v3, v3 is latest
+ // version = formatXLVersionV3
+ return migrate(formatPath, formatData)
+ case formatErasureVersionV3:
+ // v3 is the latest version, return.
+ return formatData, formatFi, nil
+ }
+ return nil, nil, fmt.Errorf(`Disk %s: unknown format version %s`, export, version)
+}
+
+// Migrates version V1 of format.json to version V2 of format.json,
+// migration fails upon any error.
+func formatErasureMigrateV1ToV2(data []byte, version string) ([]byte, error) {
+ if version != formatErasureVersionV1 {
+ return nil, fmt.Errorf(`format version expected %s, found %s`, formatErasureVersionV1, version)
+ }
+
+ formatV1 := &formatErasureV1{}
+ if err := json.Unmarshal(data, formatV1); err != nil {
+ return nil, err
+ }
+
+ formatV2 := &formatErasureV2{}
+ formatV2.Version = formatMetaVersionV1
+ formatV2.Format = formatBackendErasure
+ formatV2.Erasure.Version = formatErasureVersionV2
+ formatV2.Erasure.DistributionAlgo = formatErasureVersionV2DistributionAlgoV1
+ formatV2.Erasure.This = formatV1.Erasure.Disk
+ formatV2.Erasure.Sets = make([][]string, 1)
+ formatV2.Erasure.Sets[0] = make([]string, len(formatV1.Erasure.JBOD))
+ copy(formatV2.Erasure.Sets[0], formatV1.Erasure.JBOD)
+
+ return json.Marshal(formatV2)
+}
+
+// Migrates V2 for format.json to V3 (Flat hierarchy for multipart)
+func formatErasureMigrateV2ToV3(data []byte, export, version string) ([]byte, error) {
+ if version != formatErasureVersionV2 {
+ return nil, fmt.Errorf(`format version expected %s, found %s`, formatErasureVersionV2, version)
+ }
+
+ formatV2 := &formatErasureV2{}
+ if err := json.Unmarshal(data, formatV2); err != nil {
+ return nil, err
+ }
+
+ tmpOld := pathJoin(export, minioMetaTmpDeletedBucket, mustGetUUID())
+ if err := renameAll(pathJoin(export, minioMetaMultipartBucket),
+ tmpOld, export); err != nil && err != errFileNotFound {
+ bootLogIf(GlobalContext, fmt.Errorf("unable to rename (%s -> %s) %w, drive may be faulty please investigate",
+ pathJoin(export, minioMetaMultipartBucket),
+ tmpOld,
+ osErrToFileErr(err)))
+ }
+
+ // format-V2 struct is exactly same as format-V1 except that version is "3"
+ // which indicates the simplified multipart backend.
+ formatV3 := formatErasureV3{}
+ formatV3.Version = formatV2.Version
+ formatV3.Format = formatV2.Format
+ formatV3.Erasure = formatV2.Erasure
+ formatV3.Erasure.Version = formatErasureVersionV3
+
+ return json.Marshal(formatV3)
+}
+
+// countErrs - count a specific error.
+func countErrs(errs []error, err error) int {
+ i := 0
+ for _, err1 := range errs {
+ if err1 == err || errors.Is(err1, err) {
+ i++
+ }
+ }
+ return i
+}
+
+// Does all errors indicate we need to initialize all disks?.
+func shouldInitErasureDisks(errs []error) bool {
+ return countErrs(errs, errUnformattedDisk) == len(errs)
+}
+
+// Check if unformatted disks are equal to 50%+1 of all the drives.
+func quorumUnformattedDisks(errs []error) bool {
+ return countErrs(errs, errUnformattedDisk) >= (len(errs)/2)+1
+}
+
+// loadFormatErasureAll - load all format config from all input disks in parallel.
+func loadFormatErasureAll(storageDisks []StorageAPI, heal bool) ([]*formatErasureV3, []error) {
+ // Initialize list of errors.
+ g := errgroup.WithNErrs(len(storageDisks))
+
+ // Initialize format configs.
+ formats := make([]*formatErasureV3, len(storageDisks))
+
+ // Load format from each disk in parallel
+ for index := range storageDisks {
+ g.Go(func() error {
+ if storageDisks[index] == nil {
+ return errDiskNotFound
+ }
+ format, err := loadFormatErasure(storageDisks[index], heal)
+ if err != nil {
+ return err
+ }
+ formats[index] = format
+ if !heal {
+ // If no healing required, make the disks valid and
+ // online.
+ storageDisks[index].SetDiskID(format.Erasure.This)
+ }
+ return nil
+ }, index)
+ }
+
+ // Return all formats and errors if any.
+ return formats, g.Wait()
+}
+
+func saveFormatErasure(disk StorageAPI, format *formatErasureV3, healID string) error {
+ if disk == nil || format == nil {
+ return errDiskNotFound
+ }
+
+ // Marshal and write to disk.
+ formatData, err := json.Marshal(format)
+ if err != nil {
+ return err
+ }
+
+ tmpFormat := mustGetUUID()
+
+ // Purge any existing temporary file, okay to ignore errors here.
+ defer disk.Delete(context.TODO(), minioMetaBucket, tmpFormat, DeleteOptions{
+ Recursive: false,
+ Immediate: false,
+ })
+
+ // write to unique file.
+ if err = disk.WriteAll(context.TODO(), minioMetaBucket, tmpFormat, formatData); err != nil {
+ return err
+ }
+
+ // Rename file `uuid.json` --> `format.json`.
+ if err = disk.RenameFile(context.TODO(), minioMetaBucket, tmpFormat, minioMetaBucket, formatConfigFile); err != nil {
+ return err
+ }
+
+ disk.SetDiskID(format.Erasure.This)
+ if healID != "" {
+ ctx := context.Background()
+ ht := initHealingTracker(disk, healID)
+ return ht.save(ctx)
+ }
+ return nil
+}
+
+// loadFormatErasure - loads format.json from disk.
+func loadFormatErasure(disk StorageAPI, heal bool) (format *formatErasureV3, err error) {
+ data, err := disk.ReadAll(context.TODO(), minioMetaBucket, formatConfigFile)
+ if err != nil {
+ // 'file not found' and 'volume not found' as
+ // same. 'volume not found' usually means its a fresh disk.
+ if errors.Is(err, errFileNotFound) || errors.Is(err, errVolumeNotFound) {
+ return nil, errUnformattedDisk
+ }
+ return nil, err
+ }
+
+ // Try to decode format json into formatConfigV1 struct.
+ format = &formatErasureV3{}
+ if err = json.Unmarshal(data, format); err != nil {
+ return nil, err
+ }
+
+ if heal {
+ info, err := disk.DiskInfo(context.Background(), DiskInfoOptions{NoOp: heal})
+ if err != nil {
+ return nil, err
+ }
+ format.Info = info
+ }
+
+ // Success.
+ return format, nil
+}
+
+// Valid formatErasure basic versions.
+func checkFormatErasureValue(formatErasure *formatErasureV3, disk StorageAPI) error {
+ // Validate format version and format type.
+ if formatErasure.Version != formatMetaVersionV1 {
+ return fmt.Errorf("Unsupported version of backend format [%s] found on %s", formatErasure.Version, disk)
+ }
+ if formatErasure.Format != formatBackendErasure && formatErasure.Format != formatBackendErasureSingle {
+ return fmt.Errorf("Unsupported backend format [%s] found on %s", formatErasure.Format, disk)
+ }
+ if formatErasure.Erasure.Version != formatErasureVersionV3 {
+ return fmt.Errorf("Unsupported Erasure backend format found [%s] on %s", formatErasure.Erasure.Version, disk)
+ }
+ return nil
+}
+
+// Check all format values.
+func checkFormatErasureValues(formats []*formatErasureV3, disks []StorageAPI, setDriveCount int) error {
+ for i, formatErasure := range formats {
+ if formatErasure == nil {
+ continue
+ }
+ if err := checkFormatErasureValue(formatErasure, disks[i]); err != nil {
+ return err
+ }
+ if len(formats) != len(formatErasure.Erasure.Sets)*len(formatErasure.Erasure.Sets[0]) {
+ return fmt.Errorf("%s drive is already being used in another erasure deployment. (Number of drives specified: %d but the number of drives found in the %s drive's format.json: %d)",
+ disks[i], len(formats), humanize.Ordinal(i+1), len(formatErasure.Erasure.Sets)*len(formatErasure.Erasure.Sets[0]))
+ }
+ if len(formatErasure.Erasure.Sets[0]) != setDriveCount {
+ return fmt.Errorf("%s drive is already formatted with %d drives per erasure set. This cannot be changed to %d", disks[i], len(formatErasure.Erasure.Sets[0]), setDriveCount)
+ }
+ }
+ return nil
+}
+
+// Get backend Erasure format in quorum `format.json`.
+func getFormatErasureInQuorum(formats []*formatErasureV3) (*formatErasureV3, error) {
+ formatCountMap := make(map[int]int, len(formats))
+ for _, format := range formats {
+ if format == nil {
+ continue
+ }
+ formatCountMap[format.Drives()]++
+ }
+
+ maxDrives := 0
+ maxCount := 0
+ for drives, count := range formatCountMap {
+ if count > maxCount {
+ maxCount = count
+ maxDrives = drives
+ }
+ }
+
+ if maxDrives == 0 {
+ return nil, errErasureReadQuorum
+ }
+
+ if maxCount < len(formats)/2 {
+ return nil, errErasureReadQuorum
+ }
+
+ for i, format := range formats {
+ if format == nil {
+ continue
+ }
+ if format.Drives() == maxDrives {
+ format := formats[i].Clone()
+ format.Erasure.This = ""
+ return format, nil
+ }
+ }
+
+ return nil, errErasureReadQuorum
+}
+
+func formatErasureV3Check(reference *formatErasureV3, format *formatErasureV3) error {
+ tmpFormat := format.Clone()
+ this := tmpFormat.Erasure.This
+ tmpFormat.Erasure.This = ""
+ if len(reference.Erasure.Sets) != len(format.Erasure.Sets) {
+ return fmt.Errorf("Expected number of sets %d, got %d", len(reference.Erasure.Sets), len(format.Erasure.Sets))
+ }
+
+ // Make sure that the sets match.
+ for i := range reference.Erasure.Sets {
+ if len(reference.Erasure.Sets[i]) != len(format.Erasure.Sets[i]) {
+ return fmt.Errorf("Each set should be of same size, expected %d got %d",
+ len(reference.Erasure.Sets[i]), len(format.Erasure.Sets[i]))
+ }
+ for j := range reference.Erasure.Sets[i] {
+ if reference.Erasure.Sets[i][j] != format.Erasure.Sets[i][j] {
+ return fmt.Errorf("UUID on positions %d:%d do not match with, expected %s got %s: (%w)",
+ i, j, reference.Erasure.Sets[i][j], format.Erasure.Sets[i][j], errInconsistentDisk)
+ }
+ }
+ }
+
+ // Make sure that the diskID is found in the set.
+ for i := range len(tmpFormat.Erasure.Sets) {
+ for j := 0; j < len(tmpFormat.Erasure.Sets[i]); j++ {
+ if this == tmpFormat.Erasure.Sets[i][j] {
+ return nil
+ }
+ }
+ }
+ return fmt.Errorf("DriveID %s not found in any drive sets %s", this, format.Erasure.Sets)
+}
+
+// saveFormatErasureAll - populates `format.json` on disks in its order.
+func saveFormatErasureAll(ctx context.Context, storageDisks []StorageAPI, formats []*formatErasureV3) error {
+ g := errgroup.WithNErrs(len(storageDisks))
+
+ // Write `format.json` to all disks.
+ for index := range storageDisks {
+ g.Go(func() error {
+ if formats[index] == nil {
+ return errDiskNotFound
+ }
+ return saveFormatErasure(storageDisks[index], formats[index], "")
+ }, index)
+ }
+
+ // Wait for the routines to finish.
+ return reduceWriteQuorumErrs(ctx, g.Wait(), nil, len(storageDisks))
+}
+
+// relinquishes the underlying connection for all storage disks.
+func closeStorageDisks(storageDisks ...StorageAPI) {
+ var wg sync.WaitGroup
+ for _, disk := range storageDisks {
+ if disk == nil {
+ continue
+ }
+ wg.Add(1)
+ go func(disk StorageAPI) {
+ defer wg.Done()
+ disk.Close()
+ }(disk)
+ }
+ wg.Wait()
+}
+
+// Initialize storage disks for each endpoint.
+// Errors are returned for each endpoint with matching index.
+func initStorageDisksWithErrors(endpoints Endpoints, opts storageOpts) ([]StorageAPI, []error) {
+ // Bootstrap disks.
+ storageDisks := make([]StorageAPI, len(endpoints))
+ g := errgroup.WithNErrs(len(endpoints))
+ for index := range endpoints {
+ g.Go(func() (err error) {
+ storageDisks[index], err = newStorageAPI(endpoints[index], opts)
+ return err
+ }, index)
+ }
+ return storageDisks, g.Wait()
+}
+
+// formatErasureV3ThisEmpty - find out if '.This' field is empty
+// in any of the input `formats`, if yes return true.
+func formatErasureV3ThisEmpty(formats []*formatErasureV3) bool {
+ for _, format := range formats {
+ if format == nil {
+ continue
+ }
+ // NOTE: This code is specifically needed when migrating version
+ // V1 to V2 to V3, in a scenario such as this we only need to handle
+ // single sets since we never used to support multiple sets in releases
+ // with V1 format version.
+ if len(format.Erasure.Sets) > 1 {
+ continue
+ }
+ if format.Erasure.This == "" {
+ return true
+ }
+ }
+ return false
+}
+
+// fixFormatErasureV3 - fix format Erasure configuration on all disks.
+func fixFormatErasureV3(storageDisks []StorageAPI, endpoints Endpoints, formats []*formatErasureV3) error {
+ g := errgroup.WithNErrs(len(formats))
+ for i := range formats {
+ g.Go(func() error {
+ if formats[i] == nil || !endpoints[i].IsLocal {
+ return nil
+ }
+ // NOTE: This code is specifically needed when migrating version
+ // V1 to V2 to V3, in a scenario such as this we only need to handle
+ // single sets since we never used to support multiple sets in releases
+ // with V1 format version.
+ if len(formats[i].Erasure.Sets) > 1 {
+ return nil
+ }
+ if formats[i].Erasure.This == "" {
+ formats[i].Erasure.This = formats[i].Erasure.Sets[0][i]
+ // Heal the drive if drive has .This empty.
+ if err := saveFormatErasure(storageDisks[i], formats[i], mustGetUUID()); err != nil {
+ return err
+ }
+ }
+ return nil
+ }, i)
+ }
+ for _, err := range g.Wait() {
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// initFormatErasure - save Erasure format configuration on all disks.
+func initFormatErasure(ctx context.Context, storageDisks []StorageAPI, setCount, setDriveCount int, deploymentID string, sErrs []error) (*formatErasureV3, error) {
+ format := newFormatErasureV3(setCount, setDriveCount)
+ formats := make([]*formatErasureV3, len(storageDisks))
+ wantAtMost, err := ecDrivesNoConfig(setDriveCount)
+ if err != nil {
+ return nil, err
+ }
+
+ for i := range setCount {
+ hostCount := make(map[string]int, setDriveCount)
+ for j := range setDriveCount {
+ disk := storageDisks[i*setDriveCount+j]
+ newFormat := format.Clone()
+ newFormat.Erasure.This = format.Erasure.Sets[i][j]
+ if deploymentID != "" {
+ newFormat.ID = deploymentID
+ }
+ hostCount[disk.Hostname()]++
+ formats[i*setDriveCount+j] = newFormat
+ }
+ var once sync.Once
+ for host, count := range hostCount {
+ if count > wantAtMost {
+ if host == "" {
+ host = "local"
+ }
+ once.Do(func() {
+ if len(hostCount) == 1 {
+ return
+ }
+ logger.Info(" * Set %v:", i+1)
+ for j := range setDriveCount {
+ disk := storageDisks[i*setDriveCount+j]
+ logger.Info(" - Drive: %s", disk.String())
+ }
+ })
+ logger.Info(color.Yellow("WARNING:")+" Host %v has more than %v drives of set. "+
+ "A host failure will result in data becoming unavailable.", host, wantAtMost)
+ }
+ }
+ }
+
+ // Save formats `format.json` across all disks.
+ if err := saveFormatErasureAll(ctx, storageDisks, formats); err != nil {
+ return nil, err
+ }
+
+ return getFormatErasureInQuorum(formats)
+}
+
+// ecDrivesNoConfig returns the erasure coded drives in a set if no config has been set.
+// It will attempt to read it from env variable and fall back to drives/2.
+func ecDrivesNoConfig(setDriveCount int) (int, error) {
+ sc, err := storageclass.LookupConfig(config.KVS{}, setDriveCount)
+ if err != nil {
+ return 0, err
+ }
+ return sc.GetParityForSC(storageclass.STANDARD), nil
+}
+
+// Initialize a new set of set formats which will be written to all disks.
+func newHealFormatSets(refFormat *formatErasureV3, setCount, setDriveCount int, formats []*formatErasureV3, errs []error) ([][]*formatErasureV3, [][]DiskInfo) {
+ newFormats := make([][]*formatErasureV3, setCount)
+ for i := range refFormat.Erasure.Sets {
+ newFormats[i] = make([]*formatErasureV3, setDriveCount)
+ }
+ currentDisksInfo := make([][]DiskInfo, setCount)
+ for i := range refFormat.Erasure.Sets {
+ currentDisksInfo[i] = make([]DiskInfo, setDriveCount)
+ }
+ for i := range refFormat.Erasure.Sets {
+ for j := range refFormat.Erasure.Sets[i] {
+ if errors.Is(errs[i*setDriveCount+j], errUnformattedDisk) {
+ newFormats[i][j] = &formatErasureV3{}
+ newFormats[i][j].ID = refFormat.ID
+ newFormats[i][j].Format = refFormat.Format
+ newFormats[i][j].Version = refFormat.Version
+ newFormats[i][j].Erasure.This = refFormat.Erasure.Sets[i][j]
+ newFormats[i][j].Erasure.Sets = refFormat.Erasure.Sets
+ newFormats[i][j].Erasure.Version = refFormat.Erasure.Version
+ newFormats[i][j].Erasure.DistributionAlgo = refFormat.Erasure.DistributionAlgo
+ }
+ if format := formats[i*setDriveCount+j]; format != nil && (errs[i*setDriveCount+j] == nil) {
+ if format.Info.Endpoint != "" {
+ currentDisksInfo[i][j] = format.Info
+ }
+ }
+ }
+ }
+ return newFormats, currentDisksInfo
+}
diff --git a/cmd/format-erasure_test.go b/cmd/format-erasure_test.go
new file mode 100644
index 00000000..732f6728
--- /dev/null
+++ b/cmd/format-erasure_test.go
@@ -0,0 +1,507 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "os"
+ "reflect"
+ "testing"
+)
+
+// tests fixFormatErasureV3 - fix format.json on all disks.
+func TestFixFormatV3(t *testing.T) {
+ erasureDirs, err := getRandomDisks(8)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, erasureDir := range erasureDirs {
+ defer os.RemoveAll(erasureDir)
+ }
+ endpoints := mustGetNewEndpoints(0, 8, erasureDirs...)
+
+ storageDisks, errs := initStorageDisksWithErrors(endpoints, storageOpts{cleanUp: false, healthCheck: false})
+ for _, err := range errs {
+ if err != nil && err != errDiskNotFound {
+ t.Fatal(err)
+ }
+ }
+
+ format := newFormatErasureV3(1, 8)
+ format.Erasure.DistributionAlgo = formatErasureVersionV2DistributionAlgoV1
+ formats := make([]*formatErasureV3, 8)
+
+ for j := range 8 {
+ newFormat := format.Clone()
+ newFormat.Erasure.This = format.Erasure.Sets[0][j]
+ formats[j] = newFormat
+ }
+
+ formats[1] = nil
+ expThis := formats[2].Erasure.This
+ formats[2].Erasure.This = ""
+ if err := fixFormatErasureV3(storageDisks, endpoints, formats); err != nil {
+ t.Fatal(err)
+ }
+
+ newFormats, errs := loadFormatErasureAll(storageDisks, false)
+ for _, err := range errs {
+ if err != nil && err != errUnformattedDisk {
+ t.Fatal(err)
+ }
+ }
+ gotThis := newFormats[2].Erasure.This
+ if expThis != gotThis {
+ t.Fatalf("expected uuid %s, got %s", expThis, gotThis)
+ }
+}
+
+// tests formatErasureV3ThisEmpty conditions.
+func TestFormatErasureEmpty(t *testing.T) {
+ format := newFormatErasureV3(1, 16)
+ format.Erasure.DistributionAlgo = formatErasureVersionV2DistributionAlgoV1
+ formats := make([]*formatErasureV3, 16)
+
+ for j := range 16 {
+ newFormat := format.Clone()
+ newFormat.Erasure.This = format.Erasure.Sets[0][j]
+ formats[j] = newFormat
+ }
+
+ // empty format to indicate disk not found, but this
+ // empty should return false.
+ formats[0] = nil
+
+ if ok := formatErasureV3ThisEmpty(formats); ok {
+ t.Fatalf("expected value false, got %t", ok)
+ }
+
+ formats[2].Erasure.This = ""
+ if ok := formatErasureV3ThisEmpty(formats); !ok {
+ t.Fatalf("expected value true, got %t", ok)
+ }
+}
+
+// Tests xl format migration.
+func TestFormatErasureMigrate(t *testing.T) {
+ // Get test root.
+ rootPath := t.TempDir()
+
+ m := &formatErasureV1{}
+ m.Format = formatBackendErasure
+ m.Version = formatMetaVersionV1
+ m.Erasure.Version = formatErasureVersionV1
+ m.Erasure.Disk = mustGetUUID()
+ m.Erasure.JBOD = []string{m.Erasure.Disk, mustGetUUID(), mustGetUUID(), mustGetUUID()}
+
+ b, err := json.Marshal(m)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err = os.MkdirAll(pathJoin(rootPath, minioMetaBucket), os.FileMode(0o755)); err != nil {
+ t.Fatal(err)
+ }
+
+ if err = os.WriteFile(pathJoin(rootPath, minioMetaBucket, formatConfigFile), b, os.FileMode(0o644)); err != nil {
+ t.Fatal(err)
+ }
+
+ formatData, _, err := formatErasureMigrate(rootPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ migratedVersion, err := formatGetBackendErasureVersion(formatData)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if migratedVersion != formatErasureVersionV3 {
+ t.Fatalf("expected version: %s, got: %s", formatErasureVersionV3, migratedVersion)
+ }
+
+ b, err = os.ReadFile(pathJoin(rootPath, minioMetaBucket, formatConfigFile))
+ if err != nil {
+ t.Fatal(err)
+ }
+ formatV3 := &formatErasureV3{}
+ if err = json.Unmarshal(b, formatV3); err != nil {
+ t.Fatal(err)
+ }
+ if formatV3.Erasure.This != m.Erasure.Disk {
+ t.Fatalf("expected drive uuid: %s, got: %s", m.Erasure.Disk, formatV3.Erasure.This)
+ }
+ if len(formatV3.Erasure.Sets) != 1 {
+ t.Fatalf("expected single set after migrating from v1 to v3, but found %d", len(formatV3.Erasure.Sets))
+ }
+ if !reflect.DeepEqual(formatV3.Erasure.Sets[0], m.Erasure.JBOD) {
+ t.Fatalf("expected drive uuid: %v, got: %v", m.Erasure.JBOD, formatV3.Erasure.Sets[0])
+ }
+
+ m = &formatErasureV1{}
+ m.Format = "unknown"
+ m.Version = formatMetaVersionV1
+ m.Erasure.Version = formatErasureVersionV1
+ m.Erasure.Disk = mustGetUUID()
+ m.Erasure.JBOD = []string{m.Erasure.Disk, mustGetUUID(), mustGetUUID(), mustGetUUID()}
+
+ b, err = json.Marshal(m)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err = os.WriteFile(pathJoin(rootPath, minioMetaBucket, formatConfigFile), b, os.FileMode(0o644)); err != nil {
+ t.Fatal(err)
+ }
+
+ if _, _, err = formatErasureMigrate(rootPath); err == nil {
+ t.Fatal("Expected to fail with unexpected backend format")
+ }
+
+ m = &formatErasureV1{}
+ m.Format = formatBackendErasure
+ m.Version = formatMetaVersionV1
+ m.Erasure.Version = "30"
+ m.Erasure.Disk = mustGetUUID()
+ m.Erasure.JBOD = []string{m.Erasure.Disk, mustGetUUID(), mustGetUUID(), mustGetUUID()}
+
+ b, err = json.Marshal(m)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err = os.WriteFile(pathJoin(rootPath, minioMetaBucket, formatConfigFile), b, os.FileMode(0o644)); err != nil {
+ t.Fatal(err)
+ }
+
+ if _, _, err = formatErasureMigrate(rootPath); err == nil {
+ t.Fatal("Expected to fail with unexpected backend format version number")
+ }
+}
+
+// Tests check format xl value.
+func TestCheckFormatErasureValue(t *testing.T) {
+ testCases := []struct {
+ format *formatErasureV3
+ success bool
+ }{
+ // Invalid Erasure format version "2".
+ {
+ &formatErasureV3{
+ formatMetaV1: formatMetaV1{
+ Version: "2",
+ Format: "Erasure",
+ },
+ Erasure: struct {
+ Version string `json:"version"`
+ This string `json:"this"`
+ Sets [][]string `json:"sets"`
+ DistributionAlgo string `json:"distributionAlgo"`
+ }{
+ Version: "2",
+ },
+ },
+ false,
+ },
+ // Invalid Erasure format "Unknown".
+ {
+ &formatErasureV3{
+ formatMetaV1: formatMetaV1{
+ Version: "1",
+ Format: "Unknown",
+ },
+ Erasure: struct {
+ Version string `json:"version"`
+ This string `json:"this"`
+ Sets [][]string `json:"sets"`
+ DistributionAlgo string `json:"distributionAlgo"`
+ }{
+ Version: "2",
+ },
+ },
+ false,
+ },
+ // Invalid Erasure format version "0".
+ {
+ &formatErasureV3{
+ formatMetaV1: formatMetaV1{
+ Version: "1",
+ Format: "Erasure",
+ },
+ Erasure: struct {
+ Version string `json:"version"`
+ This string `json:"this"`
+ Sets [][]string `json:"sets"`
+ DistributionAlgo string `json:"distributionAlgo"`
+ }{
+ Version: "0",
+ },
+ },
+ false,
+ },
+ }
+
+ // Valid all test cases.
+ for i, testCase := range testCases {
+ if err := checkFormatErasureValue(testCase.format, nil); err != nil && testCase.success {
+ t.Errorf("Test %d: Expected failure %s", i+1, err)
+ }
+ }
+}
+
+// Tests getFormatErasureInQuorum()
+func TestGetFormatErasureInQuorumCheck(t *testing.T) {
+ setCount := 2
+ setDriveCount := 16
+
+ format := newFormatErasureV3(setCount, setDriveCount)
+ format.Erasure.DistributionAlgo = formatErasureVersionV2DistributionAlgoV1
+ formats := make([]*formatErasureV3, 32)
+
+ for i := range setCount {
+ for j := range setDriveCount {
+ newFormat := format.Clone()
+ newFormat.Erasure.This = format.Erasure.Sets[i][j]
+ formats[i*setDriveCount+j] = newFormat
+ }
+ }
+
+ // Return a format from list of formats in quorum.
+ quorumFormat, err := getFormatErasureInQuorum(formats)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Check if the reference format and input formats are same.
+ if err = formatErasureV3Check(quorumFormat, formats[0]); err != nil {
+ t.Fatal(err)
+ }
+
+ // QuorumFormat has .This field empty on purpose, expect a failure.
+ if err = formatErasureV3Check(formats[0], quorumFormat); err == nil {
+ t.Fatal("Unexpected success")
+ }
+
+ formats[0] = nil
+ quorumFormat, err = getFormatErasureInQuorum(formats)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ badFormat := *quorumFormat
+ badFormat.Erasure.Sets = nil
+ if err = formatErasureV3Check(quorumFormat, &badFormat); err == nil {
+ t.Fatal("Unexpected success")
+ }
+
+ badFormatUUID := *quorumFormat
+ badFormatUUID.Erasure.Sets[0][0] = "bad-uuid"
+ if err = formatErasureV3Check(quorumFormat, &badFormatUUID); err == nil {
+ t.Fatal("Unexpected success")
+ }
+
+ badFormatSetSize := *quorumFormat
+ badFormatSetSize.Erasure.Sets[0] = nil
+ if err = formatErasureV3Check(quorumFormat, &badFormatSetSize); err == nil {
+ t.Fatal("Unexpected success")
+ }
+
+ for i := range formats {
+ if i < 17 {
+ formats[i] = nil
+ }
+ }
+ if _, err = getFormatErasureInQuorum(formats); err == nil {
+ t.Fatal("Unexpected success")
+ }
+}
+
+// Get backend Erasure format in quorum `format.json`.
+func getFormatErasureInQuorumOld(formats []*formatErasureV3) (*formatErasureV3, error) {
+ formatHashes := make([]string, len(formats))
+ for i, format := range formats {
+ if format == nil {
+ continue
+ }
+ h := sha256.New()
+ for _, set := range format.Erasure.Sets {
+ for _, diskID := range set {
+ h.Write([]byte(diskID))
+ }
+ }
+ formatHashes[i] = hex.EncodeToString(h.Sum(nil))
+ }
+
+ formatCountMap := make(map[string]int)
+ for _, hash := range formatHashes {
+ if hash == "" {
+ continue
+ }
+ formatCountMap[hash]++
+ }
+
+ maxHash := ""
+ maxCount := 0
+ for hash, count := range formatCountMap {
+ if count > maxCount {
+ maxCount = count
+ maxHash = hash
+ }
+ }
+
+ if maxCount < len(formats)/2 {
+ return nil, errErasureReadQuorum
+ }
+
+ for i, hash := range formatHashes {
+ if hash == maxHash {
+ format := formats[i].Clone()
+ format.Erasure.This = ""
+ return format, nil
+ }
+ }
+
+ return nil, errErasureReadQuorum
+}
+
+func BenchmarkGetFormatErasureInQuorumOld(b *testing.B) {
+ setCount := 200
+ setDriveCount := 15
+
+ format := newFormatErasureV3(setCount, setDriveCount)
+ format.Erasure.DistributionAlgo = formatErasureVersionV2DistributionAlgoV1
+ formats := make([]*formatErasureV3, 15*200)
+
+ for i := range setCount {
+ for j := range setDriveCount {
+ newFormat := format.Clone()
+ newFormat.Erasure.This = format.Erasure.Sets[i][j]
+ formats[i*setDriveCount+j] = newFormat
+ }
+ }
+
+ b.ReportAllocs()
+
+ for b.Loop() {
+ _, _ = getFormatErasureInQuorumOld(formats)
+ }
+}
+
+func BenchmarkGetFormatErasureInQuorum(b *testing.B) {
+ setCount := 200
+ setDriveCount := 15
+
+ format := newFormatErasureV3(setCount, setDriveCount)
+ format.Erasure.DistributionAlgo = formatErasureVersionV2DistributionAlgoV1
+ formats := make([]*formatErasureV3, 15*200)
+
+ for i := range setCount {
+ for j := range setDriveCount {
+ newFormat := format.Clone()
+ newFormat.Erasure.This = format.Erasure.Sets[i][j]
+ formats[i*setDriveCount+j] = newFormat
+ }
+ }
+
+ b.ReportAllocs()
+
+ for b.Loop() {
+ _, _ = getFormatErasureInQuorum(formats)
+ }
+}
+
+// Initialize new format sets.
+func TestNewFormatSets(t *testing.T) {
+ setCount := 2
+ setDriveCount := 16
+
+ format := newFormatErasureV3(setCount, setDriveCount)
+ format.Erasure.DistributionAlgo = formatErasureVersionV2DistributionAlgoV1
+ formats := make([]*formatErasureV3, 32)
+ errs := make([]error, 32)
+
+ for i := range setCount {
+ for j := range setDriveCount {
+ newFormat := format.Clone()
+ newFormat.Erasure.This = format.Erasure.Sets[i][j]
+ formats[i*setDriveCount+j] = newFormat
+ }
+ }
+
+ quorumFormat, err := getFormatErasureInQuorum(formats)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // 16th disk is unformatted.
+ errs[15] = errUnformattedDisk
+
+ newFormats, _ := newHealFormatSets(quorumFormat, setCount, setDriveCount, formats, errs)
+ if newFormats == nil {
+ t.Fatal("Unexpected failure")
+ }
+
+ // Check if deployment IDs are preserved.
+ for i := range newFormats {
+ for j := range newFormats[i] {
+ if newFormats[i][j] == nil {
+ continue
+ }
+ if newFormats[i][j].ID != quorumFormat.ID {
+ t.Fatal("Deployment id in the new format is lost")
+ }
+ }
+ }
+}
+
+func BenchmarkInitStorageDisks256(b *testing.B) {
+ benchmarkInitStorageDisksN(b, 256)
+}
+
+func BenchmarkInitStorageDisks1024(b *testing.B) {
+ benchmarkInitStorageDisksN(b, 1024)
+}
+
+func BenchmarkInitStorageDisks2048(b *testing.B) {
+ benchmarkInitStorageDisksN(b, 2048)
+}
+
+func BenchmarkInitStorageDisksMax(b *testing.B) {
+ benchmarkInitStorageDisksN(b, 32*204)
+}
+
+func benchmarkInitStorageDisksN(b *testing.B, nDisks int) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ fsDirs, err := getRandomDisks(nDisks)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ endpoints := mustGetNewEndpoints(0, 16, fsDirs...)
+ b.RunParallel(func(pb *testing.PB) {
+ endpoints := endpoints
+ for pb.Next() {
+ initStorageDisksWithErrors(endpoints, storageOpts{cleanUp: false, healthCheck: false})
+ }
+ })
+}
diff --git a/cmd/format-meta.go b/cmd/format-meta.go
new file mode 100644
index 00000000..dd558407
--- /dev/null
+++ b/cmd/format-meta.go
@@ -0,0 +1,52 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+// Format related consts
+const (
+ // Format config file carries backend format specific details.
+ formatConfigFile = "format.json"
+)
+
+const (
+ // Version of the formatMetaV1
+ formatMetaVersionV1 = "1"
+)
+
+// format.json currently has the format:
+// {
+// "version": "1",
+// "format": "XXXXX",
+// "XXXXX": {
+//
+// }
+// }
+// Here "XXXXX" depends on the backend, currently we have "fs" and "xl" implementations.
+// formatMetaV1 should be inherited by backend format structs. Please look at format-fs.go
+// and format-xl.go for details.
+
+// Ideally we will never have a situation where we will have to change the
+// fields of this struct and deal with related migration.
+type formatMetaV1 struct {
+ // Version of the format config.
+ Version string `json:"version"`
+ // Format indicates the backend format type, supports two values 'xl' and 'fs'.
+ Format string `json:"format"`
+ // ID is the identifier for the minio deployment
+ ID string `json:"id"`
+}
diff --git a/cmd/format_string.go b/cmd/format_string.go
new file mode 100644
index 00000000..ce068152
--- /dev/null
+++ b/cmd/format_string.go
@@ -0,0 +1,28 @@
+// Code generated by "stringer -type=format -trimprefix=format untar.go"; DO NOT EDIT.
+
+package cmd
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[formatUnknown-0]
+ _ = x[formatGzip-1]
+ _ = x[formatZstd-2]
+ _ = x[formatLZ4-3]
+ _ = x[formatS2-4]
+ _ = x[formatBZ2-5]
+}
+
+const _format_name = "UnknownGzipZstdLZ4S2BZ2"
+
+var _format_index = [...]uint8{0, 7, 11, 15, 18, 20, 23}
+
+func (i format) String() string {
+ if i < 0 || i >= format(len(_format_index)-1) {
+ return "format(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _format_name[_format_index[i]:_format_index[i+1]]
+}
diff --git a/cmd/ftp-server-driver.go b/cmd/ftp-server-driver.go
new file mode 100644
index 00000000..7f21eeb8
--- /dev/null
+++ b/cmd/ftp-server-driver.go
@@ -0,0 +1,561 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "crypto/subtle"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "os"
+ "path"
+ "strings"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio-go/v7"
+ "github.com/minio/minio-go/v7/pkg/credentials"
+ "github.com/minio/minio/internal/auth"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/pkg/v3/mimedb"
+ ftp "goftp.io/server/v2"
+)
+
+var _ ftp.Driver = &ftpDriver{}
+
+// ftpDriver implements ftpDriver to store files in minio
+type ftpDriver struct {
+ endpoint string
+}
+
+// NewFTPDriver implements ftp.Driver interface
+func NewFTPDriver() ftp.Driver {
+ return &ftpDriver{endpoint: fmt.Sprintf("127.0.0.1:%s", globalMinioPort)}
+}
+
+func buildMinioPath(p string) string {
+ return strings.TrimPrefix(p, SlashSeparator)
+}
+
+func buildMinioDir(p string) string {
+ v := buildMinioPath(p)
+ if !strings.HasSuffix(v, SlashSeparator) {
+ return v + SlashSeparator
+ }
+ return v
+}
+
+type minioFileInfo struct {
+ p string
+ info minio.ObjectInfo
+ isDir bool
+}
+
+func (m *minioFileInfo) Name() string {
+ return m.p
+}
+
+func (m *minioFileInfo) Size() int64 {
+ return m.info.Size
+}
+
+func (m *minioFileInfo) Mode() os.FileMode {
+ if m.isDir {
+ return os.ModeDir
+ }
+ return os.ModePerm
+}
+
+var minFileDate = time.Date(1980, 1, 1, 0, 0, 0, 0, time.UTC) // Workaround for Filezilla
+
+func (m *minioFileInfo) ModTime() time.Time {
+ if !m.info.LastModified.IsZero() {
+ return m.info.LastModified
+ }
+ return minFileDate
+}
+
+func (m *minioFileInfo) IsDir() bool {
+ return m.isDir
+}
+
+func (m *minioFileInfo) Sys() any {
+ return nil
+}
+
+//msgp:ignore ftpMetrics
+type ftpMetrics struct{}
+
+var globalFtpMetrics ftpMetrics
+
+func ftpTrace(s *ftp.Context, startTime time.Time, source, objPath string, err error, sz int64) madmin.TraceInfo {
+ var errStr string
+ if err != nil {
+ errStr = err.Error()
+ }
+ return madmin.TraceInfo{
+ TraceType: madmin.TraceFTP,
+ Time: startTime,
+ NodeName: globalLocalNodeName,
+ FuncName: s.Cmd,
+ Duration: time.Since(startTime),
+ Path: objPath,
+ Error: errStr,
+ Bytes: sz,
+ Custom: map[string]string{
+ "user": s.Sess.LoginUser(),
+ "cmd": s.Cmd,
+ "param": s.Param,
+ "login": fmt.Sprintf("%t", s.Sess.IsLogin()),
+ "source": source,
+ },
+ }
+}
+
+func (m *ftpMetrics) log(s *ftp.Context, paths ...string) func(sz int64, err error) {
+ startTime := time.Now()
+ source := getSource(2)
+ return func(sz int64, err error) {
+ globalTrace.Publish(ftpTrace(s, startTime, source, strings.Join(paths, " "), err, sz))
+ }
+}
+
+// Stat implements ftpDriver
+func (driver *ftpDriver) Stat(ctx *ftp.Context, objPath string) (fi os.FileInfo, err error) {
+ stopFn := globalFtpMetrics.log(ctx, objPath)
+ defer stopFn(0, err)
+
+ if objPath == SlashSeparator {
+ return &minioFileInfo{
+ p: SlashSeparator,
+ isDir: true,
+ }, nil
+ }
+
+ bucket, object := path2BucketObject(objPath)
+ if bucket == "" {
+ return nil, errors.New("bucket name cannot be empty")
+ }
+
+ clnt, err := driver.getMinIOClient(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ if object == "" {
+ ok, err := clnt.BucketExists(context.Background(), bucket)
+ if err != nil {
+ return nil, err
+ }
+ if !ok {
+ return nil, os.ErrNotExist
+ }
+ return &minioFileInfo{
+ p: pathClean(bucket),
+ info: minio.ObjectInfo{Key: bucket},
+ isDir: true,
+ }, nil
+ }
+
+ objInfo, err := clnt.StatObject(context.Background(), bucket, object, minio.StatObjectOptions{})
+ if err != nil {
+ if minio.ToErrorResponse(err).Code == "NoSuchKey" {
+ // dummy return to satisfy LIST (stat -> list) behavior.
+ return &minioFileInfo{
+ p: pathClean(object),
+ info: minio.ObjectInfo{Key: object},
+ isDir: true,
+ }, nil
+ }
+ return nil, err
+ }
+
+ isDir := strings.HasSuffix(objInfo.Key, SlashSeparator)
+ return &minioFileInfo{
+ p: pathClean(object),
+ info: objInfo,
+ isDir: isDir,
+ }, nil
+}
+
+// ListDir implements ftpDriver
+func (driver *ftpDriver) ListDir(ctx *ftp.Context, objPath string, callback func(os.FileInfo) error) (err error) {
+ stopFn := globalFtpMetrics.log(ctx, objPath)
+ defer stopFn(0, err)
+
+ clnt, err := driver.getMinIOClient(ctx)
+ if err != nil {
+ return err
+ }
+
+ cctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ bucket, prefix := path2BucketObject(objPath)
+ if bucket == "" {
+ buckets, err := clnt.ListBuckets(cctx)
+ if err != nil {
+ return err
+ }
+
+ for _, bucket := range buckets {
+ info := minioFileInfo{
+ p: pathClean(bucket.Name),
+ info: minio.ObjectInfo{Key: retainSlash(bucket.Name), LastModified: bucket.CreationDate},
+ isDir: true,
+ }
+ if err := callback(&info); err != nil {
+ return err
+ }
+ }
+
+ return nil
+ }
+
+ prefix = retainSlash(prefix)
+
+ for object := range clnt.ListObjects(cctx, bucket, minio.ListObjectsOptions{
+ Prefix: prefix,
+ Recursive: false,
+ }) {
+ if object.Err != nil {
+ return object.Err
+ }
+
+ if object.Key == prefix {
+ continue
+ }
+
+ isDir := strings.HasSuffix(object.Key, SlashSeparator)
+ info := minioFileInfo{
+ p: pathClean(strings.TrimPrefix(object.Key, prefix)),
+ info: object,
+ isDir: isDir,
+ }
+
+ if err := callback(&info); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (driver *ftpDriver) CheckPasswd(c *ftp.Context, username, password string) (ok bool, err error) {
+ stopFn := globalFtpMetrics.log(c, username)
+ defer stopFn(0, err)
+
+ if globalIAMSys.LDAPConfig.Enabled() {
+ sa, _, err := globalIAMSys.getServiceAccount(context.Background(), username)
+ if err != nil && !errors.Is(err, errNoSuchServiceAccount) {
+ return false, err
+ }
+ if errors.Is(err, errNoSuchServiceAccount) {
+ lookupRes, groupDistNames, err := globalIAMSys.LDAPConfig.Bind(username, password)
+ if err != nil {
+ return false, err
+ }
+ ldapPolicies, _ := globalIAMSys.PolicyDBGet(lookupRes.NormDN, groupDistNames...)
+ return len(ldapPolicies) > 0, nil
+ }
+ return subtle.ConstantTimeCompare([]byte(sa.Credentials.SecretKey), []byte(password)) == 1, nil
+ }
+
+ ui, ok := globalIAMSys.GetUser(context.Background(), username)
+ if !ok {
+ return false, nil
+ }
+ return subtle.ConstantTimeCompare([]byte(ui.Credentials.SecretKey), []byte(password)) == 1, nil
+}
+
+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())
+ if !ok && !globalIAMSys.LDAPConfig.Enabled() {
+ return nil, errNoSuchUser
+ }
+ if !ok && globalIAMSys.LDAPConfig.Enabled() {
+ sa, _, err := globalIAMSys.getServiceAccount(context.Background(), ctx.Sess.LoginUser())
+ if err != nil && !errors.Is(err, errNoSuchServiceAccount) {
+ return nil, err
+ }
+
+ var mcreds *credentials.Credentials
+ if errors.Is(err, errNoSuchServiceAccount) {
+ lookupResult, targetGroups, err := globalIAMSys.LDAPConfig.LookupUserDN(ctx.Sess.LoginUser())
+ if err != nil {
+ return nil, err
+ }
+ ldapPolicies, _ := globalIAMSys.PolicyDBGet(lookupResult.NormDN, targetGroups...)
+ if len(ldapPolicies) == 0 {
+ return nil, errAuthentication
+ }
+ expiryDur, err := globalIAMSys.LDAPConfig.GetExpiryDuration("")
+ if err != nil {
+ return nil, err
+ }
+ claims := make(map[string]any)
+ claims[expClaim] = UTCNow().Add(expiryDur).Unix()
+
+ claims[ldapUser] = lookupResult.NormDN
+ claims[ldapActualUser] = lookupResult.ActualDN
+ claims[ldapUserN] = ctx.Sess.LoginUser()
+
+ // Add LDAP attributes that were looked up into the claims.
+ for attribKey, attribValue := range lookupResult.Attributes {
+ claims[ldapAttribPrefix+attribKey] = attribValue
+ }
+
+ cred, err := auth.GetNewCredentialsWithMetadata(claims, globalActiveCred.SecretKey)
+ if err != nil {
+ return nil, err
+ }
+
+ // Set the parent of the temporary access key, this is useful
+ // in obtaining service accounts by this cred.
+ cred.ParentUser = lookupResult.NormDN
+
+ // Set this value to LDAP groups, LDAP user can be part
+ // of large number of groups
+ cred.Groups = targetGroups
+
+ // Set the newly generated credentials, policyName is empty on purpose
+ // LDAP policies are applied automatically using their ldapUser, ldapGroups
+ // mapping.
+ updatedAt, err := globalIAMSys.SetTempUser(context.Background(), cred.AccessKey, cred, "")
+ if err != nil {
+ return nil, err
+ }
+
+ // Call hook for site replication.
+ replLogIf(context.Background(), globalSiteReplicationSys.IAMChangeHook(context.Background(), madmin.SRIAMItem{
+ Type: madmin.SRIAMItemSTSAcc,
+ STSCredential: &madmin.SRSTSCredential{
+ AccessKey: cred.AccessKey,
+ SecretKey: cred.SecretKey,
+ SessionToken: cred.SessionToken,
+ ParentUser: cred.ParentUser,
+ },
+ UpdatedAt: updatedAt,
+ }))
+
+ mcreds = credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken)
+ } else {
+ mcreds = credentials.NewStaticV4(sa.Credentials.AccessKey, sa.Credentials.SecretKey, "")
+ }
+
+ return minio.New(driver.endpoint, &minio.Options{
+ Creds: mcreds,
+ Secure: globalIsTLS,
+ Transport: tr,
+ TrailingHeaders: true,
+ })
+ }
+
+ // ok == true - at this point
+
+ if ui.Credentials.IsTemp() {
+ // Temporary credentials are not allowed.
+ return nil, errAuthentication
+ }
+
+ return minio.New(driver.endpoint, &minio.Options{
+ Creds: credentials.NewStaticV4(ui.Credentials.AccessKey, ui.Credentials.SecretKey, ""),
+ Secure: globalIsTLS,
+ Transport: tr,
+ TrailingHeaders: true,
+ })
+}
+
+// DeleteDir implements ftpDriver
+func (driver *ftpDriver) DeleteDir(ctx *ftp.Context, objPath string) (err error) {
+ stopFn := globalFtpMetrics.log(ctx, objPath)
+ defer stopFn(0, err)
+
+ bucket, prefix := path2BucketObject(objPath)
+ if bucket == "" {
+ return errors.New("deleting all buckets not allowed")
+ }
+
+ clnt, err := driver.getMinIOClient(ctx)
+ if err != nil {
+ return err
+ }
+
+ cctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ if prefix == "" {
+ // if all objects are not deleted yet this call may fail.
+ return clnt.RemoveBucket(cctx, bucket)
+ }
+
+ objectsCh := make(chan minio.ObjectInfo)
+
+ // Send object names that are needed to be removed to objectsCh
+ go func() {
+ defer xioutil.SafeClose(objectsCh)
+ opts := minio.ListObjectsOptions{
+ Prefix: prefix,
+ Recursive: true,
+ }
+ for object := range clnt.ListObjects(cctx, bucket, opts) {
+ if object.Err != nil {
+ return
+ }
+ objectsCh <- object
+ }
+ }()
+
+ // Call RemoveObjects API
+ for err := range clnt.RemoveObjects(context.Background(), bucket, objectsCh, minio.RemoveObjectsOptions{}) {
+ if err.Err != nil {
+ return err.Err
+ }
+ }
+
+ return nil
+}
+
+// DeleteFile implements ftpDriver
+func (driver *ftpDriver) DeleteFile(ctx *ftp.Context, objPath string) (err error) {
+ stopFn := globalFtpMetrics.log(ctx, objPath)
+ defer stopFn(0, err)
+
+ bucket, object := path2BucketObject(objPath)
+ if bucket == "" {
+ return errors.New("bucket name cannot be empty")
+ }
+
+ clnt, err := driver.getMinIOClient(ctx)
+ if err != nil {
+ return err
+ }
+
+ return clnt.RemoveObject(context.Background(), bucket, object, minio.RemoveObjectOptions{})
+}
+
+// Rename implements ftpDriver
+func (driver *ftpDriver) Rename(ctx *ftp.Context, fromObjPath string, toObjPath string) (err error) {
+ stopFn := globalFtpMetrics.log(ctx, fromObjPath, toObjPath)
+ defer stopFn(0, err)
+
+ return NotImplemented{}
+}
+
+// MakeDir implements ftpDriver
+func (driver *ftpDriver) MakeDir(ctx *ftp.Context, objPath string) (err error) {
+ stopFn := globalFtpMetrics.log(ctx, objPath)
+ defer stopFn(0, err)
+
+ bucket, prefix := path2BucketObject(objPath)
+ if bucket == "" {
+ return errors.New("bucket name cannot be empty")
+ }
+
+ clnt, err := driver.getMinIOClient(ctx)
+ if err != nil {
+ return err
+ }
+
+ if prefix == "" {
+ return clnt.MakeBucket(context.Background(), bucket, minio.MakeBucketOptions{Region: globalSite.Region()})
+ }
+
+ dirPath := buildMinioDir(prefix)
+
+ _, err = clnt.PutObject(context.Background(), bucket, dirPath, bytes.NewReader([]byte("")), 0, minio.PutObjectOptions{
+ DisableContentSha256: true,
+ })
+ return err
+}
+
+// GetFile implements ftpDriver
+func (driver *ftpDriver) GetFile(ctx *ftp.Context, objPath string, offset int64) (n int64, rc io.ReadCloser, err error) {
+ stopFn := globalFtpMetrics.log(ctx, objPath)
+ defer stopFn(n, err)
+
+ bucket, object := path2BucketObject(objPath)
+ if bucket == "" {
+ return 0, nil, errors.New("bucket name cannot be empty")
+ }
+
+ clnt, err := driver.getMinIOClient(ctx)
+ if err != nil {
+ return 0, nil, err
+ }
+
+ opts := minio.GetObjectOptions{}
+ obj, err := clnt.GetObject(context.Background(), bucket, object, opts)
+ if err != nil {
+ return 0, nil, err
+ }
+ defer func() {
+ if err != nil && obj != nil {
+ obj.Close()
+ }
+ }()
+
+ _, err = obj.Seek(offset, io.SeekStart)
+ if err != nil {
+ return 0, nil, err
+ }
+
+ info, err := obj.Stat()
+ if err != nil {
+ return 0, nil, err
+ }
+ n = info.Size - offset
+ return n, obj, nil
+}
+
+// PutFile implements ftpDriver
+func (driver *ftpDriver) PutFile(ctx *ftp.Context, objPath string, data io.Reader, offset int64) (n int64, err error) {
+ stopFn := globalFtpMetrics.log(ctx, objPath)
+ defer stopFn(n, err)
+
+ bucket, object := path2BucketObject(objPath)
+ if bucket == "" {
+ return 0, errors.New("bucket name cannot be empty")
+ }
+
+ if offset != -1 {
+ // FTP - APPEND not implemented
+ return 0, NotImplemented{}
+ }
+
+ clnt, err := driver.getMinIOClient(ctx)
+ if err != nil {
+ return 0, err
+ }
+
+ info, err := clnt.PutObject(context.Background(), bucket, object, data, -1, minio.PutObjectOptions{
+ ContentType: mimedb.TypeByExtension(path.Ext(object)),
+ DisableContentSha256: true,
+ Checksum: minio.ChecksumFullObjectCRC32C,
+ })
+ n = info.Size
+ return n, err
+}
diff --git a/cmd/ftp-server.go b/cmd/ftp-server.go
new file mode 100644
index 00000000..562336e3
--- /dev/null
+++ b/cmd/ftp-server.go
@@ -0,0 +1,171 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "fmt"
+ "net"
+ "strconv"
+ "strings"
+
+ "github.com/minio/minio/internal/logger"
+ ftp "goftp.io/server/v2"
+)
+
+var globalRemoteFTPClientTransport = NewRemoteTargetHTTPTransport(true)()
+
+// minioLogger use an instance of this to log in a standard format
+type minioLogger struct{}
+
+// Print implement Logger
+func (log *minioLogger) Print(sessionID string, message any) {
+ if serverDebugLog {
+ fmt.Printf("%s %s\n", sessionID, message)
+ }
+}
+
+// Printf implement Logger
+func (log *minioLogger) Printf(sessionID string, format string, v ...any) {
+ if serverDebugLog {
+ if sessionID != "" {
+ fmt.Printf("%s %s\n", sessionID, fmt.Sprintf(format, v...))
+ } else {
+ fmt.Printf(format+"\n", v...)
+ }
+ }
+}
+
+// PrintCommand implement Logger
+func (log *minioLogger) PrintCommand(sessionID string, command string, params string) {
+ if serverDebugLog {
+ if command == "PASS" {
+ fmt.Printf("%s > PASS ****\n", sessionID)
+ } else {
+ fmt.Printf("%s > %s %s\n", sessionID, command, params)
+ }
+ }
+}
+
+// PrintResponse implement Logger
+func (log *minioLogger) PrintResponse(sessionID string, code int, message string) {
+ if serverDebugLog {
+ fmt.Printf("%s < %d %s\n", sessionID, code, message)
+ }
+}
+
+func startFTPServer(args []string) {
+ var (
+ port int
+ publicIP string
+ portRange string
+ tlsPrivateKey string
+ tlsPublicCert string
+ forceTLS bool
+ )
+
+ var err error
+ for _, arg := range args {
+ tokens := strings.SplitN(arg, "=", 2)
+ if len(tokens) != 2 {
+ logger.Fatal(fmt.Errorf("invalid arguments passed to --ftp=%s", arg), "unable to start FTP server")
+ }
+ switch tokens[0] {
+ case "address":
+ host, portStr, err := net.SplitHostPort(tokens[1])
+ if err != nil {
+ logger.Fatal(fmt.Errorf("invalid arguments passed to --ftp=%s (%v)", arg, err), "unable to start FTP server")
+ }
+ port, err = strconv.Atoi(portStr)
+ if err != nil {
+ logger.Fatal(fmt.Errorf("invalid arguments passed to --ftp=%s (%v)", arg, err), "unable to start FTP server")
+ }
+ if port < 1 || port > 65535 {
+ logger.Fatal(fmt.Errorf("invalid arguments passed to --ftp=%s, (port number must be between 1 to 65535)", arg), "unable to start FTP server")
+ }
+ publicIP = host
+ case "passive-port-range":
+ portRange = tokens[1]
+ case "tls-private-key":
+ tlsPrivateKey = tokens[1]
+ case "tls-public-cert":
+ tlsPublicCert = tokens[1]
+ case "force-tls":
+ forceTLS, err = strconv.ParseBool(tokens[1])
+ if err != nil {
+ logger.Fatal(fmt.Errorf("invalid arguments passed to --ftp=%s (%v)", arg, err), "unable to start FTP server")
+ }
+ }
+ }
+
+ // Verify if only partial inputs are given for FTP(secure)
+ {
+ if tlsPrivateKey == "" && tlsPublicCert != "" {
+ logger.Fatal(fmt.Errorf("invalid TLS arguments provided missing private key --ftp=\"tls-private-key=path/to/private.key\""), "unable to start FTP server")
+ }
+
+ if tlsPrivateKey != "" && tlsPublicCert == "" {
+ logger.Fatal(fmt.Errorf("invalid TLS arguments provided missing public cert --ftp=\"tls-public-cert=path/to/public.crt\""), "unable to start FTP server")
+ }
+ if port == 0 {
+ port = 8021 // Default FTP port, since no port was given.
+ }
+ }
+
+ // If no TLS certs were provided, server is running in TLS for S3 API
+ // we automatically make FTP also run under TLS mode.
+ if globalIsTLS && tlsPrivateKey == "" && tlsPublicCert == "" {
+ tlsPrivateKey = getPrivateKeyFile()
+ tlsPublicCert = getPublicCertFile()
+ }
+
+ tls := tlsPrivateKey != "" && tlsPublicCert != ""
+
+ if forceTLS && !tls {
+ logger.Fatal(fmt.Errorf("invalid TLS arguments provided. force-tls, but missing private key --ftp=\"tls-private-key=path/to/private.key\""), "unable to start FTP server")
+ }
+
+ name := "MinIO FTP Server"
+ if tls {
+ name = "MinIO FTP(Secure) Server"
+ }
+
+ ftpServer, err := ftp.NewServer(&ftp.Options{
+ Name: name,
+ WelcomeMessage: fmt.Sprintf("Welcome to '%s' FTP Server Version='%s' License='%s'", MinioStoreName, MinioLicense, Version),
+ Driver: NewFTPDriver(),
+ Port: port,
+ Perm: ftp.NewSimplePerm("nobody", "nobody"),
+ TLS: tls,
+ KeyFile: tlsPrivateKey,
+ CertFile: tlsPublicCert,
+ ExplicitFTPS: tls,
+ Logger: &minioLogger{},
+ PassivePorts: portRange,
+ PublicIP: publicIP,
+ ForceTLS: forceTLS,
+ })
+ if err != nil {
+ logger.Fatal(err, "unable to initialize FTP server")
+ }
+
+ logger.Info(fmt.Sprintf("%s listening on %s", name, net.JoinHostPort(publicIP, strconv.Itoa(port))))
+
+ if err = ftpServer.ListenAndServe(); err != nil {
+ logger.Fatal(err, "unable to start FTP server")
+ }
+}
diff --git a/cmd/generic-handlers.go b/cmd/generic-handlers.go
new file mode 100644
index 00000000..88a11166
--- /dev/null
+++ b/cmd/generic-handlers.go
@@ -0,0 +1,632 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "fmt"
+ "net"
+ "net/http"
+ "path"
+ "runtime/debug"
+ "slices"
+ "strings"
+ "sync/atomic"
+ "time"
+ "unicode"
+
+ "github.com/dustin/go-humanize"
+ "github.com/minio/minio-go/v7/pkg/s3utils"
+ "github.com/minio/minio-go/v7/pkg/set"
+ "github.com/minio/minio/internal/grid"
+ xnet "github.com/minio/pkg/v3/net"
+
+ "github.com/minio/minio/internal/amztime"
+ "github.com/minio/minio/internal/config/dns"
+ "github.com/minio/minio/internal/crypto"
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/minio/internal/mcontext"
+)
+
+const (
+ // Maximum allowed form data field values. 64MiB is a guessed practical value
+ // which is more than enough to accommodate any form data fields and headers.
+ requestFormDataSize = 64 * humanize.MiByte
+
+ // For any HTTP request, request body should be not more than 16GiB + requestFormDataSize
+ // where, 16GiB is the maximum allowed object size for object upload.
+ requestMaxBodySize = globalMaxObjectSize + requestFormDataSize
+
+ // Maximum size for http headers - See: https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html
+ maxHeaderSize = 8 * 1024
+
+ // Maximum size for user-defined metadata - See: https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html
+ maxUserDataSize = 2 * 1024
+
+ // maxBuckets upto 500000 for any MinIO deployment.
+ maxBuckets = 500 * 1000
+)
+
+// ReservedMetadataPrefix is the prefix of a metadata key which
+// is reserved and for internal use only.
+const (
+ ReservedMetadataPrefix = "X-Minio-Internal-"
+ ReservedMetadataPrefixLower = "x-minio-internal-"
+)
+
+// containsReservedMetadata returns true if the http.Header contains
+// keys which are treated as metadata but are reserved for internal use
+// and must not set by clients
+func containsReservedMetadata(header http.Header) bool {
+ for key := range header {
+ if _, ok := validSSEReplicationHeaders[key]; ok {
+ return false
+ }
+ if stringsHasPrefixFold(key, ReservedMetadataPrefix) {
+ return true
+ }
+ }
+ return false
+}
+
+// isHTTPHeaderSizeTooLarge returns true if the provided
+// header is larger than 8 KB or the user-defined metadata
+// is larger than 2 KB.
+func isHTTPHeaderSizeTooLarge(header http.Header) bool {
+ var size, usersize int
+ for key := range header {
+ length := len(key) + len(header.Get(key))
+ size += length
+ for _, prefix := range userMetadataKeyPrefixes {
+ if stringsHasPrefixFold(key, prefix) {
+ usersize += length
+ break
+ }
+ }
+ if usersize > maxUserDataSize || size > maxHeaderSize {
+ return true
+ }
+ }
+ return false
+}
+
+// Limits body and header to specific allowed maximum limits as per S3/MinIO API requirements.
+func setRequestLimitMiddleware(h http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
+
+ // Reject unsupported reserved metadata first before validation.
+ if containsReservedMetadata(r.Header) {
+ if ok {
+ tc.FuncName = "handler.ValidRequest"
+ tc.ResponseRecorder.LogErrBody = true
+ }
+
+ defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
+ writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrUnsupportedMetadata), r.URL)
+ return
+ }
+
+ if isHTTPHeaderSizeTooLarge(r.Header) {
+ if ok {
+ tc.FuncName = "handler.ValidRequest"
+ tc.ResponseRecorder.LogErrBody = true
+ }
+
+ defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
+ writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrMetadataTooLarge), r.URL)
+ atomic.AddUint64(&globalHTTPStats.rejectedRequestsHeader, 1)
+ return
+ }
+ // Restricting read data to a given maximum length
+ r.Body = http.MaxBytesReader(w, r.Body, requestMaxBodySize)
+ h.ServeHTTP(w, r)
+ })
+}
+
+// Reserved bucket.
+const (
+ minioReservedBucket = "minio"
+ minioReservedBucketPath = SlashSeparator + minioReservedBucket
+
+ loginPathPrefix = SlashSeparator + "login"
+)
+
+func guessIsBrowserReq(r *http.Request) bool {
+ aType := getRequestAuthType(r)
+ return strings.Contains(r.Header.Get("User-Agent"), "Mozilla") &&
+ globalBrowserEnabled && aType == authTypeAnonymous
+}
+
+func setBrowserRedirectMiddleware(h http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ read := r.Method == http.MethodGet || r.Method == http.MethodHead
+ // Re-direction is handled specifically for browser requests.
+ if !guessIsHealthCheckReq(r) && guessIsBrowserReq(r) && read && globalBrowserRedirect {
+ // Fetch the redirect location if any.
+ if u := getRedirectLocation(r); u != nil {
+ // Employ a temporary re-direct.
+ http.Redirect(w, r, u.String(), http.StatusTemporaryRedirect)
+ return
+ }
+ }
+ h.ServeHTTP(w, r)
+ })
+}
+
+var redirectPrefixes = map[string]struct{}{
+ "favicon-16x16.png": {},
+ "favicon-32x32.png": {},
+ "favicon-96x96.png": {},
+ "index.html": {},
+ minioReservedBucket: {},
+}
+
+// Fetch redirect location if urlPath satisfies certain
+// criteria. Some special names are considered to be
+// redirectable, this is purely internal function and
+// serves only limited purpose on redirect-handler for
+// browser requests.
+func getRedirectLocation(r *http.Request) *xnet.URL {
+ resource, err := getResource(r.URL.Path, r.Host, globalDomainNames)
+ if err != nil {
+ return nil
+ }
+ bucket, _ := path2BucketObject(resource)
+ _, redirect := redirectPrefixes[path.Clean(bucket)]
+ if redirect || resource == slashSeparator {
+ if globalBrowserRedirectURL != nil {
+ return globalBrowserRedirectURL
+ }
+ xhost, err := xnet.ParseHost(r.Host)
+ if err != nil {
+ return nil
+ }
+ return &xnet.URL{
+ Host: net.JoinHostPort(xhost.Name, globalMinioConsolePort),
+ Scheme: func() string {
+ scheme := "http"
+ if r.TLS != nil {
+ scheme = "https"
+ }
+ return scheme
+ }(),
+ }
+ }
+ return nil
+}
+
+// guessIsHealthCheckReq - returns true if incoming request looks
+// like healthCheck request
+func guessIsHealthCheckReq(req *http.Request) bool {
+ if req == nil {
+ return false
+ }
+ aType := getRequestAuthType(req)
+ return aType == authTypeAnonymous && (req.Method == http.MethodGet || req.Method == http.MethodHead) &&
+ (req.URL.Path == healthCheckPathPrefix+healthCheckLivenessPath ||
+ req.URL.Path == healthCheckPathPrefix+healthCheckReadinessPath ||
+ req.URL.Path == healthCheckPathPrefix+healthCheckClusterPath ||
+ req.URL.Path == healthCheckPathPrefix+healthCheckClusterReadPath)
+}
+
+// guessIsMetricsReq - returns true if incoming request looks
+// like metrics request
+func guessIsMetricsReq(req *http.Request) bool {
+ if req == nil {
+ return false
+ }
+ aType := getRequestAuthType(req)
+ return (aType == authTypeAnonymous || aType == authTypeJWT) &&
+ req.URL.Path == minioReservedBucketPath+prometheusMetricsPathLegacy ||
+ req.URL.Path == minioReservedBucketPath+prometheusMetricsV2ClusterPath ||
+ req.URL.Path == minioReservedBucketPath+prometheusMetricsV2NodePath ||
+ req.URL.Path == minioReservedBucketPath+prometheusMetricsV2BucketPath ||
+ req.URL.Path == minioReservedBucketPath+prometheusMetricsV2ResourcePath ||
+ strings.HasPrefix(req.URL.Path, minioReservedBucketPath+metricsV3Path)
+}
+
+// guessIsRPCReq - returns true if the request is for an RPC endpoint.
+func guessIsRPCReq(req *http.Request) bool {
+ if req == nil {
+ return false
+ }
+ if req.Method == http.MethodGet && req.URL != nil {
+ switch req.URL.Path {
+ case grid.RoutePath, grid.RouteLockPath:
+ return true
+ }
+ }
+
+ return (req.Method == http.MethodPost || req.Method == http.MethodGet) &&
+ strings.HasPrefix(req.URL.Path, minioReservedBucketPath+SlashSeparator)
+}
+
+// Check to allow access to the reserved "bucket" `/minio` for Admin
+// API requests.
+func isAdminReq(r *http.Request) bool {
+ return strings.HasPrefix(r.URL.Path, adminPathPrefix)
+}
+
+// Check to allow access to the reserved "bucket" `/minio` for KMS
+// API requests.
+func isKMSReq(r *http.Request) bool {
+ return strings.HasPrefix(r.URL.Path, kmsPathPrefix)
+}
+
+// Supported Amz date headers.
+var amzDateHeaders = []string{
+ // Do not change this order, x-amz-date value should be
+ // validated first.
+ "x-amz-date",
+ "date",
+}
+
+// parseAmzDateHeader - parses supported amz date headers, in
+// supported amz date formats.
+func parseAmzDateHeader(req *http.Request) (time.Time, APIErrorCode) {
+ for _, amzDateHeader := range amzDateHeaders {
+ amzDateStr := req.Header.Get(amzDateHeader)
+ if amzDateStr != "" {
+ t, err := amztime.Parse(amzDateStr)
+ if err != nil {
+ return time.Time{}, ErrMalformedDate
+ }
+ return t, ErrNone
+ }
+ }
+ // Date header missing.
+ return time.Time{}, ErrMissingDateHeader
+}
+
+func hasBadHost(host string) error {
+ if globalIsCICD && strings.TrimSpace(host) == "" {
+ // under CI/CD test setups ignore empty hosts as invalid hosts
+ return nil
+ }
+ _, err := xnet.ParseHost(host)
+ return err
+}
+
+// Check if the incoming path has bad path components,
+// such as ".." and "."
+func hasBadPathComponent(path string) bool {
+ n := len(path)
+ if n > 32<<10 {
+ // At 32K we are beyond reasonable.
+ return true
+ }
+ i := 0
+ // Skip leading slashes (for sake of Windows \ is included as well)
+ for i < n && (path[i] == SlashSeparatorChar || path[i] == '\\') {
+ 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
+ case segmentEnd-segmentStart == 1 && path[segmentStart] == '.':
+ return true
+ }
+ i++
+ }
+ return false
+}
+
+// Check if client is sending a malicious request.
+func hasMultipleAuth(r *http.Request) bool {
+ authTypeCount := 0
+ for _, hasValidAuth := range []func(*http.Request) bool{
+ isRequestSignatureV2, isRequestPresignedSignatureV2,
+ isRequestSignatureV4, isRequestPresignedSignatureV4,
+ isRequestJWT, isRequestPostPolicySignatureV4,
+ } {
+ if hasValidAuth(r) {
+ authTypeCount++
+ }
+ }
+ return authTypeCount > 1
+}
+
+// requestValidityHandler validates all the incoming paths for
+// any malicious requests.
+func setRequestValidityMiddleware(h http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
+
+ if err := hasBadHost(r.Host); err != nil {
+ if ok {
+ tc.FuncName = "handler.ValidRequest"
+ tc.ResponseRecorder.LogErrBody = true
+ }
+
+ defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
+ invalidReq := errorCodes.ToAPIErr(ErrInvalidRequest)
+ invalidReq.Description = fmt.Sprintf("%s (%s)", invalidReq.Description, err)
+ writeErrorResponse(r.Context(), w, invalidReq, r.URL)
+ atomic.AddUint64(&globalHTTPStats.rejectedRequestsInvalid, 1)
+ return
+ }
+
+ // Check for bad components in URL path.
+ if hasBadPathComponent(r.URL.Path) {
+ if ok {
+ tc.FuncName = "handler.ValidRequest"
+ tc.ResponseRecorder.LogErrBody = true
+ }
+
+ defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
+ writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInvalidResourceName), r.URL)
+ atomic.AddUint64(&globalHTTPStats.rejectedRequestsInvalid, 1)
+ return
+ }
+ // Check for bad components in URL query values.
+ for k, vv := range r.Form {
+ if k == "delimiter" { // delimiters are allowed to have `.` or `..`
+ continue
+ }
+ if slices.ContainsFunc(vv, hasBadPathComponent) {
+ if ok {
+ tc.FuncName = "handler.ValidRequest"
+ tc.ResponseRecorder.LogErrBody = true
+ }
+
+ defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
+ writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInvalidResourceName), r.URL)
+ atomic.AddUint64(&globalHTTPStats.rejectedRequestsInvalid, 1)
+ return
+ }
+ }
+ if hasMultipleAuth(r) {
+ if ok {
+ tc.FuncName = "handler.Auth"
+ tc.ResponseRecorder.LogErrBody = true
+ }
+
+ defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
+ invalidReq := errorCodes.ToAPIErr(ErrInvalidRequest)
+ invalidReq.Description = fmt.Sprintf("%s (request has multiple authentication types, please use one)", invalidReq.Description)
+ writeErrorResponse(r.Context(), w, invalidReq, r.URL)
+ atomic.AddUint64(&globalHTTPStats.rejectedRequestsInvalid, 1)
+ return
+ }
+ // For all other requests reject access to reserved buckets
+ bucketName, _ := request2BucketObjectName(r)
+ if isMinioReservedBucket(bucketName) || isMinioMetaBucket(bucketName) {
+ if !guessIsRPCReq(r) && !guessIsBrowserReq(r) && !guessIsHealthCheckReq(r) && !guessIsMetricsReq(r) && !isAdminReq(r) && !isKMSReq(r) {
+ if ok {
+ tc.FuncName = "handler.ValidRequest"
+ tc.ResponseRecorder.LogErrBody = true
+ }
+ defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
+ writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrAllAccessDisabled), r.URL)
+ return
+ }
+ } else {
+ // Validate bucket names if it is not empty
+ if bucketName != "" && s3utils.CheckValidBucketNameStrict(bucketName) != nil {
+ if ok {
+ tc.FuncName = "handler.ValidRequest"
+ tc.ResponseRecorder.LogErrBody = true
+ }
+ defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
+ writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInvalidBucketName), r.URL)
+ return
+ }
+ }
+ // Deny SSE-C requests if not made over TLS
+ if !globalIsTLS && (crypto.SSEC.IsRequested(r.Header) || crypto.SSECopy.IsRequested(r.Header)) {
+ if r.Method == http.MethodHead {
+ if ok {
+ tc.FuncName = "handler.ValidRequest"
+ tc.ResponseRecorder.LogErrBody = false
+ }
+
+ defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
+ writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(ErrInsecureSSECustomerRequest))
+ } else {
+ if ok {
+ tc.FuncName = "handler.ValidRequest"
+ tc.ResponseRecorder.LogErrBody = true
+ }
+
+ defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
+ writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInsecureSSECustomerRequest), r.URL)
+ }
+ return
+ }
+ h.ServeHTTP(w, r)
+ })
+}
+
+// setBucketForwardingMiddleware middleware forwards the path style requests
+// on a bucket to the right bucket location, bucket to IP configuration
+// is obtained from centralized etcd configuration service.
+func setBucketForwardingMiddleware(h http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if origin := w.Header().Get("Access-Control-Allow-Origin"); origin == "null" {
+ // This is a workaround change to ensure that "Origin: null"
+ // incoming request to a response back as "*" instead of "null"
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ }
+ if globalDNSConfig == nil || !globalBucketFederation ||
+ guessIsHealthCheckReq(r) || guessIsMetricsReq(r) ||
+ guessIsRPCReq(r) || guessIsLoginSTSReq(r) || isAdminReq(r) || isKMSReq(r) {
+ h.ServeHTTP(w, r)
+ return
+ }
+
+ bucket, object := request2BucketObjectName(r)
+
+ // Requests in federated setups for STS type calls which are
+ // performed at '/' resource should be routed by the muxer,
+ // the assumption is simply such that requests without a bucket
+ // in a federated setup cannot be proxied, so serve them at
+ // current server.
+ if bucket == "" {
+ h.ServeHTTP(w, r)
+ return
+ }
+
+ // MakeBucket requests should be handled at current endpoint
+ if r.Method == http.MethodPut && bucket != "" && object == "" && r.URL.RawQuery == "" {
+ h.ServeHTTP(w, r)
+ return
+ }
+
+ // CopyObject requests should be handled at current endpoint as path style
+ // requests have target bucket and object in URI and source details are in
+ // header fields
+ if r.Method == http.MethodPut && r.Header.Get(xhttp.AmzCopySource) != "" {
+ bucket, object = path2BucketObject(r.Header.Get(xhttp.AmzCopySource))
+ if bucket == "" || object == "" {
+ h.ServeHTTP(w, r)
+ return
+ }
+ }
+ sr, err := globalDNSConfig.Get(bucket)
+ if err != nil {
+ defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
+ if err == dns.ErrNoEntriesFound {
+ writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrNoSuchBucket), r.URL)
+ } else {
+ writeErrorResponse(r.Context(), w, toAPIError(r.Context(), err), r.URL)
+ }
+ return
+ }
+ if globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(sr)...)).IsEmpty() {
+ r.URL.Scheme = "http"
+ if globalIsTLS {
+ r.URL.Scheme = "https"
+ }
+ r.URL.Host = getHostFromSrv(sr)
+ // Make sure we remove any existing headers before
+ // proxying the request to another node.
+ for k := range w.Header() {
+ w.Header().Del(k)
+ }
+ globalForwarder.ServeHTTP(w, r)
+ return
+ }
+ h.ServeHTTP(w, r)
+ })
+}
+
+// addCustomHeadersMiddleware adds various HTTP(S) response headers.
+// Security Headers enable various security protections behaviors in the client's browser.
+func addCustomHeadersMiddleware(h http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ header := w.Header()
+ header.Set("X-XSS-Protection", "1; mode=block") // Prevents against XSS attacks
+ header.Set("X-Content-Type-Options", "nosniff") // Prevent mime-sniff
+ header.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") // HSTS mitigates variants of MITM attacks
+
+ // Previously, this value was set right before a response was sent to
+ // the client. So, logger and Error response XML were not using this
+ // value. This is set here so that this header can be logged as
+ // part of the log entry, Error response XML and auditing.
+ // Set custom headers such as x-amz-request-id for each request.
+ w.Header().Set(xhttp.AmzRequestID, mustGetRequestID(UTCNow()))
+ if globalLocalNodeName != "" {
+ w.Header().Set(xhttp.AmzRequestHostID, globalLocalNodeNameHex)
+ }
+ h.ServeHTTP(w, r)
+ })
+}
+
+// criticalErrorHandler handles panics and fatal errors by
+// `panic(logger.ErrCritical)` as done by `logger.CriticalIf`.
+//
+// It should be always the first / highest HTTP handler.
+func setCriticalErrorHandler(h http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ defer func() {
+ if rec := recover(); rec == logger.ErrCritical { // handle
+ stack := debug.Stack()
+ logger.Error("critical: \"%s %s\": %v\n%s", r.Method, r.URL, rec, string(stack))
+ writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInternalError), r.URL)
+ return
+ } else if rec != nil {
+ stack := debug.Stack()
+ logger.Error("panic: \"%s %s\": %v\n%s", r.Method, r.URL, rec, string(stack))
+ // Try to write an error response, upstream may not have written header.
+ writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInternalError), r.URL)
+ return
+ }
+ }()
+ h.ServeHTTP(w, r)
+ })
+}
+
+// setUploadForwardingMiddleware middleware forwards multiparts requests
+// in a site replication setup to peer that initiated the upload
+func setUploadForwardingMiddleware(h http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if !globalSiteReplicationSys.isEnabled() ||
+ guessIsHealthCheckReq(r) || guessIsMetricsReq(r) ||
+ guessIsRPCReq(r) || guessIsLoginSTSReq(r) || isAdminReq(r) {
+ h.ServeHTTP(w, r)
+ return
+ }
+
+ bucket, object := request2BucketObjectName(r)
+ uploadID := r.Form.Get(xhttp.UploadID)
+
+ if bucket != "" && object != "" && uploadID != "" {
+ deplID, err := getDeplIDFromUpload(uploadID)
+ if err != nil {
+ h.ServeHTTP(w, r)
+ return
+ }
+ remote, self := globalSiteReplicationSys.getPeerForUpload(deplID)
+ if self {
+ h.ServeHTTP(w, r)
+ return
+ }
+ r.URL.Scheme = remote.EndpointURL.Scheme
+ r.URL.Host = remote.EndpointURL.Host
+ // Make sure we remove any existing headers before
+ // proxying the request to another node.
+ for k := range w.Header() {
+ w.Header().Del(k)
+ }
+ ctx := newContext(r, w, "SiteReplicationUploadForwarding")
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+ globalForwarder.ServeHTTP(w, r)
+ return
+ }
+ h.ServeHTTP(w, r)
+ })
+}
diff --git a/cmd/generic-handlers_contrib.go b/cmd/generic-handlers_contrib.go
new file mode 100644
index 00000000..2a2b07e9
--- /dev/null
+++ b/cmd/generic-handlers_contrib.go
@@ -0,0 +1,32 @@
+/*
+ * MinIO Object Storage (c) 2021 MinIO, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cmd
+
+import (
+ "net/http"
+ "strings"
+)
+
+// guessIsLoginSTSReq - returns true if incoming request is Login STS user
+func guessIsLoginSTSReq(req *http.Request) bool {
+ if req == nil {
+ return false
+ }
+ return strings.HasPrefix(req.URL.Path, loginPathPrefix) ||
+ (req.Method == http.MethodPost && req.URL.Path == SlashSeparator &&
+ getRequestAuthType(req) == authTypeSTS)
+}
diff --git a/cmd/generic-handlers_test.go b/cmd/generic-handlers_test.go
new file mode 100644
index 00000000..c2d71ae9
--- /dev/null
+++ b/cmd/generic-handlers_test.go
@@ -0,0 +1,210 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strconv"
+ "strings"
+ "testing"
+
+ "github.com/minio/minio/internal/crypto"
+ "github.com/minio/minio/internal/grid"
+ xhttp "github.com/minio/minio/internal/http"
+)
+
+// Tests request guess function for net/rpc requests.
+func TestGuessIsRPC(t *testing.T) {
+ if guessIsRPCReq(nil) {
+ t.Fatal("Unexpected return for nil request")
+ }
+
+ u, err := url.Parse("http://localhost:9000/minio/lock")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ r := &http.Request{
+ Proto: "HTTP/1.0",
+ Method: http.MethodPost,
+ URL: u,
+ }
+ if !guessIsRPCReq(r) {
+ t.Fatal("Test shouldn't fail for a possible net/rpc request.")
+ }
+ r = &http.Request{
+ Proto: "HTTP/1.1",
+ Method: http.MethodGet,
+ URL: u,
+ }
+ if !guessIsRPCReq(r) {
+ t.Fatal("Test shouldn't fail for a possible net/rpc request.")
+ }
+ r = &http.Request{
+ Proto: "HTTP/1.1",
+ Method: http.MethodGet,
+ URL: &url.URL{Path: grid.RoutePath},
+ }
+ if !guessIsRPCReq(r) {
+ t.Fatal("Grid RPC path not detected")
+ }
+ r = &http.Request{
+ Proto: "HTTP/1.1",
+ Method: http.MethodGet,
+ URL: &url.URL{Path: grid.RouteLockPath},
+ }
+ if !guessIsRPCReq(r) {
+ t.Fatal("Grid RPC path not detected")
+ }
+}
+
+var isHTTPHeaderSizeTooLargeTests = []struct {
+ header http.Header
+ shouldFail bool
+}{
+ {header: generateHeader(0, 0), shouldFail: false},
+ {header: generateHeader(1024, 0), shouldFail: false},
+ {header: generateHeader(2048, 0), shouldFail: false},
+ {header: generateHeader(8*1024+1, 0), shouldFail: true},
+ {header: generateHeader(0, 1024), shouldFail: false},
+ {header: generateHeader(0, 2048), shouldFail: true},
+ {header: generateHeader(0, 2048+1), shouldFail: true},
+}
+
+func generateHeader(size, usersize int) http.Header {
+ header := http.Header{}
+ for i := range size {
+ header.Set(strconv.Itoa(i), "")
+ }
+ userlength := 0
+ for i := 0; userlength < usersize; i++ {
+ userlength += len(userMetadataKeyPrefixes[0] + strconv.Itoa(i))
+ header.Set(userMetadataKeyPrefixes[0]+strconv.Itoa(i), "")
+ }
+ return header
+}
+
+func TestIsHTTPHeaderSizeTooLarge(t *testing.T) {
+ for i, test := range isHTTPHeaderSizeTooLargeTests {
+ if res := isHTTPHeaderSizeTooLarge(test.header); res != test.shouldFail {
+ t.Errorf("Test %d: Expected %v got %v", i, res, test.shouldFail)
+ }
+ }
+}
+
+var containsReservedMetadataTests = []struct {
+ header http.Header
+ shouldFail bool
+}{
+ {
+ header: http.Header{"X-Minio-Key": []string{"value"}},
+ },
+ {
+ header: http.Header{crypto.MetaIV: []string{"iv"}},
+ shouldFail: false,
+ },
+ {
+ header: http.Header{crypto.MetaAlgorithm: []string{crypto.InsecureSealAlgorithm}},
+ shouldFail: false,
+ },
+ {
+ header: http.Header{crypto.MetaSealedKeySSEC: []string{"mac"}},
+ shouldFail: false,
+ },
+ {
+ header: http.Header{ReservedMetadataPrefix + "Key": []string{"value"}},
+ shouldFail: true,
+ },
+}
+
+func TestContainsReservedMetadata(t *testing.T) {
+ for _, test := range containsReservedMetadataTests {
+ t.Run("", func(t *testing.T) {
+ contains := containsReservedMetadata(test.header)
+ if contains && !test.shouldFail {
+ t.Errorf("contains reserved header but should not fail")
+ } else if !contains && test.shouldFail {
+ t.Errorf("does not contain reserved header but failed")
+ }
+ })
+ }
+}
+
+var sseTLSHandlerTests = []struct {
+ URL *url.URL
+ Header http.Header
+ IsTLS, ShouldFail bool
+}{
+ {URL: &url.URL{}, Header: http.Header{}, IsTLS: false, ShouldFail: false}, // 0
+ {URL: &url.URL{}, Header: http.Header{xhttp.AmzServerSideEncryptionCustomerAlgorithm: []string{"AES256"}}, IsTLS: false, ShouldFail: true}, // 1
+ {URL: &url.URL{}, Header: http.Header{xhttp.AmzServerSideEncryptionCustomerAlgorithm: []string{"AES256"}}, IsTLS: true, ShouldFail: false}, // 2
+ {URL: &url.URL{}, Header: http.Header{xhttp.AmzServerSideEncryptionCustomerKey: []string{""}}, IsTLS: true, ShouldFail: false}, // 3
+ {URL: &url.URL{}, Header: http.Header{xhttp.AmzServerSideEncryptionCopyCustomerAlgorithm: []string{""}}, IsTLS: false, ShouldFail: true}, // 4
+}
+
+func TestSSETLSHandler(t *testing.T) {
+ defer func(isSSL bool) { globalIsTLS = isSSL }(globalIsTLS) // reset globalIsTLS after test
+
+ var okHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ }
+ for i, test := range sseTLSHandlerTests {
+ globalIsTLS = test.IsTLS
+
+ w := httptest.NewRecorder()
+ r := new(http.Request)
+ r.Header = test.Header
+ r.URL = test.URL
+
+ h := setRequestValidityMiddleware(okHandler)
+ h.ServeHTTP(w, r)
+
+ switch {
+ case test.ShouldFail && w.Code == http.StatusOK:
+ t.Errorf("Test %d: should fail but status code is HTTP %d", i, w.Code)
+ case !test.ShouldFail && w.Code != http.StatusOK:
+ t.Errorf("Test %d: should not fail but status code is HTTP %d and not 200 OK", i, w.Code)
+ }
+ }
+}
+
+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 b.Loop() {
+ if got := hasBadPathComponent(tt.input); got != tt.want {
+ t.Fatalf("hasBadPathComponent() = %v, want %v", got, tt.want)
+ }
+ }
+ })
+ }
+}
diff --git a/cmd/global-heal.go b/cmd/global-heal.go
new file mode 100644
index 00000000..57cce16e
--- /dev/null
+++ b/cmd/global-heal.go
@@ -0,0 +1,594 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "math/rand"
+ "runtime"
+ "sort"
+ "time"
+
+ "github.com/dustin/go-humanize"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/bucket/lifecycle"
+ 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/color"
+ "github.com/minio/minio/internal/config/storageclass"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/console"
+ "github.com/minio/pkg/v3/wildcard"
+ "github.com/minio/pkg/v3/workers"
+)
+
+const (
+ bgHealingUUID = "0000-0000-0000-0000"
+)
+
+// NewBgHealSequence creates a background healing sequence
+// operation which scans all objects and heal them.
+func newBgHealSequence() *healSequence {
+ reqInfo := &logger.ReqInfo{API: "BackgroundHeal"}
+ ctx, cancelCtx := context.WithCancel(logger.SetReqInfo(GlobalContext, reqInfo))
+
+ hs := madmin.HealOpts{
+ // Remove objects that do not have read-quorum
+ Remove: healDeleteDangling,
+ }
+
+ return &healSequence{
+ startTime: UTCNow(),
+ clientToken: bgHealingUUID,
+ // run-background heal with reserved bucket
+ bucket: minioReservedBucket,
+ settings: hs,
+ currentStatus: healSequenceStatus{
+ Summary: healNotStartedStatus,
+ HealSettings: hs,
+ },
+ cancelCtx: cancelCtx,
+ ctx: ctx,
+ reportProgress: false,
+ scannedItemsMap: make(map[madmin.HealItemType]int64),
+ healedItemsMap: make(map[madmin.HealItemType]int64),
+ healFailedItemsMap: make(map[madmin.HealItemType]int64),
+ }
+}
+
+// getLocalBackgroundHealStatus will return the heal status of the local node
+func getLocalBackgroundHealStatus(ctx context.Context, o ObjectLayer) (madmin.BgHealState, bool) {
+ if globalBackgroundHealState == nil {
+ return madmin.BgHealState{}, false
+ }
+
+ bgSeq, ok := globalBackgroundHealState.getHealSequenceByToken(bgHealingUUID)
+ if !ok {
+ return madmin.BgHealState{}, false
+ }
+
+ status := madmin.BgHealState{
+ ScannedItemsCount: bgSeq.getScannedItemsCount(),
+ }
+
+ healDisksMap := map[string]struct{}{}
+ for _, ep := range getLocalDisksToHeal() {
+ healDisksMap[ep.String()] = struct{}{}
+ }
+
+ if o == nil {
+ healing := globalBackgroundHealState.getLocalHealingDisks()
+ for _, disk := range healing {
+ status.HealDisks = append(status.HealDisks, disk.Endpoint)
+ }
+
+ return status, true
+ }
+
+ si := o.LocalStorageInfo(ctx, true)
+
+ indexed := make(map[string][]madmin.Disk)
+ for _, disk := range si.Disks {
+ setIdx := fmt.Sprintf("%d-%d", disk.PoolIndex, disk.SetIndex)
+ indexed[setIdx] = append(indexed[setIdx], disk)
+ }
+
+ for id, disks := range indexed {
+ ss := madmin.SetStatus{
+ ID: id,
+ SetIndex: disks[0].SetIndex,
+ PoolIndex: disks[0].PoolIndex,
+ }
+ for _, disk := range disks {
+ ss.Disks = append(ss.Disks, disk)
+ if disk.Healing {
+ ss.HealStatus = "Healing"
+ ss.HealPriority = "high"
+ status.HealDisks = append(status.HealDisks, disk.Endpoint)
+ }
+ }
+ sortDisks(ss.Disks)
+ status.Sets = append(status.Sets, ss)
+ }
+ sort.Slice(status.Sets, func(i, j int) bool {
+ return status.Sets[i].ID < status.Sets[j].ID
+ })
+
+ backendInfo := o.BackendInfo()
+ status.SCParity = make(map[string]int)
+ status.SCParity[storageclass.STANDARD] = backendInfo.StandardSCParity
+ status.SCParity[storageclass.RRS] = backendInfo.RRSCParity
+
+ return status, true
+}
+
+type healEntryResult struct {
+ bytes uint64
+ success bool
+ skipped bool
+ entryDone bool
+ name string
+}
+
+// healErasureSet lists and heals all objects in a specific erasure set
+func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string, tracker *healingTracker) error {
+ bgSeq, found := globalBackgroundHealState.getHealSequenceByToken(bgHealingUUID)
+ if !found {
+ return errors.New("no local healing sequence initialized, unable to heal the drive")
+ }
+
+ scanMode := madmin.HealNormalScan
+
+ // Make sure to copy since `buckets slice`
+ // is modified in place by tracker.
+ healBuckets := make([]string, len(buckets))
+ copy(healBuckets, buckets)
+
+ objAPI := newObjectLayerFn()
+ if objAPI == nil {
+ return errServerNotInitialized
+ }
+
+ started := tracker.Started
+ if started.IsZero() || started.Equal(timeSentinel) {
+ healingLogIf(ctx, fmt.Errorf("unexpected tracker healing start time found: %v", started))
+ started = time.Time{}
+ }
+
+ // Final tracer update before quitting
+ defer func() {
+ tracker.setObject("")
+ tracker.setBucket("")
+ healingLogIf(ctx, tracker.update(ctx))
+ }()
+
+ for _, bucket := range healBuckets {
+ if err := bgSeq.healBucket(objAPI, bucket, true); err != nil {
+ // Log bucket healing error if any, we shall retry again.
+ healingLogIf(ctx, err)
+ }
+ }
+
+ info, err := tracker.disk.DiskInfo(ctx, DiskInfoOptions{})
+ if err != nil {
+ return fmt.Errorf("unable to get disk information before healing it: %w", err)
+ }
+
+ var numHealers uint64
+
+ if numCores := uint64(runtime.GOMAXPROCS(0)); info.NRRequests > numCores {
+ numHealers = numCores / 4
+ } else {
+ numHealers = info.NRRequests / 4
+ }
+ if numHealers < 4 {
+ numHealers = 4
+ }
+ // allow overriding this value as well..
+ if v := globalHealConfig.GetWorkers(); v > 0 {
+ numHealers = uint64(v)
+ }
+
+ healingLogEvent(ctx, "Healing drive '%s' - use %d parallel workers.", tracker.disk.String(), numHealers)
+
+ jt, _ := workers.New(int(numHealers))
+
+ healEntryDone := func(name string) healEntryResult {
+ return healEntryResult{
+ entryDone: true,
+ name: name,
+ }
+ }
+
+ healEntrySuccess := func(sz uint64) healEntryResult {
+ return healEntryResult{
+ bytes: sz,
+ success: true,
+ }
+ }
+
+ healEntryFailure := func(sz uint64) healEntryResult {
+ return healEntryResult{
+ bytes: sz,
+ }
+ }
+
+ healEntrySkipped := func(sz uint64) healEntryResult {
+ return healEntryResult{
+ bytes: sz,
+ skipped: true,
+ }
+ }
+
+ // Collect updates to tracker from concurrent healEntry calls
+ results := make(chan healEntryResult, 1000)
+ quitting := make(chan struct{})
+ defer func() {
+ close(results)
+ <-quitting
+ }()
+
+ go func() {
+ for res := range results {
+ if res.entryDone {
+ tracker.setObject(res.name)
+ if time.Since(tracker.getLastUpdate()) > time.Minute {
+ healingLogIf(ctx, tracker.update(ctx))
+ }
+ continue
+ }
+
+ tracker.updateProgress(res.success, res.skipped, res.bytes)
+ }
+
+ healingLogIf(ctx, tracker.update(ctx))
+ close(quitting)
+ }()
+
+ var retErr error
+
+ // Heal all buckets with all objects
+ for _, bucket := range healBuckets {
+ if tracker.isHealed(bucket) {
+ continue
+ }
+
+ var forwardTo string
+ // If we resume to the same bucket, forward to last known item.
+ b := tracker.getBucket()
+ if b == bucket {
+ forwardTo = tracker.getObject()
+ }
+ if b != "" {
+ // Reset to where last bucket ended if resuming.
+ tracker.resume()
+ }
+ tracker.setObject("")
+ tracker.setBucket(bucket)
+ // Heal current bucket again in case if it is failed
+ // in the beginning of erasure set healing
+ if err := bgSeq.healBucket(objAPI, bucket, true); err != nil {
+ // Set this such that when we return this function
+ // we let the caller retry this disk again for the
+ // buckets that failed healing.
+ retErr = err
+ healingLogIf(ctx, err)
+ continue
+ }
+
+ var (
+ vc *versioning.Versioning
+ lc *lifecycle.Lifecycle
+ lr objectlock.Retention
+ rcfg *replication.Config
+ )
+
+ if !isMinioMetaBucketName(bucket) {
+ vc, err = globalBucketVersioningSys.Get(bucket)
+ if err != nil {
+ retErr = err
+ healingLogIf(ctx, err)
+ continue
+ }
+ // Check if the current bucket has a configured lifecycle policy
+ lc, err = globalLifecycleSys.Get(bucket)
+ if err != nil && !errors.Is(err, BucketLifecycleNotFound{Bucket: bucket}) {
+ retErr = err
+ healingLogIf(ctx, err)
+ continue
+ }
+ // Check if bucket is object locked.
+ lr, err = globalBucketObjectLockSys.Get(bucket)
+ if err != nil {
+ retErr = err
+ healingLogIf(ctx, err)
+ continue
+ }
+ rcfg, err = getReplicationConfig(ctx, bucket)
+ if err != nil {
+ retErr = err
+ healingLogIf(ctx, err)
+ continue
+ }
+ }
+
+ if serverDebugLog {
+ console.Debugf(color.Green("healDrive:")+" healing bucket %s content on %s erasure set\n",
+ bucket, humanize.Ordinal(er.setIndex+1))
+ }
+
+ disks, _, healing := er.getOnlineDisksWithHealingAndInfo(true)
+ if len(disks) == healing {
+ // All drives in this erasure set were reformatted for some reasons, abort healing and mark it as successful
+ healingLogIf(ctx, errors.New("all drives are in healing state, aborting.."))
+ return nil
+ }
+
+ disks = disks[:len(disks)-healing] // healing drives are always at the end of the list
+
+ if len(disks) < er.setDriveCount/2 {
+ return fmt.Errorf("not enough drives (found=%d, healing=%d, total=%d) are available to heal `%s`", len(disks), healing, er.setDriveCount, tracker.disk.String())
+ }
+
+ rand.Shuffle(len(disks), func(i, j int) {
+ disks[i], disks[j] = disks[j], disks[i]
+ })
+
+ filterLifecycle := func(bucket, object string, fi FileInfo) bool {
+ if lc == nil {
+ return false
+ }
+ versioned := vc != nil && vc.Versioned(object)
+ objInfo := fi.ToObjectInfo(bucket, object, versioned)
+
+ evt := evalActionFromLifecycle(ctx, *lc, lr, rcfg, objInfo)
+ switch {
+ case evt.Action.DeleteRestored(): // if restored copy has expired,delete it synchronously
+ applyExpiryOnTransitionedObject(ctx, newObjectLayerFn(), objInfo, evt, lcEventSrc_Heal)
+ return false
+ case evt.Action.Delete():
+ globalExpiryState.enqueueByDays(objInfo, evt, lcEventSrc_Heal)
+ return true
+ default:
+ return false
+ }
+ }
+
+ send := func(result healEntryResult) bool {
+ select {
+ case <-ctx.Done():
+ if !contextCanceled(ctx) {
+ healingLogIf(ctx, ctx.Err())
+ }
+ return false
+ case results <- result:
+ bgSeq.countScanned(madmin.HealItemObject)
+ return true
+ }
+ }
+
+ // Note: updates from healEntry to tracker must be sent on results channel.
+ healEntry := func(bucket string, entry metaCacheEntry) {
+ defer jt.Give()
+
+ if entry.name == "" && len(entry.metadata) == 0 {
+ // ignore entries that don't have metadata.
+ return
+ }
+ if entry.isDir() {
+ // ignore healing entry.name's with `/` suffix.
+ return
+ }
+
+ // We might land at .metacache, .trash, .multipart
+ // no need to heal them skip, only when bucket
+ // is '.minio.sys'
+ if bucket == minioMetaBucket {
+ if wildcard.Match("buckets/*/.metacache/*", entry.name) {
+ return
+ }
+ if wildcard.Match("tmp/.trash/*", entry.name) {
+ return
+ }
+ if wildcard.Match("multipart/*", entry.name) {
+ return
+ }
+ }
+
+ // erasureObjects layer needs object names to be encoded
+ encodedEntryName := encodeDirObject(entry.name)
+
+ var result healEntryResult
+ fivs, err := entry.fileInfoVersions(bucket)
+ if err != nil {
+ res, err := er.HealObject(ctx, bucket, encodedEntryName, "",
+ madmin.HealOpts{
+ ScanMode: scanMode,
+ Remove: healDeleteDangling,
+ })
+ if err != nil {
+ if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
+ // queueing happens across namespace, ignore
+ // objects that are not found.
+ return
+ }
+ result = healEntryFailure(0)
+ bgSeq.countFailed(madmin.HealItemObject)
+ healingLogIf(ctx, fmt.Errorf("unable to heal object %s/%s: %w", bucket, entry.name, err))
+ } else {
+ bgSeq.countHealed(madmin.HealItemObject)
+ result = healEntrySuccess(uint64(res.ObjectSize))
+ }
+
+ send(result)
+ return
+ }
+
+ var versionNotFound int
+ for _, version := range fivs.Versions {
+ // Ignore healing a version if:
+ // - It is uploaded after the drive healing is started
+ // - An object that is already expired by ILM rule.
+ if !started.IsZero() && version.ModTime.After(started) || filterLifecycle(bucket, version.Name, version) {
+ versionNotFound++
+ if !send(healEntrySkipped(uint64(version.Size))) {
+ return
+ }
+ continue
+ }
+
+ res, err := er.HealObject(ctx, bucket, encodedEntryName,
+ version.VersionID, madmin.HealOpts{
+ ScanMode: scanMode,
+ Remove: healDeleteDangling,
+ })
+ if err != nil {
+ if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
+ // queueing happens across namespace, ignore
+ // objects that are not found.
+ versionNotFound++
+ continue
+ }
+ } else {
+ // Look for the healing results
+ if res.After.Drives[tracker.DiskIndex].State != madmin.DriveStateOk {
+ err = fmt.Errorf("unexpected after heal state: %s", res.After.Drives[tracker.DiskIndex].State)
+ }
+ }
+
+ if err == nil {
+ bgSeq.countHealed(madmin.HealItemObject)
+ result = healEntrySuccess(uint64(version.Size))
+ } else {
+ bgSeq.countFailed(madmin.HealItemObject)
+ result = healEntryFailure(uint64(version.Size))
+ if version.VersionID != "" {
+ healingLogIf(ctx, fmt.Errorf("unable to heal object %s/%s (version-id=%s): %w",
+ bucket, version.Name, version.VersionID, err))
+ } else {
+ healingLogIf(ctx, fmt.Errorf("unable to heal object %s/%s: %w",
+ bucket, version.Name, err))
+ }
+ }
+
+ if !send(result) {
+ return
+ }
+ }
+
+ // All versions resulted in 'ObjectNotFound/VersionNotFound'
+ if versionNotFound == len(fivs.Versions) {
+ return
+ }
+
+ send(healEntryDone(entry.name))
+
+ // Wait and proceed if there are active requests
+ waitForLowHTTPReq()
+ }
+
+ // How to resolve partial results.
+ resolver := metadataResolutionParams{
+ dirQuorum: 1,
+ objQuorum: 1,
+ bucket: bucket,
+ }
+
+ err = listPathRaw(ctx, listPathRawOptions{
+ disks: disks,
+ bucket: bucket,
+ recursive: true,
+ forwardTo: forwardTo,
+ minDisks: 1,
+ reportNotFound: false,
+ agreed: func(entry metaCacheEntry) {
+ jt.Take()
+ go healEntry(bucket, entry)
+ },
+ partial: func(entries metaCacheEntries, _ []error) {
+ entry, ok := entries.resolve(&resolver)
+ if !ok {
+ // check if we can get one entry at least
+ // proceed to heal nonetheless.
+ entry, _ = entries.firstFound()
+ }
+ jt.Take()
+ go healEntry(bucket, *entry)
+ },
+ finished: func(errs []error) {
+ success := countErrs(errs, nil)
+ if success < len(disks)/2+1 {
+ retErr = fmt.Errorf("one or more errors reported during listing: %v", errors.Join(errs...))
+ }
+ },
+ })
+ jt.Wait() // synchronize all the concurrent heal jobs
+ if err != nil {
+ // Set this such that when we return this function
+ // we let the caller retry this disk again for the
+ // buckets it failed to list.
+ retErr = err
+ }
+
+ if retErr != nil {
+ healingLogIf(ctx, fmt.Errorf("listing failed with: %v on bucket: %v", retErr, bucket))
+ continue
+ }
+
+ select {
+ // If context is canceled don't mark as done...
+ case <-ctx.Done():
+ return ctx.Err()
+ default:
+ tracker.bucketDone(bucket)
+ healingLogIf(ctx, tracker.update(ctx))
+ }
+ }
+ if retErr != nil {
+ return retErr
+ }
+
+ // Last sanity check
+ if len(tracker.QueuedBuckets) > 0 {
+ return fmt.Errorf("not all buckets were healed: %v", tracker.QueuedBuckets)
+ }
+
+ return nil
+}
+
+func healBucket(bucket string, scan madmin.HealScanMode) error {
+ // Get background heal sequence to send elements to heal
+ bgSeq, ok := globalBackgroundHealState.getHealSequenceByToken(bgHealingUUID)
+ if ok {
+ return bgSeq.queueHealTask(healSource{bucket: bucket}, madmin.HealItemBucket)
+ }
+ return nil
+}
+
+// healObject sends the given object/version to the background healing workers
+func healObject(bucket, object, versionID string, scan madmin.HealScanMode) error {
+ // Get background heal sequence to send elements to heal
+ bgSeq, ok := globalBackgroundHealState.getHealSequenceByToken(bgHealingUUID)
+ if ok {
+ return bgSeq.healObject(bucket, object, versionID, scan)
+ }
+ return nil
+}
diff --git a/cmd/globals.go b/cmd/globals.go
new file mode 100644
index 00000000..734e6abe
--- /dev/null
+++ b/cmd/globals.go
@@ -0,0 +1,491 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "crypto/x509"
+ "errors"
+ "net/http"
+ "os"
+ "sync"
+ "time"
+
+ consoleapi "github.com/minio/console/api"
+ "github.com/minio/dnscache"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio-go/v7"
+ "github.com/minio/minio-go/v7/pkg/set"
+ "github.com/minio/minio/internal/bpool"
+ "github.com/minio/minio/internal/bucket/bandwidth"
+ "github.com/minio/minio/internal/config"
+ "github.com/minio/minio/internal/config/browser"
+ "github.com/minio/minio/internal/grid"
+ "github.com/minio/minio/internal/handlers"
+ "github.com/minio/minio/internal/kms"
+ "go.uber.org/atomic"
+
+ "github.com/dustin/go-humanize"
+ "github.com/minio/minio/internal/auth"
+ "github.com/minio/minio/internal/config/callhome"
+ "github.com/minio/minio/internal/config/compress"
+ "github.com/minio/minio/internal/config/dns"
+ "github.com/minio/minio/internal/config/drive"
+ idplugin "github.com/minio/minio/internal/config/identity/plugin"
+ polplugin "github.com/minio/minio/internal/config/policy/plugin"
+ "github.com/minio/minio/internal/config/storageclass"
+ "github.com/minio/minio/internal/config/subnet"
+ xhttp "github.com/minio/minio/internal/http"
+ etcd "go.etcd.io/etcd/client/v3"
+
+ levent "github.com/minio/minio/internal/config/lambda/event"
+ "github.com/minio/minio/internal/event"
+ "github.com/minio/minio/internal/pubsub"
+ "github.com/minio/pkg/v3/certs"
+ "github.com/minio/pkg/v3/env"
+ xnet "github.com/minio/pkg/v3/net"
+)
+
+// minio configuration related constants.
+const (
+ GlobalMinioDefaultPort = "9000"
+
+ globalMinioDefaultRegion = ""
+ // This is a sha256 output of ``arn:aws:iam::minio:user/admin``,
+ // this is kept in present form to be compatible with S3 owner ID
+ // requirements -
+ //
+ // ```
+ // The canonical user ID is the Amazon S3–only concept.
+ // It is 64-character obfuscated version of the account ID.
+ // ```
+ // http://docs.aws.amazon.com/AmazonS3/latest/dev/example-walkthroughs-managing-access-example4.html
+ globalMinioDefaultOwnerID = "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4"
+ globalMinioDefaultStorageClass = "STANDARD"
+ globalWindowsOSName = "windows"
+ globalMacOSName = "darwin"
+ globalMinioModeFS = "mode-server-fs"
+ globalMinioModeErasureSD = "mode-server-xl-single"
+ globalMinioModeErasure = "mode-server-xl"
+ globalMinioModeDistErasure = "mode-server-distributed-xl"
+ globalDirSuffix = "__XLDIR__"
+ globalDirSuffixWithSlash = globalDirSuffix + slashSeparator
+
+ // Add new global values here.
+)
+
+const (
+ // Limit fields size (except file) to 1Mib since Policy document
+ // can reach that size according to https://aws.amazon.com/articles/1434
+ maxFormFieldSize = int64(1 * humanize.MiByte)
+
+ // The maximum allowed time difference between the incoming request
+ // date and server date during signature verification.
+ globalMaxSkewTime = 15 * time.Minute // 15 minutes skew allowed.
+
+ // GlobalStaleUploadsExpiry - Expiry duration after which the uploads in multipart,
+ // tmp directory are deemed stale.
+ GlobalStaleUploadsExpiry = time.Hour * 24 // 24 hrs.
+
+ // GlobalStaleUploadsCleanupInterval - Cleanup interval when the stale uploads cleanup is initiated.
+ GlobalStaleUploadsCleanupInterval = time.Hour * 6 // 6 hrs.
+
+ // Refresh interval to update in-memory iam config cache.
+ globalRefreshIAMInterval = 10 * time.Minute
+
+ // Limit of location constraint XML for unauthenticated PUT bucket operations.
+ maxLocationConstraintSize = 3 * humanize.MiByte
+
+ // Maximum size of default bucket encryption configuration allowed
+ maxBucketSSEConfigSize = 1 * humanize.MiByte
+
+ // diskFillFraction is the fraction of a disk we allow to be filled.
+ diskFillFraction = 0.99
+
+ // diskReserveFraction is the fraction of a disk where we will fill other server pools first.
+ // If all pools reach this, we will use all pools with regular placement.
+ diskReserveFraction = 0.15
+
+ // diskAssumeUnknownSize is the size to assume when an unknown size upload is requested.
+ diskAssumeUnknownSize = 1 << 30
+
+ // diskMinInodes is the minimum number of inodes we want free on a disk to perform writes.
+ diskMinInodes = 1000
+
+ // tlsClientSessionCacheSize is the cache size for client sessions.
+ tlsClientSessionCacheSize = 100
+)
+
+func init() {
+ // Injected to prevent circular dependency.
+ pubsub.GetByteBuffer = grid.GetByteBuffer
+}
+
+type poolDisksLayout struct {
+ cmdline string
+ layout [][]string
+}
+
+type disksLayout struct {
+ legacy bool
+ pools []poolDisksLayout
+}
+
+type serverCtxt struct {
+ JSON, Quiet bool
+ Anonymous bool
+ StrictS3Compat bool
+ Addr, ConsoleAddr string
+ ConfigDir, CertsDir string
+ configDirSet, certsDirSet bool
+ Interface string
+
+ RootUser, RootPwd string
+
+ FTP []string
+ SFTP []string
+
+ MemLimit uint64
+
+ UserTimeout time.Duration
+ IdleTimeout time.Duration
+ ReadHeaderTimeout time.Duration
+ MaxIdleConnsPerHost int
+
+ SendBufSize, RecvBufSize int
+ CrossDomainXML string
+ // The layout of disks as interpreted
+ Layout disksLayout
+}
+
+var (
+ // Global user opts context
+ globalServerCtxt serverCtxt
+
+ // Indicates if the running minio server is distributed setup.
+ globalIsDistErasure = false
+
+ // Indicates if the running minio server is an erasure-code backend.
+ globalIsErasure = false
+
+ // Indicates if the running minio server is in single drive XL mode.
+ globalIsErasureSD = false
+
+ // Indicates if server code should go through testing path.
+ globalIsTesting = false
+
+ // This flag is set to 'true' by default
+ globalBrowserEnabled = true
+
+ // Custom browser redirect URL, not set by default
+ // and it is automatically deduced.
+ globalBrowserRedirectURL *xnet.URL
+
+ // Disable redirect, default is enabled.
+ globalBrowserRedirect bool
+
+ // globalBrowserConfig Browser user configurable settings
+ globalBrowserConfig browser.Config
+
+ // This flag is set to 'true' when MINIO_UPDATE env is set to 'off'. Default is false.
+ globalInplaceUpdateDisabled = false
+
+ // Captures site name and region
+ globalSite config.Site
+
+ // MinIO local server address (in `host:port` format)
+ globalMinioAddr = ""
+
+ // MinIO default port, can be changed through command line.
+ globalMinioPort = GlobalMinioDefaultPort
+ globalMinioConsolePort = "13333"
+
+ // Holds the host that was passed using --address
+ globalMinioHost = ""
+ // Holds the host that was passed using --console-address
+ globalMinioConsoleHost = ""
+
+ // Holds the possible host endpoint.
+ globalMinioEndpoint = ""
+ globalMinioEndpointURL *xnet.URL
+
+ // globalConfigSys server config system.
+ globalConfigSys *ConfigSys
+
+ globalNotificationSys *NotificationSys
+
+ globalEventNotifier *EventNotifier
+ globalNotifyTargetList *event.TargetList
+ globalLambdaTargetList *levent.TargetList
+
+ globalBucketMetadataSys *BucketMetadataSys
+ globalBucketMonitor *bandwidth.Monitor
+ globalPolicySys *PolicySys
+ globalIAMSys *IAMSys
+ globalBytePoolCap atomic.Pointer[bpool.BytePoolCap]
+
+ globalLifecycleSys *LifecycleSys
+ globalBucketSSEConfigSys *BucketSSEConfigSys
+ globalBucketTargetSys *BucketTargetSys
+ // globalAPIConfig controls S3 API requests throttling,
+ // healthCheck readiness deadlines and cors settings.
+ globalAPIConfig = apiConfig{listQuorum: "strict", rootAccess: true}
+
+ globalStorageClass storageclass.Config
+
+ globalAuthNPlugin *idplugin.AuthNPlugin
+
+ // CA root certificates, a nil value means system certs pool will be used
+ globalRootCAs *x509.CertPool
+
+ // IsSSL indicates if the server is configured with SSL.
+ globalIsTLS bool
+
+ globalTLSCerts *certs.Manager
+
+ globalHTTPServer *xhttp.Server
+ globalTCPOptions xhttp.TCPOptions
+ globalHTTPServerErrorCh = make(chan error)
+ globalOSSignalCh = make(chan os.Signal, 1)
+
+ // global Trace system to send HTTP request/response
+ // and Storage/OS calls info to registered listeners.
+ globalTrace = pubsub.New[madmin.TraceInfo, madmin.TraceType](8)
+
+ // global Listen system to send S3 API events to registered listeners
+ globalHTTPListen = pubsub.New[event.Event, pubsub.Mask](0)
+
+ // global console system to send console logs to
+ // registered listeners
+ globalConsoleSys *HTTPConsoleLoggerSys
+
+ // All unique drives for this deployment
+ globalEndpoints EndpointServerPools
+ // All unique nodes for this deployment
+ globalNodes []Node
+
+ // The name of this local node, fetched from arguments
+ globalLocalNodeName string
+ globalLocalNodeNameHex string
+ globalNodeNamesHex = make(map[string]struct{})
+
+ // The global subnet config
+ globalSubnetConfig subnet.Config
+
+ // The global callhome config
+ globalCallhomeConfig callhome.Config
+
+ // The global drive config
+ globalDriveConfig drive.Config
+
+ // Global server's network statistics
+ globalConnStats = newConnStats()
+
+ // Global HTTP request statistics
+ globalHTTPStats = newHTTPStats()
+
+ // Global bucket network and API statistics
+ globalBucketConnStats = newBucketConnStats()
+ globalBucketHTTPStats = newBucketHTTPStats()
+
+ // Time when the server is started
+ globalBootTime = UTCNow()
+
+ globalActiveCred auth.Credentials
+ globalNodeAuthToken string
+ globalSiteReplicatorCred siteReplicatorCred
+
+ // Captures if root credentials are set via ENV.
+ globalCredViaEnv bool
+
+ globalPublicCerts []*x509.Certificate
+
+ globalDomainNames []string // Root domains for virtual host style requests
+ globalDomainIPs set.StringSet // Root domain IP address(s) for a distributed MinIO deployment
+
+ globalOperationTimeout = newDynamicTimeout(10*time.Minute, 5*time.Minute) // default timeout for general ops
+ globalDeleteOperationTimeout = newDynamicTimeout(5*time.Minute, 1*time.Minute) // default time for delete ops
+
+ globalBucketObjectLockSys *BucketObjectLockSys
+ globalBucketQuotaSys *BucketQuotaSys
+ globalBucketVersioningSys *BucketVersioningSys
+
+ // Allocated etcd endpoint for config and bucket DNS.
+ globalEtcdClient *etcd.Client
+
+ // Cluster replication manager.
+ globalSiteReplicationSys SiteReplicationSys
+
+ // Cluster replication resync metrics
+ globalSiteResyncMetrics *siteResyncMetrics
+
+ // Is set to true when Bucket federation is requested
+ // and is 'true' when etcdConfig.PathPrefix is empty
+ globalBucketFederation bool
+
+ // Allocated DNS config wrapper over etcd client.
+ globalDNSConfig dns.Store
+
+ // GlobalKMS initialized KMS configuration
+ GlobalKMS *kms.KMS
+
+ // Common lock for various subsystems performing the leader tasks
+ globalLeaderLock *sharedLock
+
+ // Auto-Encryption, if enabled, turns any non-SSE-C request
+ // into an SSE-S3 request. If enabled a valid, non-empty KMS
+ // configuration must be present.
+ globalAutoEncryption bool
+
+ // Is compression enabled?
+ globalCompressConfigMu sync.Mutex
+ globalCompressConfig compress.Config
+
+ // Some standard object extensions which we strictly dis-allow for compression.
+ standardExcludeCompressExtensions = []string{".gz", ".bz2", ".rar", ".zip", ".7z", ".xz", ".mp4", ".mkv", ".mov", ".jpg", ".png", ".gif"}
+
+ // Some standard content-types which we strictly dis-allow for compression.
+ standardExcludeCompressContentTypes = []string{"video/*", "audio/*", "application/zip", "application/x-gzip", "application/x-zip-compressed", " application/x-compress", "application/x-spoon"}
+
+ // AuthZ Plugin system.
+ globalAuthZPlugin *polplugin.AuthZPlugin
+
+ // Deployment ID - unique per deployment
+ globalDeploymentIDPtr atomic.Pointer[string]
+ globalDeploymentID = func() string {
+ ptr := globalDeploymentIDPtr.Load()
+ if ptr == nil {
+ return ""
+ }
+ return *ptr
+ }
+
+ globalAllHealState = newHealState(GlobalContext, true)
+
+ // The always present healing routine ready to heal objects
+ globalBackgroundHealRoutine = newHealRoutine()
+ globalBackgroundHealState = newHealState(GlobalContext, false)
+
+ globalMRFState = newMRFState()
+
+ // If writes to FS backend should be O_SYNC.
+ globalFSOSync bool
+
+ globalProxyEndpoints []ProxyEndpoint
+
+ globalInternodeTransport http.RoundTripper
+
+ globalRemoteTargetTransport http.RoundTripper
+
+ globalDNSCache = &dnscache.Resolver{
+ Timeout: 5 * time.Second,
+ }
+
+ globalForwarder *handlers.Forwarder
+
+ globalTierConfigMgr *TierConfigMgr
+
+ globalConsoleSrv *consoleapi.Server
+
+ // handles service freeze or un-freeze S3 API calls.
+ globalServiceFreeze atomic.Value
+
+ // Only needed for tracking
+ globalServiceFreezeCnt int32
+ globalServiceFreezeMu sync.Mutex // Updates.
+
+ // Map of local drives to this node, this is set during server startup,
+ // disk reconnect and mutated by HealFormat. Hold globalLocalDrivesMu to access.
+ globalLocalDrivesMap map[string]StorageAPI
+ globalLocalDrivesMu sync.RWMutex
+
+ globalDriveMonitoring = env.Get("_MINIO_DRIVE_ACTIVE_MONITORING", config.EnableOn) == config.EnableOn
+
+ // Is MINIO_CI_CD set?
+ globalIsCICD bool
+
+ globalRootDiskThreshold uint64
+
+ // Used for collecting stats for netperf
+ globalNetPerfMinDuration = time.Second * 10
+ globalNetPerfRX netPerfRX
+ globalSiteNetPerfRX netPerfRX
+ globalObjectPerfBucket = "minio-perf-test-tmp-bucket"
+ globalObjectPerfUserMetadata = "X-Amz-Meta-Minio-Object-Perf" // Clients can set this to bypass S3 API service freeze. Used by object pref tests.
+
+ // MinIO version unix timestamp
+ globalVersionUnix uint64
+
+ // MinIO client
+ globalMinioClient *minio.Client
+
+ // Public key for subnet confidential information
+ subnetAdminPublicKey = []byte("-----BEGIN PUBLIC KEY-----\nMIIBCgKCAQEAyC+ol5v0FP+QcsR6d1KypR/063FInmNEFsFzbEwlHQyEQN3O7kNI\nwVDN1vqp1wDmJYmv4VZGRGzfFw1q+QV7K1TnysrEjrqpVxfxzDQCoUadAp8IxLLc\ns2fjyDNxnZjoC6fTID9C0khKnEa5fPZZc3Ihci9SiCGkPmyUyCGVSxWXIKqL2Lrj\nyDc0pGeEhWeEPqw6q8X2jvTC246tlzqpDeNsPbcv2KblXRcKniQNbBrizT37CKHQ\nM6hc9kugrZbFuo8U5/4RQvZPJnx/DVjLDyoKo2uzuVQs4s+iBrA5sSSLp8rPED/3\n6DgWw3e244Dxtrg972dIT1IOqgn7KUJzVQIDAQAB\n-----END PUBLIC KEY-----")
+ subnetAdminPublicKeyDev = []byte("-----BEGIN PUBLIC KEY-----\nMIIBCgKCAQEArhQYXQd6zI4uagtVfthAPOt6i4AYHnEWCoNeAovM4MNl42I9uQFh\n3VHkbWj9Gpx9ghf6PgRgK+8FcFvy+StmGcXpDCiFywXX24uNhcZjscX1C4Esk0BW\nidfI2eXYkOlymD4lcK70SVgJvC693Qa7Z3FE1KU8Nfv2bkxEE4bzOkojX9t6a3+J\nR8X6Z2U8EMlH1qxJPgiPogELhWP0qf2Lq7GwSAflo1Tj/ytxvD12WrnE0Rrj/8yP\nSnp7TbYm91KocKMExlmvx3l2XPLxeU8nf9U0U+KOmorejD3MDMEPF+tlk9LB3JWP\nZqYYe38rfALVTn4RVJriUcNOoEpEyC0WEwIDAQAB\n-----END PUBLIC KEY-----")
+
+ // dynamic sleeper to avoid thundering herd for trash folder expunge routine
+ deleteCleanupSleeper = newDynamicSleeper(5, 25*time.Millisecond, false)
+
+ // dynamic sleeper for multipart expiration routine
+ deleteMultipartCleanupSleeper = newDynamicSleeper(5, 25*time.Millisecond, false)
+
+ // Is MINIO_SYNC_BOOT set?
+ globalEnableSyncBoot bool
+
+ // Contains NIC interface name used for internode communication
+ globalInternodeInterface string
+ globalInternodeInterfaceOnce sync.Once
+
+ // Set last client perf extra time (get lock, and validate)
+ globalLastClientPerfExtraTime int64
+
+ // Captures all batch jobs metrics globally
+ globalBatchJobsMetrics batchJobMetrics
+
+ // Indicates if server was started as `--address ":0"`
+ globalDynamicAPIPort bool
+
+ // Add new variable global values here.
+)
+
+var globalAuthPluginMutex sync.Mutex
+
+func newGlobalAuthNPluginFn() *idplugin.AuthNPlugin {
+ globalAuthPluginMutex.Lock()
+ defer globalAuthPluginMutex.Unlock()
+ return globalAuthNPlugin
+}
+
+func newGlobalAuthZPluginFn() *polplugin.AuthZPlugin {
+ globalAuthPluginMutex.Lock()
+ defer globalAuthPluginMutex.Unlock()
+ return globalAuthZPlugin
+}
+
+func setGlobalAuthNPlugin(authn *idplugin.AuthNPlugin) {
+ globalAuthPluginMutex.Lock()
+ globalAuthNPlugin = authn
+ globalAuthPluginMutex.Unlock()
+}
+
+func setGlobalAuthZPlugin(authz *polplugin.AuthZPlugin) {
+ globalAuthPluginMutex.Lock()
+ globalAuthZPlugin = authz
+ globalAuthPluginMutex.Unlock()
+}
+
+var errSelfTestFailure = errors.New("self test failed. unsafe to start server")
diff --git a/cmd/grid.go b/cmd/grid.go
new file mode 100644
index 00000000..0b442267
--- /dev/null
+++ b/cmd/grid.go
@@ -0,0 +1,107 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "crypto/tls"
+ "sync/atomic"
+
+ "github.com/minio/minio/internal/crypto"
+ "github.com/minio/minio/internal/grid"
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/minio/internal/rest"
+)
+
+// globalGrid is the global grid manager.
+var globalGrid atomic.Pointer[grid.Manager]
+
+// globalLockGrid is the global lock grid manager.
+var globalLockGrid atomic.Pointer[grid.Manager]
+
+// globalGridStart is a channel that will block startup of grid connections until closed.
+var globalGridStart = make(chan struct{})
+
+// globalLockGridStart is a channel that will block startup of lock grid connections until closed.
+var globalLockGridStart = make(chan struct{})
+
+func initGlobalGrid(ctx context.Context, eps EndpointServerPools) error {
+ hosts, local := eps.GridHosts()
+ lookupHost := globalDNSCache.LookupHost
+ g, err := grid.NewManager(ctx, grid.ManagerOptions{
+ // Pass Dialer for websocket grid, make sure we do not
+ // provide any DriveOPTimeout() function, as that is not
+ // useful over persistent connections.
+ Dialer: grid.ConnectWS(
+ grid.ContextDialer(xhttp.DialContextWithLookupHost(lookupHost, xhttp.NewInternodeDialContext(rest.DefaultTimeout, globalTCPOptions.ForWebsocket()))),
+ newCachedAuthToken(),
+ &tls.Config{
+ RootCAs: globalRootCAs,
+ CipherSuites: crypto.TLSCiphers(),
+ CurvePreferences: crypto.TLSCurveIDs(),
+ }),
+ Local: local,
+ Hosts: hosts,
+ AuthToken: validateStorageRequestToken,
+ AuthFn: newCachedAuthToken(),
+ BlockConnect: globalGridStart,
+ // Record incoming and outgoing bytes.
+ Incoming: globalConnStats.incInternodeInputBytes,
+ Outgoing: globalConnStats.incInternodeOutputBytes,
+ TraceTo: globalTrace,
+ RoutePath: grid.RoutePath,
+ })
+ if err != nil {
+ return err
+ }
+ globalGrid.Store(g)
+ return nil
+}
+
+func initGlobalLockGrid(ctx context.Context, eps EndpointServerPools) error {
+ hosts, local := eps.GridHosts()
+ lookupHost := globalDNSCache.LookupHost
+ g, err := grid.NewManager(ctx, grid.ManagerOptions{
+ // Pass Dialer for websocket grid, make sure we do not
+ // provide any DriveOPTimeout() function, as that is not
+ // useful over persistent connections.
+ Dialer: grid.ConnectWSWithRoutePath(
+ grid.ContextDialer(xhttp.DialContextWithLookupHost(lookupHost, xhttp.NewInternodeDialContext(rest.DefaultTimeout, globalTCPOptions.ForWebsocket()))),
+ newCachedAuthToken(),
+ &tls.Config{
+ RootCAs: globalRootCAs,
+ CipherSuites: crypto.TLSCiphers(),
+ CurvePreferences: crypto.TLSCurveIDs(),
+ }, grid.RouteLockPath),
+ Local: local,
+ Hosts: hosts,
+ AuthToken: validateStorageRequestToken,
+ AuthFn: newCachedAuthToken(),
+ BlockConnect: globalGridStart,
+ // Record incoming and outgoing bytes.
+ Incoming: globalConnStats.incInternodeInputBytes,
+ Outgoing: globalConnStats.incInternodeOutputBytes,
+ TraceTo: globalTrace,
+ RoutePath: grid.RouteLockPath,
+ })
+ if err != nil {
+ return err
+ }
+ globalLockGrid.Store(g)
+ return nil
+}
diff --git a/cmd/handler-api.go b/cmd/handler-api.go
new file mode 100644
index 00000000..09790a60
--- /dev/null
+++ b/cmd/handler-api.go
@@ -0,0 +1,420 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "math"
+ "net/http"
+ "os"
+ "runtime"
+ "slices"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/dustin/go-humanize"
+ "github.com/shirou/gopsutil/v3/mem"
+
+ "github.com/minio/minio/internal/config/api"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/minio/internal/mcontext"
+)
+
+type apiConfig struct {
+ mu sync.RWMutex
+
+ requestsPool chan struct{}
+ clusterDeadline time.Duration
+ listQuorum string
+ corsAllowOrigins []string
+ replicationPriority string
+ replicationMaxWorkers int
+ replicationMaxLWorkers int
+ transitionWorkers int
+
+ staleUploadsExpiry time.Duration
+ staleUploadsCleanupInterval time.Duration
+ deleteCleanupInterval time.Duration
+ enableODirect bool
+ gzipObjects bool
+ rootAccess bool
+ syncEvents bool
+ objectMaxVersions int64
+}
+
+const (
+ cgroupV1MemLimitFile = "/sys/fs/cgroup/memory/memory.limit_in_bytes"
+ cgroupV2MemLimitFile = "/sys/fs/cgroup/memory.max"
+)
+
+func cgroupMemLimit() (limit uint64) {
+ buf, err := os.ReadFile(cgroupV2MemLimitFile)
+ if err != nil {
+ buf, err = os.ReadFile(cgroupV1MemLimitFile)
+ }
+ if err != nil {
+ return 0
+ }
+ limit, err = strconv.ParseUint(strings.TrimSpace(string(buf)), 10, 64)
+ if err != nil {
+ // The kernel can return valid but non integer values
+ // but still, no need to interpret more
+ return 0
+ }
+ if limit >= 100*humanize.TiByte {
+ // No limit set, or unreasonably high. Ignore
+ return 0
+ }
+ return limit
+}
+
+func availableMemory() (available uint64) {
+ available = 2048 * blockSizeV2 * 2 // Default to 4 GiB when we can't find the limits.
+
+ if runtime.GOOS == "linux" {
+ // Honor cgroup limits if set.
+ limit := cgroupMemLimit()
+ if limit > 0 {
+ // A valid value is found, return its 90%
+ available = (limit * 9) / 10
+ return available
+ }
+ } // for all other platforms limits are based on virtual memory.
+
+ memStats, err := mem.VirtualMemory()
+ if err != nil {
+ return available
+ }
+
+ // A valid value is available return its 90%
+ available = (memStats.Available * 9) / 10
+ return available
+}
+
+func (t *apiConfig) init(cfg api.Config, setDriveCounts []int, legacy bool) {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ clusterDeadline := cfg.ClusterDeadline
+ if clusterDeadline == 0 {
+ clusterDeadline = 10 * time.Second
+ }
+ t.clusterDeadline = clusterDeadline
+ corsAllowOrigin := cfg.CorsAllowOrigin
+ if len(corsAllowOrigin) == 0 {
+ corsAllowOrigin = []string{"*"}
+ }
+ t.corsAllowOrigins = corsAllowOrigin
+
+ var apiRequestsMaxPerNode int
+ if cfg.RequestsMax <= 0 {
+ maxSetDrives := slices.Max(setDriveCounts)
+
+ // Returns 75% of max memory allowed
+ maxMem := globalServerCtxt.MemLimit
+
+ // max requests per node is calculated as
+ // total_ram / ram_per_request
+ blockSize := xioutil.LargeBlock + xioutil.SmallBlock
+ if legacy {
+ // ram_per_request is (1MiB+32KiB) * driveCount \
+ // + 2 * 10MiB (default erasure block size v1) + 2 * 1MiB (default erasure block size v2)
+ apiRequestsMaxPerNode = int(maxMem / uint64(maxSetDrives*blockSize+int(blockSizeV1*2+blockSizeV2*2)))
+ } else {
+ // ram_per_request is (1MiB+32KiB) * driveCount \
+ // + 2 * 1MiB (default erasure block size v2)
+ apiRequestsMaxPerNode = int(maxMem / uint64(maxSetDrives*blockSize+int(blockSizeV2*2)))
+ }
+ } else {
+ apiRequestsMaxPerNode = cfg.RequestsMax
+ if n := totalNodeCount(); n > 0 {
+ apiRequestsMaxPerNode /= n
+ }
+ }
+
+ if globalIsDistErasure {
+ logger.Info("Configured max API requests per node based on available memory: %d", apiRequestsMaxPerNode)
+ }
+
+ if cap(t.requestsPool) != apiRequestsMaxPerNode {
+ // Only replace if needed.
+ // Existing requests will use the previous limit,
+ // but new requests will use the new limit.
+ // There will be a short overlap window,
+ // but this shouldn't last long.
+ t.requestsPool = make(chan struct{}, apiRequestsMaxPerNode)
+ }
+ listQuorum := cfg.ListQuorum
+ if listQuorum == "" {
+ listQuorum = "strict"
+ }
+ t.listQuorum = listQuorum
+ if r := globalReplicationPool.GetNonBlocking(); r != nil &&
+ (cfg.ReplicationPriority != t.replicationPriority || cfg.ReplicationMaxWorkers != t.replicationMaxWorkers || cfg.ReplicationMaxLWorkers != t.replicationMaxLWorkers) {
+ r.ResizeWorkerPriority(cfg.ReplicationPriority, cfg.ReplicationMaxWorkers, cfg.ReplicationMaxLWorkers)
+ }
+ t.replicationPriority = cfg.ReplicationPriority
+ t.replicationMaxWorkers = cfg.ReplicationMaxWorkers
+ t.replicationMaxLWorkers = cfg.ReplicationMaxLWorkers
+
+ // N B api.transition_workers will be deprecated
+ if globalTransitionState != nil {
+ globalTransitionState.UpdateWorkers(cfg.TransitionWorkers)
+ }
+ t.transitionWorkers = cfg.TransitionWorkers
+
+ t.staleUploadsExpiry = cfg.StaleUploadsExpiry
+ t.deleteCleanupInterval = cfg.DeleteCleanupInterval
+ t.enableODirect = cfg.EnableODirect
+ t.gzipObjects = cfg.GzipObjects
+ t.rootAccess = cfg.RootAccess
+ t.syncEvents = cfg.SyncEvents
+ t.objectMaxVersions = cfg.ObjectMaxVersions
+
+ if t.staleUploadsCleanupInterval != cfg.StaleUploadsCleanupInterval {
+ t.staleUploadsCleanupInterval = cfg.StaleUploadsCleanupInterval
+
+ // signal that cleanup interval has changed
+ select {
+ case staleUploadsCleanupIntervalChangedCh <- struct{}{}:
+ default: // in case the channel is blocked...
+ }
+ }
+}
+
+func (t *apiConfig) odirectEnabled() bool {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+
+ return t.enableODirect
+}
+
+func (t *apiConfig) shouldGzipObjects() bool {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+
+ return t.gzipObjects
+}
+
+func (t *apiConfig) permitRootAccess() bool {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+
+ return t.rootAccess
+}
+
+func (t *apiConfig) getListQuorum() string {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+
+ if t.listQuorum == "" {
+ return "strict"
+ }
+
+ return t.listQuorum
+}
+
+func (t *apiConfig) getCorsAllowOrigins() []string {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+
+ if len(t.corsAllowOrigins) == 0 {
+ return []string{"*"}
+ }
+
+ corsAllowOrigins := make([]string, len(t.corsAllowOrigins))
+ copy(corsAllowOrigins, t.corsAllowOrigins)
+ return corsAllowOrigins
+}
+
+func (t *apiConfig) getStaleUploadsCleanupInterval() time.Duration {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+
+ if t.staleUploadsCleanupInterval == 0 {
+ return 6 * time.Hour // default 6 hours
+ }
+
+ return t.staleUploadsCleanupInterval
+}
+
+func (t *apiConfig) getStaleUploadsExpiry() time.Duration {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+
+ if t.staleUploadsExpiry == 0 {
+ return 24 * time.Hour // default 24 hours
+ }
+
+ return t.staleUploadsExpiry
+}
+
+func (t *apiConfig) getDeleteCleanupInterval() time.Duration {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+
+ if t.deleteCleanupInterval == 0 {
+ return 5 * time.Minute // every 5 minutes
+ }
+
+ return t.deleteCleanupInterval
+}
+
+func (t *apiConfig) getClusterDeadline() time.Duration {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+
+ if t.clusterDeadline == 0 {
+ return 10 * time.Second
+ }
+
+ return t.clusterDeadline
+}
+
+func (t *apiConfig) getRequestsPoolCapacity() int {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+
+ return cap(t.requestsPool)
+}
+
+func (t *apiConfig) getRequestsPool() chan struct{} {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+
+ if t.requestsPool == nil {
+ return nil
+ }
+
+ return t.requestsPool
+}
+
+// maxClients throttles the S3 API calls
+func maxClients(f http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ globalHTTPStats.incS3RequestsIncoming()
+
+ if r.Header.Get(globalObjectPerfUserMetadata) == "" {
+ if val := globalServiceFreeze.Load(); val != nil {
+ if unlock, ok := val.(chan struct{}); ok && unlock != nil {
+ // Wait until unfrozen.
+ select {
+ case <-unlock:
+ case <-r.Context().Done():
+ // if client canceled we don't need to wait here forever.
+ return
+ }
+ }
+ }
+ }
+
+ globalHTTPStats.addRequestsInQueue(1)
+ pool := globalAPIConfig.getRequestsPool()
+ if pool == nil {
+ globalHTTPStats.addRequestsInQueue(-1)
+ f.ServeHTTP(w, r)
+ return
+ }
+
+ if tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt); ok {
+ tc.FuncName = "s3.MaxClients"
+ }
+
+ w.Header().Set("X-RateLimit-Limit", strconv.Itoa(cap(pool)))
+ w.Header().Set("X-RateLimit-Remaining", strconv.Itoa(cap(pool)-len(pool)))
+
+ ctx := r.Context()
+ select {
+ case pool <- struct{}{}:
+ defer func() { <-pool }()
+ globalHTTPStats.addRequestsInQueue(-1)
+ if contextCanceled(ctx) {
+ w.WriteHeader(499)
+ return
+ }
+ f.ServeHTTP(w, r)
+ case <-r.Context().Done():
+ globalHTTPStats.addRequestsInQueue(-1)
+ // When the client disconnects before getting the S3 handler
+ // status code response, set the status code to 499 so this request
+ // will be properly audited and traced.
+ w.WriteHeader(499)
+ default:
+ globalHTTPStats.addRequestsInQueue(-1)
+ if contextCanceled(ctx) {
+ w.WriteHeader(499)
+ return
+ }
+ // Send a http timeout message
+ writeErrorResponse(ctx, w,
+ errorCodes.ToAPIErr(ErrTooManyRequests),
+ r.URL)
+ }
+ }
+}
+
+func (t *apiConfig) getReplicationOpts() replicationPoolOpts {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+
+ if t.replicationPriority == "" {
+ return replicationPoolOpts{
+ Priority: "auto",
+ MaxWorkers: WorkerMaxLimit,
+ MaxLWorkers: LargeWorkerCount,
+ }
+ }
+
+ return replicationPoolOpts{
+ Priority: t.replicationPriority,
+ MaxWorkers: t.replicationMaxWorkers,
+ MaxLWorkers: t.replicationMaxLWorkers,
+ }
+}
+
+func (t *apiConfig) getTransitionWorkers() int {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+
+ if t.transitionWorkers <= 0 {
+ return runtime.GOMAXPROCS(0) / 2
+ }
+
+ return t.transitionWorkers
+}
+
+func (t *apiConfig) isSyncEventsEnabled() bool {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+
+ return t.syncEvents
+}
+
+func (t *apiConfig) getObjectMaxVersions() int64 {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+
+ if t.objectMaxVersions <= 0 {
+ // defaults to 'IntMax' when unset.
+ return math.MaxInt64
+ }
+
+ return t.objectMaxVersions
+}
diff --git a/cmd/handler-utils.go b/cmd/handler-utils.go
new file mode 100644
index 00000000..7752cfec
--- /dev/null
+++ b/cmd/handler-utils.go
@@ -0,0 +1,504 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/textproto"
+ "regexp"
+ "strings"
+ "sync/atomic"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/auth"
+ "github.com/minio/minio/internal/handlers"
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/minio/internal/mcontext"
+ xnet "github.com/minio/pkg/v3/net"
+)
+
+const (
+ copyDirective = "COPY"
+ replaceDirective = "REPLACE"
+ accessDirective = "ACCESS"
+)
+
+// Parses location constraint from the incoming reader.
+func parseLocationConstraint(r *http.Request) (location string, s3Error APIErrorCode) {
+ // If the request has no body with content-length set to 0,
+ // we do not have to validate location constraint. Bucket will
+ // be created at default region.
+ locationConstraint := createBucketLocationConfiguration{}
+ err := xmlDecoder(r.Body, &locationConstraint, r.ContentLength)
+ if err != nil && r.ContentLength != 0 {
+ internalLogOnceIf(GlobalContext, err, "location-constraint-xml-parsing")
+ // Treat all other failures as XML parsing errors.
+ return "", ErrMalformedXML
+ } // else for both err as nil or io.EOF
+ location = locationConstraint.Location
+ if location == "" {
+ location = globalSite.Region()
+ }
+ if !isValidLocation(location) {
+ return location, ErrInvalidRegion
+ }
+
+ return location, ErrNone
+}
+
+// Validates input location is same as configured region
+// of MinIO server.
+func isValidLocation(location string) bool {
+ region := globalSite.Region()
+ return region == "" || region == location
+}
+
+// Supported headers that needs to be extracted.
+var supportedHeaders = []string{
+ "content-type",
+ "cache-control",
+ "content-language",
+ "content-encoding",
+ "content-disposition",
+ "x-amz-storage-class",
+ xhttp.AmzStorageClass,
+ xhttp.AmzObjectTagging,
+ "expires",
+ xhttp.AmzBucketReplicationStatus,
+ "X-Minio-Replication-Server-Side-Encryption-Sealed-Key",
+ "X-Minio-Replication-Server-Side-Encryption-Seal-Algorithm",
+ "X-Minio-Replication-Server-Side-Encryption-Iv",
+ "X-Minio-Replication-Encrypted-Multipart",
+ "X-Minio-Replication-Actual-Object-Size",
+ ReplicationSsecChecksumHeader,
+ // Add more supported headers here.
+}
+
+// mapping of internal headers to allowed replication headers
+var validSSEReplicationHeaders = map[string]string{
+ "X-Minio-Internal-Server-Side-Encryption-Sealed-Key": "X-Minio-Replication-Server-Side-Encryption-Sealed-Key",
+ "X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm": "X-Minio-Replication-Server-Side-Encryption-Seal-Algorithm",
+ "X-Minio-Internal-Server-Side-Encryption-Iv": "X-Minio-Replication-Server-Side-Encryption-Iv",
+ "X-Minio-Internal-Encrypted-Multipart": "X-Minio-Replication-Encrypted-Multipart",
+ "X-Minio-Internal-Actual-Object-Size": "X-Minio-Replication-Actual-Object-Size",
+ // Add more supported headers here.
+}
+
+// mapping of replication headers to internal headers
+var replicationToInternalHeaders = map[string]string{
+ "X-Minio-Replication-Server-Side-Encryption-Sealed-Key": "X-Minio-Internal-Server-Side-Encryption-Sealed-Key",
+ "X-Minio-Replication-Server-Side-Encryption-Seal-Algorithm": "X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm",
+ "X-Minio-Replication-Server-Side-Encryption-Iv": "X-Minio-Internal-Server-Side-Encryption-Iv",
+ "X-Minio-Replication-Encrypted-Multipart": "X-Minio-Internal-Encrypted-Multipart",
+ "X-Minio-Replication-Actual-Object-Size": "X-Minio-Internal-Actual-Object-Size",
+ ReplicationSsecChecksumHeader: ReplicationSsecChecksumHeader,
+ // Add more supported headers here.
+}
+
+// isDirectiveValid - check if tagging-directive is valid.
+func isDirectiveValid(v string) bool {
+ // Check if set metadata-directive is valid.
+ return isDirectiveCopy(v) || isDirectiveReplace(v)
+}
+
+// Check if the directive COPY is requested.
+func isDirectiveCopy(value string) bool {
+ // By default if directive is not set we
+ // treat it as 'COPY' this function returns true.
+ return value == copyDirective || value == ""
+}
+
+// Check if the directive REPLACE is requested.
+func isDirectiveReplace(value string) bool {
+ return value == replaceDirective
+}
+
+// userMetadataKeyPrefixes contains the prefixes of used-defined metadata keys.
+// All values stored with a key starting with one of the following prefixes
+// must be extracted from the header.
+var userMetadataKeyPrefixes = []string{
+ "x-amz-meta-",
+ "x-minio-meta-",
+}
+
+// extractMetadataFromReq extracts metadata from HTTP header and HTTP queryString.
+func extractMetadataFromReq(ctx context.Context, r *http.Request) (metadata map[string]string, err error) {
+ return extractMetadata(ctx, textproto.MIMEHeader(r.Form), textproto.MIMEHeader(r.Header))
+}
+
+func extractMetadata(ctx context.Context, mimesHeader ...textproto.MIMEHeader) (metadata map[string]string, err error) {
+ metadata = make(map[string]string)
+
+ for _, hdr := range mimesHeader {
+ // Extract all query values.
+ err = extractMetadataFromMime(ctx, hdr, metadata)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // Set content-type to default value if it is not set.
+ if _, ok := metadata[strings.ToLower(xhttp.ContentType)]; !ok {
+ metadata[strings.ToLower(xhttp.ContentType)] = "binary/octet-stream"
+ }
+
+ // https://github.com/google/security-research/security/advisories/GHSA-76wf-9vgp-pj7w
+ for k := range metadata {
+ if equals(k, xhttp.AmzMetaUnencryptedContentLength, xhttp.AmzMetaUnencryptedContentMD5) {
+ delete(metadata, k)
+ }
+ }
+
+ if contentEncoding, ok := metadata[strings.ToLower(xhttp.ContentEncoding)]; ok {
+ contentEncoding = trimAwsChunkedContentEncoding(contentEncoding)
+ if contentEncoding != "" {
+ // Make sure to trim and save the content-encoding
+ // parameter for a streaming signature which is set
+ // to a custom value for example: "aws-chunked,gzip".
+ metadata[strings.ToLower(xhttp.ContentEncoding)] = contentEncoding
+ } else {
+ // Trimmed content encoding is empty when the header
+ // value is set to "aws-chunked" only.
+
+ // Make sure to delete the content-encoding parameter
+ // for a streaming signature which is set to value
+ // for example: "aws-chunked"
+ delete(metadata, strings.ToLower(xhttp.ContentEncoding))
+ }
+ }
+
+ // Success.
+ return metadata, nil
+}
+
+// extractMetadata extracts metadata from map values.
+func extractMetadataFromMime(ctx context.Context, v textproto.MIMEHeader, m map[string]string) error {
+ if v == nil {
+ bugLogIf(ctx, errInvalidArgument)
+ return errInvalidArgument
+ }
+
+ nv := make(textproto.MIMEHeader, len(v))
+ for k, kv := range v {
+ // Canonicalize all headers, to remove any duplicates.
+ nv[http.CanonicalHeaderKey(k)] = kv
+ }
+
+ // Save all supported headers.
+ for _, supportedHeader := range supportedHeaders {
+ value, ok := nv[http.CanonicalHeaderKey(supportedHeader)]
+ if ok {
+ if v, ok := replicationToInternalHeaders[supportedHeader]; ok {
+ m[v] = strings.Join(value, ",")
+ } else {
+ m[supportedHeader] = strings.Join(value, ",")
+ }
+ }
+ }
+
+ for key := range v {
+ for _, prefix := range userMetadataKeyPrefixes {
+ if !stringsHasPrefixFold(key, prefix) {
+ continue
+ }
+ value, ok := nv[http.CanonicalHeaderKey(key)]
+ if ok {
+ m[key] = strings.Join(value, ",")
+ break
+ }
+ }
+ }
+ return nil
+}
+
+// Returns access credentials in the request Authorization header.
+func getReqAccessCred(r *http.Request, region string) (cred auth.Credentials) {
+ cred, _, _ = getReqAccessKeyV4(r, region, serviceS3)
+ if cred.AccessKey == "" {
+ cred, _, _ = getReqAccessKeyV2(r)
+ }
+ return cred
+}
+
+// Extract request params to be sent with event notification.
+func extractReqParams(r *http.Request) map[string]string {
+ if r == nil {
+ return nil
+ }
+
+ region := globalSite.Region()
+ cred := getReqAccessCred(r, region)
+
+ principalID := cred.AccessKey
+ if cred.ParentUser != "" {
+ principalID = cred.ParentUser
+ }
+
+ // Success.
+ m := map[string]string{
+ "region": region,
+ "principalId": principalID,
+ "sourceIPAddress": handlers.GetSourceIP(r),
+ // Add more fields here.
+ }
+ if rangeField := r.Header.Get(xhttp.Range); rangeField != "" {
+ m["range"] = rangeField
+ }
+
+ if _, ok := r.Header[xhttp.MinIOSourceReplicationRequest]; ok {
+ m[xhttp.MinIOSourceReplicationRequest] = ""
+ }
+ return m
+}
+
+// Extract response elements to be sent with event notification.
+func extractRespElements(w http.ResponseWriter) map[string]string {
+ if w == nil {
+ return map[string]string{}
+ }
+ return map[string]string{
+ "requestId": w.Header().Get(xhttp.AmzRequestID),
+ "nodeId": w.Header().Get(xhttp.AmzRequestHostID),
+ "content-length": w.Header().Get(xhttp.ContentLength),
+ // Add more fields here.
+ }
+}
+
+// Trims away `aws-chunked` from the content-encoding header if present.
+// Streaming signature clients can have custom content-encoding such as
+// `aws-chunked,gzip` here we need to only save `gzip`.
+// For more refer http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
+func trimAwsChunkedContentEncoding(contentEnc string) (trimmedContentEnc string) {
+ if contentEnc == "" {
+ return contentEnc
+ }
+ var newEncs []string
+ for enc := range strings.SplitSeq(contentEnc, ",") {
+ if enc != streamingContentEncoding {
+ newEncs = append(newEncs, enc)
+ }
+ }
+ return strings.Join(newEncs, ",")
+}
+
+func collectInternodeStats(f http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ f.ServeHTTP(w, r)
+
+ tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
+ if !ok || tc == nil {
+ return
+ }
+
+ globalConnStats.incInternodeInputBytes(int64(tc.RequestRecorder.Size()))
+ globalConnStats.incInternodeOutputBytes(int64(tc.ResponseRecorder.Size()))
+ }
+}
+
+func collectAPIStats(api string, f http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ resource, err := getResource(r.URL.Path, r.Host, globalDomainNames)
+ if err != nil {
+ defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
+
+ apiErr := errorCodes.ToAPIErr(ErrUnsupportedHostHeader)
+ apiErr.Description = fmt.Sprintf("%s: %v", apiErr.Description, err)
+
+ writeErrorResponse(r.Context(), w, apiErr, r.URL)
+ return
+ }
+
+ bucket, _ := path2BucketObject(resource)
+
+ meta, err := globalBucketMetadataSys.Get(bucket) // check if this bucket exists.
+ countBktStat := bucket != "" && bucket != minioReservedBucket && err == nil && !meta.Created.IsZero()
+ if countBktStat {
+ globalBucketHTTPStats.updateHTTPStats(bucket, api, nil)
+ }
+
+ globalHTTPStats.currentS3Requests.Inc(api)
+ f.ServeHTTP(w, r)
+ globalHTTPStats.currentS3Requests.Dec(api)
+
+ tc, _ := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
+ if tc != nil {
+ globalHTTPStats.updateStats(api, tc.ResponseRecorder)
+ globalConnStats.incS3InputBytes(int64(tc.RequestRecorder.Size()))
+ globalConnStats.incS3OutputBytes(int64(tc.ResponseRecorder.Size()))
+
+ if countBktStat {
+ globalBucketConnStats.incS3InputBytes(bucket, int64(tc.RequestRecorder.Size()))
+ globalBucketConnStats.incS3OutputBytes(bucket, int64(tc.ResponseRecorder.Size()))
+ globalBucketHTTPStats.updateHTTPStats(bucket, api, tc.ResponseRecorder)
+ }
+ }
+ }
+}
+
+// Returns "/bucketName/objectName" for path-style or virtual-host-style requests.
+func getResource(path string, host string, domains []string) (string, error) {
+ if len(domains) == 0 {
+ return path, nil
+ }
+
+ // If virtual-host-style is enabled construct the "resource" properly.
+ xhost, err := xnet.ParseHost(host)
+ if err != nil {
+ return "", err
+ }
+
+ for _, domain := range domains {
+ if xhost.Name == minioReservedBucket+"."+domain {
+ continue
+ }
+ if !strings.HasSuffix(xhost.Name, "."+domain) {
+ continue
+ }
+ bucket := strings.TrimSuffix(xhost.Name, "."+domain)
+ return SlashSeparator + pathJoin(bucket, path), nil
+ }
+ return path, nil
+}
+
+var regexVersion = regexp.MustCompile(`^/minio.*/(v\d+)/.*`)
+
+func extractAPIVersion(r *http.Request) string {
+ if matches := regexVersion.FindStringSubmatch(r.URL.Path); len(matches) > 1 {
+ return matches[1]
+ }
+ return "unknown"
+}
+
+func methodNotAllowedHandler(api string) func(w http.ResponseWriter, r *http.Request) {
+ return errorResponseHandler
+}
+
+// If none of the http routes match respond with appropriate errors
+func errorResponseHandler(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodOptions {
+ return
+ }
+ desc := "Do not upgrade one server at a time - please follow the recommended guidelines mentioned here https://github.com/minio/minio#upgrading-minio for your environment"
+ switch {
+ case strings.HasPrefix(r.URL.Path, peerRESTPrefix):
+ writeErrorResponseString(r.Context(), w, APIError{
+ Code: "XMinioPeerVersionMismatch",
+ Description: desc,
+ HTTPStatusCode: http.StatusUpgradeRequired,
+ }, r.URL)
+ case strings.HasPrefix(r.URL.Path, storageRESTPrefix):
+ writeErrorResponseString(r.Context(), w, APIError{
+ Code: "XMinioStorageVersionMismatch",
+ Description: desc,
+ HTTPStatusCode: http.StatusUpgradeRequired,
+ }, r.URL)
+ case strings.HasPrefix(r.URL.Path, adminPathPrefix):
+ var desc string
+ version := extractAPIVersion(r)
+ switch version {
+ case "v1", madmin.AdminAPIVersionV2:
+ desc = fmt.Sprintf("Server expects client requests with 'admin' API version '%s', found '%s', please upgrade the client to latest releases", madmin.AdminAPIVersion, version)
+ case madmin.AdminAPIVersion:
+ desc = fmt.Sprintf("This 'admin' API is not supported by server in '%s'", getMinioMode())
+ default:
+ desc = fmt.Sprintf("Unexpected client 'admin' API version found '%s', expected '%s', please downgrade the client to older releases", version, madmin.AdminAPIVersion)
+ }
+ writeErrorResponseJSON(r.Context(), w, APIError{
+ Code: "XMinioAdminVersionMismatch",
+ Description: desc,
+ HTTPStatusCode: http.StatusUpgradeRequired,
+ }, r.URL)
+ default:
+ defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
+ defer atomic.AddUint64(&globalHTTPStats.rejectedRequestsInvalid, 1)
+
+ // When we are not running in S3 Express mode, generate appropriate error
+ // for x-amz-write-offset HEADER specified.
+ if _, ok := r.Header[xhttp.AmzWriteOffsetBytes]; ok {
+ tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
+ if ok {
+ tc.FuncName = "s3.AppendObject"
+ tc.ResponseRecorder.LogErrBody = true
+ }
+
+ writeErrorResponse(r.Context(), w, getAPIError(ErrNotImplemented), r.URL)
+ return
+ }
+
+ tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
+ if ok {
+ tc.FuncName = "s3.ValidRequest"
+ tc.ResponseRecorder.LogErrBody = true
+ }
+
+ writeErrorResponse(r.Context(), w, APIError{
+ Code: "BadRequest",
+ Description: fmt.Sprintf("An unsupported API call for method: %s at '%s'",
+ r.Method, r.URL.Path),
+ HTTPStatusCode: http.StatusBadRequest,
+ }, r.URL)
+ }
+}
+
+// gets host name for current node
+func getHostName(r *http.Request) (hostName string) {
+ if globalIsDistErasure {
+ hostName = globalLocalNodeName
+ } else {
+ hostName = r.Host
+ }
+ return hostName
+}
+
+// Proxy any request to an endpoint.
+func proxyRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, ep ProxyEndpoint, returnErr bool) (success bool) {
+ success = true
+
+ // Make sure we remove any existing headers before
+ // proxying the request to another node.
+ for k := range w.Header() {
+ w.Header().Del(k)
+ }
+
+ f := handlers.NewForwarder(&handlers.Forwarder{
+ PassHost: true,
+ RoundTripper: ep.Transport,
+ ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
+ success = false
+ if err != nil && !errors.Is(err, context.Canceled) {
+ proxyLogIf(GlobalContext, err)
+ }
+ if returnErr {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ }
+ },
+ })
+
+ r.URL.Scheme = "http"
+ if globalIsTLS {
+ r.URL.Scheme = "https"
+ }
+
+ r.URL.Host = ep.Host
+ f.ServeHTTP(w, r)
+ return success
+}
diff --git a/cmd/handler-utils_test.go b/cmd/handler-utils_test.go
new file mode 100644
index 00000000..517f93fc
--- /dev/null
+++ b/cmd/handler-utils_test.go
@@ -0,0 +1,203 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "encoding/xml"
+ "io"
+ "net/http"
+ "net/textproto"
+ "os"
+ "reflect"
+ "testing"
+
+ "github.com/minio/minio/internal/config"
+)
+
+// Tests validate bucket LocationConstraint.
+func TestIsValidLocationConstraint(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ obj, fsDir, err := prepareFS(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(fsDir)
+ if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil {
+ t.Fatal(err)
+ }
+
+ // Corrupted XML
+ malformedReq := &http.Request{
+ Body: io.NopCloser(bytes.NewReader([]byte("<>"))),
+ ContentLength: int64(len("<>")),
+ }
+
+ // Not an XML
+ badRequest := &http.Request{
+ Body: io.NopCloser(bytes.NewReader([]byte("garbage"))),
+ ContentLength: int64(len("garbage")),
+ }
+
+ // generates the input request with XML bucket configuration set to the request body.
+ createExpectedRequest := func(req *http.Request, location string) *http.Request {
+ createBucketConfig := createBucketLocationConfiguration{}
+ createBucketConfig.Location = location
+ createBucketConfigBytes, _ := xml.Marshal(createBucketConfig)
+ createBucketConfigBuffer := bytes.NewReader(createBucketConfigBytes)
+ req.Body = io.NopCloser(createBucketConfigBuffer)
+ req.ContentLength = int64(createBucketConfigBuffer.Len())
+ return req
+ }
+
+ testCases := []struct {
+ request *http.Request
+ serverConfigRegion string
+ expectedCode APIErrorCode
+ }{
+ // Test case - 1.
+ {createExpectedRequest(&http.Request{}, "eu-central-1"), globalMinioDefaultRegion, ErrNone},
+ // Test case - 2.
+ // In case of empty request body ErrNone is returned.
+ {createExpectedRequest(&http.Request{}, ""), globalMinioDefaultRegion, ErrNone},
+ // Test case - 3
+ // In case of garbage request body ErrMalformedXML is returned.
+ {badRequest, globalMinioDefaultRegion, ErrMalformedXML},
+ // Test case - 4
+ // In case of invalid XML request body ErrMalformedXML is returned.
+ {malformedReq, globalMinioDefaultRegion, ErrMalformedXML},
+ }
+
+ for i, testCase := range testCases {
+ config.SetRegion(globalServerConfig, testCase.serverConfigRegion)
+ _, actualCode := parseLocationConstraint(testCase.request)
+ if testCase.expectedCode != actualCode {
+ t.Errorf("Test %d: Expected the APIErrCode to be %d, but instead found %d", i+1, testCase.expectedCode, actualCode)
+ }
+ }
+}
+
+// Tests validate metadata extraction from http headers.
+func TestExtractMetadataHeaders(t *testing.T) {
+ testCases := []struct {
+ header http.Header
+ metadata map[string]string
+ shouldFail bool
+ }{
+ // Validate if there a known 'content-type'.
+ {
+ header: http.Header{
+ "Content-Type": []string{"image/png"},
+ },
+ metadata: map[string]string{
+ "content-type": "image/png",
+ },
+ shouldFail: false,
+ },
+ // Validate if there are no keys to extract.
+ {
+ header: http.Header{
+ "Test-1": []string{"123"},
+ },
+ metadata: map[string]string{},
+ shouldFail: false,
+ },
+ // Validate that there are all headers extracted
+ {
+ header: http.Header{
+ "X-Amz-Meta-Appid": []string{"amz-meta"},
+ "X-Minio-Meta-Appid": []string{"minio-meta"},
+ },
+ metadata: map[string]string{
+ "X-Amz-Meta-Appid": "amz-meta",
+ "X-Minio-Meta-Appid": "minio-meta",
+ },
+ shouldFail: false,
+ },
+ // Fail if header key is not in canonicalized form
+ {
+ header: http.Header{
+ "x-amz-meta-appid": []string{"amz-meta"},
+ },
+ metadata: map[string]string{
+ "x-amz-meta-appid": "amz-meta",
+ },
+ shouldFail: false,
+ },
+ // Support multiple values
+ {
+ header: http.Header{
+ "x-amz-meta-key": []string{"amz-meta1", "amz-meta2"},
+ },
+ metadata: map[string]string{
+ "x-amz-meta-key": "amz-meta1,amz-meta2",
+ },
+ shouldFail: false,
+ },
+ // Empty header input returns empty metadata.
+ {
+ header: nil,
+ metadata: nil,
+ shouldFail: true,
+ },
+ }
+
+ // Validate if the extracting headers.
+ for i, testCase := range testCases {
+ metadata := make(map[string]string)
+ err := extractMetadataFromMime(t.Context(), textproto.MIMEHeader(testCase.header), metadata)
+ if err != nil && !testCase.shouldFail {
+ t.Fatalf("Test %d failed to extract metadata: %v", i+1, err)
+ }
+ if err == nil && testCase.shouldFail {
+ t.Fatalf("Test %d should fail, but it passed", i+1)
+ }
+ if err == nil && !reflect.DeepEqual(metadata, testCase.metadata) {
+ t.Fatalf("Test %d failed: Expected \"%#v\", got \"%#v\"", i+1, testCase.metadata, metadata)
+ }
+ }
+}
+
+// Test getResource()
+func TestGetResource(t *testing.T) {
+ testCases := []struct {
+ p string
+ host string
+ domains []string
+ expectedResource string
+ }{
+ {"/a/b/c", "test.mydomain.com", []string{"mydomain.com"}, "/test/a/b/c"},
+ {"/a/b/c", "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:17000", []string{"mydomain.com"}, "/a/b/c"},
+ {"/a/b/c", "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", []string{"mydomain.com"}, "/a/b/c"},
+ {"/a/b/c", "192.168.1.1:9000", []string{"mydomain.com"}, "/a/b/c"},
+ {"/a/b/c", "test.mydomain.com", []string{"notmydomain.com"}, "/a/b/c"},
+ {"/a/b/c", "test.mydomain.com", nil, "/a/b/c"},
+ }
+ for i, test := range testCases {
+ gotResource, err := getResource(test.p, test.host, test.domains)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if gotResource != test.expectedResource {
+ t.Fatalf("test %d: expected %s got %s", i+1, test.expectedResource, gotResource)
+ }
+ }
+}
diff --git a/cmd/hasher.go b/cmd/hasher.go
new file mode 100644
index 00000000..a5e5d7d1
--- /dev/null
+++ b/cmd/hasher.go
@@ -0,0 +1,49 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "crypto/md5"
+ "encoding/hex"
+
+ "github.com/minio/minio/internal/hash/sha256"
+)
+
+// getSHA256Hash returns SHA-256 hash in hex encoding of given data.
+func getSHA256Hash(data []byte) string {
+ return hex.EncodeToString(getSHA256Sum(data))
+}
+
+// getSHA256Hash returns SHA-256 sum of given data.
+func getSHA256Sum(data []byte) []byte {
+ hash := sha256.New()
+ hash.Write(data)
+ return hash.Sum(nil)
+}
+
+// getMD5Sum returns MD5 sum of given data.
+func getMD5Sum(data []byte) []byte {
+ hash := md5.New()
+ hash.Write(data)
+ return hash.Sum(nil)
+}
+
+// getMD5Hash returns MD5 hash in hex encoding of given data.
+func getMD5Hash(data []byte) string {
+ return hex.EncodeToString(getMD5Sum(data))
+}
diff --git a/cmd/healingmetric_string.go b/cmd/healingmetric_string.go
new file mode 100644
index 00000000..012fc7aa
--- /dev/null
+++ b/cmd/healingmetric_string.go
@@ -0,0 +1,25 @@
+// Code generated by "stringer -type=healingMetric -trimprefix=healingMetric erasure-healing.go"; DO NOT EDIT.
+
+package cmd
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[healingMetricBucket-0]
+ _ = x[healingMetricObject-1]
+ _ = x[healingMetricCheckAbandonedParts-2]
+}
+
+const _healingMetric_name = "BucketObjectCheckAbandonedParts"
+
+var _healingMetric_index = [...]uint8{0, 6, 12, 31}
+
+func (i healingMetric) String() string {
+ if i >= healingMetric(len(_healingMetric_index)-1) {
+ return "healingMetric(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _healingMetric_name[_healingMetric_index[i]:_healingMetric_index[i+1]]
+}
diff --git a/cmd/healthcheck-handler.go b/cmd/healthcheck-handler.go
new file mode 100644
index 00000000..12368d1d
--- /dev/null
+++ b/cmd/healthcheck-handler.go
@@ -0,0 +1,212 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "net/http"
+ "strconv"
+ "time"
+
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/minio/internal/kms"
+)
+
+const unavailable = "offline"
+
+func checkHealth(w http.ResponseWriter) ObjectLayer {
+ objLayer := newObjectLayerFn()
+ if objLayer == nil {
+ w.Header().Set(xhttp.MinIOServerStatus, unavailable)
+ writeResponse(w, http.StatusServiceUnavailable, nil, mimeNone)
+ return nil
+ }
+
+ if !globalBucketMetadataSys.Initialized() {
+ w.Header().Set(xhttp.MinIOServerStatus, "bucket-metadata-offline")
+ writeResponse(w, http.StatusServiceUnavailable, nil, mimeNone)
+ return nil
+ }
+
+ if !globalIAMSys.Initialized() {
+ w.Header().Set(xhttp.MinIOServerStatus, "iam-offline")
+ writeResponse(w, http.StatusServiceUnavailable, nil, mimeNone)
+ return nil
+ }
+
+ return objLayer
+}
+
+// ClusterCheckHandler returns if the server is ready for requests.
+func ClusterCheckHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "ClusterCheckHandler")
+
+ objLayer := checkHealth(w)
+ if objLayer == nil {
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, globalAPIConfig.getClusterDeadline())
+ defer cancel()
+
+ opts := HealthOptions{
+ Maintenance: r.Form.Get("maintenance") == "true",
+ DeploymentType: r.Form.Get("deployment-type"),
+ }
+ result := objLayer.Health(ctx, opts)
+ w.Header().Set(xhttp.MinIOWriteQuorum, strconv.Itoa(result.WriteQuorum))
+ w.Header().Set(xhttp.MinIOStorageClassDefaults, strconv.FormatBool(result.UsingDefaults))
+ // return how many drives are being healed if any
+ if result.HealingDrives > 0 {
+ w.Header().Set(xhttp.MinIOHealingDrives, strconv.Itoa(result.HealingDrives))
+ }
+ if !result.Healthy {
+ // As a maintenance call we are purposefully asked to be taken
+ // down, this is for orchestrators to know if we can safely
+ // take this server down, return appropriate error.
+ if opts.Maintenance {
+ writeResponse(w, http.StatusPreconditionFailed, nil, mimeNone)
+ } else {
+ writeResponse(w, http.StatusServiceUnavailable, nil, mimeNone)
+ }
+ return
+ }
+ writeResponse(w, http.StatusOK, nil, mimeNone)
+}
+
+// ClusterReadCheckHandler returns if the server is ready for requests.
+func ClusterReadCheckHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "ClusterReadCheckHandler")
+
+ objLayer := checkHealth(w)
+ if objLayer == nil {
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, globalAPIConfig.getClusterDeadline())
+ defer cancel()
+
+ opts := HealthOptions{
+ Maintenance: r.Form.Get("maintenance") == "true",
+ DeploymentType: r.Form.Get("deployment-type"),
+ }
+ result := objLayer.Health(ctx, opts)
+ w.Header().Set(xhttp.MinIOReadQuorum, strconv.Itoa(result.ReadQuorum))
+ w.Header().Set(xhttp.MinIOStorageClassDefaults, strconv.FormatBool(result.UsingDefaults))
+ // return how many drives are being healed if any
+ if result.HealingDrives > 0 {
+ w.Header().Set(xhttp.MinIOHealingDrives, strconv.Itoa(result.HealingDrives))
+ }
+ if !result.HealthyRead {
+ // As a maintenance call we are purposefully asked to be taken
+ // down, this is for orchestrators to know if we can safely
+ // take this server down, return appropriate error.
+ if opts.Maintenance {
+ writeResponse(w, http.StatusPreconditionFailed, nil, mimeNone)
+ } else {
+ writeResponse(w, http.StatusServiceUnavailable, nil, mimeNone)
+ }
+ return
+ }
+ writeResponse(w, http.StatusOK, nil, mimeNone)
+}
+
+// ReadinessCheckHandler checks whether MinIO is up and ready to serve requests.
+// It also checks whether the KMS is available and whether etcd is reachable,
+// if configured.
+func ReadinessCheckHandler(w http.ResponseWriter, r *http.Request) {
+ if objLayer := newObjectLayerFn(); objLayer == nil {
+ w.Header().Set(xhttp.MinIOServerStatus, unavailable) // Service not initialized yet
+ }
+ if r.Header.Get(xhttp.MinIOPeerCall) != "" {
+ writeResponse(w, http.StatusOK, nil, mimeNone)
+ return
+ }
+
+ if int(globalHTTPStats.loadRequestsInQueue()) > globalAPIConfig.getRequestsPoolCapacity() {
+ apiErr := getAPIError(ErrBusy)
+ switch r.Method {
+ case http.MethodHead:
+ writeResponse(w, apiErr.HTTPStatusCode, nil, mimeNone)
+ case http.MethodGet:
+ writeErrorResponse(r.Context(), w, apiErr, r.URL)
+ }
+ return
+ }
+
+ // Verify if KMS is reachable if its configured
+ if GlobalKMS != nil {
+ ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
+ defer cancel()
+
+ if _, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{AssociatedData: kms.Context{"healthcheck": ""}}); err != nil {
+ switch r.Method {
+ case http.MethodHead:
+ apiErr := toAPIError(r.Context(), err)
+ writeResponse(w, apiErr.HTTPStatusCode, nil, mimeNone)
+ case http.MethodGet:
+ writeErrorResponse(r.Context(), w, toAPIError(r.Context(), err), r.URL)
+ }
+ return
+ }
+ }
+
+ if globalEtcdClient != nil {
+ // Borrowed from
+ // https://github.com/etcd-io/etcd/blob/main/etcdctl/ctlv3/command/ep_command.go#L118
+ ctx, cancel := context.WithTimeout(r.Context(), defaultContextTimeout)
+ defer cancel()
+ if _, err := globalEtcdClient.Get(ctx, "health"); err != nil {
+ // etcd unreachable throw an error..
+ switch r.Method {
+ case http.MethodHead:
+ apiErr := toAPIError(r.Context(), err)
+ writeResponse(w, apiErr.HTTPStatusCode, nil, mimeNone)
+ case http.MethodGet:
+ writeErrorResponse(r.Context(), w, toAPIError(r.Context(), err), r.URL)
+ }
+ return
+ }
+ }
+ writeResponse(w, http.StatusOK, nil, mimeNone)
+}
+
+// LivenessCheckHandler checks whether MinIO is up. It differs from the
+// readiness handler since a failing liveness check causes pod restarts
+// in K8S environments. Therefore, it does not contact external systems.
+func LivenessCheckHandler(w http.ResponseWriter, r *http.Request) {
+ if objLayer := newObjectLayerFn(); objLayer == nil {
+ w.Header().Set(xhttp.MinIOServerStatus, unavailable) // Service not initialized yet
+ }
+ if r.Header.Get(xhttp.MinIOPeerCall) != "" {
+ writeResponse(w, http.StatusOK, nil, mimeNone)
+ return
+ }
+
+ if int(globalHTTPStats.loadRequestsInQueue()) > globalAPIConfig.getRequestsPoolCapacity() {
+ apiErr := getAPIError(ErrBusy)
+ switch r.Method {
+ case http.MethodHead:
+ writeResponse(w, apiErr.HTTPStatusCode, nil, mimeNone)
+ case http.MethodGet:
+ writeErrorResponse(r.Context(), w, apiErr, r.URL)
+ }
+ return
+ }
+ writeResponse(w, http.StatusOK, nil, mimeNone)
+}
diff --git a/cmd/healthcheck-router.go b/cmd/healthcheck-router.go
new file mode 100644
index 00000000..d9adbfd2
--- /dev/null
+++ b/cmd/healthcheck-router.go
@@ -0,0 +1,53 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "net/http"
+
+ "github.com/minio/mux"
+)
+
+const (
+ healthCheckPath = "/health"
+ healthCheckLivenessPath = "/live"
+ healthCheckReadinessPath = "/ready"
+ healthCheckClusterPath = "/cluster"
+ healthCheckClusterReadPath = "/cluster/read"
+ healthCheckPathPrefix = minioReservedBucketPath + healthCheckPath
+)
+
+// registerHealthCheckRouter - add handler functions for liveness and readiness routes.
+func registerHealthCheckRouter(router *mux.Router) {
+ // Healthcheck router
+ healthRouter := router.PathPrefix(healthCheckPathPrefix).Subrouter()
+
+ // Cluster check handler to verify cluster is active
+ healthRouter.Methods(http.MethodGet).Path(healthCheckClusterPath).HandlerFunc(httpTraceAll(ClusterCheckHandler))
+ healthRouter.Methods(http.MethodHead).Path(healthCheckClusterPath).HandlerFunc(httpTraceAll(ClusterCheckHandler))
+ healthRouter.Methods(http.MethodGet).Path(healthCheckClusterReadPath).HandlerFunc(httpTraceAll(ClusterReadCheckHandler))
+ healthRouter.Methods(http.MethodHead).Path(healthCheckClusterReadPath).HandlerFunc(httpTraceAll(ClusterReadCheckHandler))
+
+ // Liveness handler
+ healthRouter.Methods(http.MethodGet).Path(healthCheckLivenessPath).HandlerFunc(httpTraceAll(LivenessCheckHandler))
+ healthRouter.Methods(http.MethodHead).Path(healthCheckLivenessPath).HandlerFunc(httpTraceAll(LivenessCheckHandler))
+
+ // Readiness handler
+ healthRouter.Methods(http.MethodGet).Path(healthCheckReadinessPath).HandlerFunc(httpTraceAll(ReadinessCheckHandler))
+ healthRouter.Methods(http.MethodHead).Path(healthCheckReadinessPath).HandlerFunc(httpTraceAll(ReadinessCheckHandler))
+}
diff --git a/cmd/http-stats.go b/cmd/http-stats.go
new file mode 100644
index 00000000..18a24e58
--- /dev/null
+++ b/cmd/http-stats.go
@@ -0,0 +1,458 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "net/http"
+ "strings"
+ "sync"
+ "sync/atomic"
+
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+// connStats - Network statistics
+// Count total input/output transferred bytes during
+// the server's life.
+type connStats struct {
+ internodeInputBytes uint64
+ internodeOutputBytes uint64
+ s3InputBytes uint64
+ s3OutputBytes uint64
+}
+
+// Increase internode total input bytes
+func (s *connStats) incInternodeInputBytes(n int64) {
+ atomic.AddUint64(&s.internodeInputBytes, uint64(n))
+}
+
+// Increase internode total output bytes
+func (s *connStats) incInternodeOutputBytes(n int64) {
+ atomic.AddUint64(&s.internodeOutputBytes, uint64(n))
+}
+
+// Return internode total input bytes
+func (s *connStats) getInternodeInputBytes() uint64 {
+ return atomic.LoadUint64(&s.internodeInputBytes)
+}
+
+// Return total output bytes
+func (s *connStats) getInternodeOutputBytes() uint64 {
+ return atomic.LoadUint64(&s.internodeOutputBytes)
+}
+
+// Increase S3 total input bytes
+func (s *connStats) incS3InputBytes(n int64) {
+ atomic.AddUint64(&s.s3InputBytes, uint64(n))
+}
+
+// Increase S3 total output bytes
+func (s *connStats) incS3OutputBytes(n int64) {
+ atomic.AddUint64(&s.s3OutputBytes, uint64(n))
+}
+
+// Return S3 total input bytes
+func (s *connStats) getS3InputBytes() uint64 {
+ return atomic.LoadUint64(&s.s3InputBytes)
+}
+
+// Return S3 total output bytes
+func (s *connStats) getS3OutputBytes() uint64 {
+ return atomic.LoadUint64(&s.s3OutputBytes)
+}
+
+// Return connection stats (total input/output bytes and total s3 input/output bytes)
+func (s *connStats) toServerConnStats() serverConnStats {
+ return serverConnStats{
+ internodeInputBytes: s.getInternodeInputBytes(), // Traffic internode received
+ internodeOutputBytes: s.getInternodeOutputBytes(), // Traffic internode sent
+ s3InputBytes: s.getS3InputBytes(), // Traffic S3 received
+ s3OutputBytes: s.getS3OutputBytes(), // Traffic S3 sent
+ }
+}
+
+// Prepare new ConnStats structure
+func newConnStats() *connStats {
+ return &connStats{}
+}
+
+type bucketS3RXTX struct {
+ s3InputBytes uint64
+ s3OutputBytes uint64
+}
+
+type bucketHTTPAPIStats struct {
+ currentS3Requests *HTTPAPIStats
+ totalS3Requests *HTTPAPIStats
+ totalS34xxErrors *HTTPAPIStats
+ totalS35xxErrors *HTTPAPIStats
+ totalS3Canceled *HTTPAPIStats
+}
+
+type bucketHTTPStats struct {
+ sync.RWMutex
+ httpStats map[string]bucketHTTPAPIStats
+}
+
+func newBucketHTTPStats() *bucketHTTPStats {
+ return &bucketHTTPStats{
+ httpStats: make(map[string]bucketHTTPAPIStats),
+ }
+}
+
+func (bh *bucketHTTPStats) delete(bucket string) {
+ bh.Lock()
+ defer bh.Unlock()
+
+ delete(bh.httpStats, bucket)
+}
+
+func (bh *bucketHTTPStats) updateHTTPStats(bucket, api string, w *xhttp.ResponseRecorder) {
+ if bh == nil {
+ return
+ }
+
+ if w != nil {
+ // Increment the prometheus http request response histogram with API, Bucket
+ bucketHTTPRequestsDuration.With(prometheus.Labels{
+ "api": api,
+ "bucket": bucket,
+ }).Observe(w.TTFB().Seconds())
+ }
+
+ bh.Lock()
+ defer bh.Unlock()
+
+ hstats, ok := bh.httpStats[bucket]
+ if !ok {
+ hstats = bucketHTTPAPIStats{
+ currentS3Requests: &HTTPAPIStats{},
+ totalS3Requests: &HTTPAPIStats{},
+ totalS3Canceled: &HTTPAPIStats{},
+ totalS34xxErrors: &HTTPAPIStats{},
+ totalS35xxErrors: &HTTPAPIStats{},
+ }
+ }
+
+ if w == nil { // when response recorder nil, this is an active request
+ hstats.currentS3Requests.Inc(api)
+ bh.httpStats[bucket] = hstats
+ return
+ } // else {
+ hstats.currentS3Requests.Dec(api) // decrement this once we have the response recorder.
+
+ hstats.totalS3Requests.Inc(api)
+ code := w.StatusCode
+
+ switch {
+ case code == 0:
+ case code == 499:
+ // 499 is a good error, shall be counted as canceled.
+ hstats.totalS3Canceled.Inc(api)
+ case code >= http.StatusBadRequest:
+ if code >= http.StatusInternalServerError {
+ hstats.totalS35xxErrors.Inc(api)
+ } else {
+ hstats.totalS34xxErrors.Inc(api)
+ }
+ }
+
+ bh.httpStats[bucket] = hstats
+}
+
+func (bh *bucketHTTPStats) load(bucket string) bucketHTTPAPIStats {
+ if bh == nil {
+ return bucketHTTPAPIStats{
+ currentS3Requests: &HTTPAPIStats{},
+ totalS3Requests: &HTTPAPIStats{},
+ totalS3Canceled: &HTTPAPIStats{},
+ totalS34xxErrors: &HTTPAPIStats{},
+ totalS35xxErrors: &HTTPAPIStats{},
+ }
+ }
+
+ bh.RLock()
+ defer bh.RUnlock()
+
+ val, ok := bh.httpStats[bucket]
+ if ok {
+ return val
+ }
+
+ return bucketHTTPAPIStats{
+ currentS3Requests: &HTTPAPIStats{},
+ totalS3Requests: &HTTPAPIStats{},
+ totalS3Canceled: &HTTPAPIStats{},
+ totalS34xxErrors: &HTTPAPIStats{},
+ totalS35xxErrors: &HTTPAPIStats{},
+ }
+}
+
+type bucketConnStats struct {
+ sync.RWMutex
+ stats map[string]*bucketS3RXTX
+}
+
+func newBucketConnStats() *bucketConnStats {
+ return &bucketConnStats{
+ stats: make(map[string]*bucketS3RXTX),
+ }
+}
+
+// Increase S3 total input bytes for input bucket
+func (s *bucketConnStats) incS3InputBytes(bucket string, n int64) {
+ s.Lock()
+ defer s.Unlock()
+ stats, ok := s.stats[bucket]
+ if !ok {
+ stats = &bucketS3RXTX{
+ s3InputBytes: uint64(n),
+ }
+ } else {
+ stats.s3InputBytes += uint64(n)
+ }
+ s.stats[bucket] = stats
+}
+
+// Increase S3 total output bytes for input bucket
+func (s *bucketConnStats) incS3OutputBytes(bucket string, n int64) {
+ s.Lock()
+ defer s.Unlock()
+ stats, ok := s.stats[bucket]
+ if !ok {
+ stats = &bucketS3RXTX{
+ s3OutputBytes: uint64(n),
+ }
+ } else {
+ stats.s3OutputBytes += uint64(n)
+ }
+ s.stats[bucket] = stats
+}
+
+type inOutBytes struct {
+ In uint64
+ Out uint64
+}
+
+// Return S3 total input bytes for input bucket
+func (s *bucketConnStats) getS3InOutBytes() map[string]inOutBytes {
+ s.RLock()
+ defer s.RUnlock()
+
+ if len(s.stats) == 0 {
+ return nil
+ }
+
+ bucketStats := make(map[string]inOutBytes, len(s.stats))
+ for k, v := range s.stats {
+ bucketStats[k] = inOutBytes{
+ In: v.s3InputBytes,
+ Out: v.s3OutputBytes,
+ }
+ }
+ return bucketStats
+}
+
+// Return S3 total input/output bytes for each
+func (s *bucketConnStats) getBucketS3InOutBytes(buckets []string) map[string]inOutBytes {
+ s.RLock()
+ defer s.RUnlock()
+
+ if len(s.stats) == 0 || len(buckets) == 0 {
+ return nil
+ }
+
+ bucketStats := make(map[string]inOutBytes, len(buckets))
+ for _, bucket := range buckets {
+ if stats, ok := s.stats[bucket]; ok {
+ bucketStats[bucket] = inOutBytes{
+ In: stats.s3InputBytes,
+ Out: stats.s3OutputBytes,
+ }
+ }
+ }
+
+ return bucketStats
+}
+
+// delete metrics once bucket is deleted.
+func (s *bucketConnStats) delete(bucket string) {
+ s.Lock()
+ defer s.Unlock()
+
+ delete(s.stats, bucket)
+}
+
+// HTTPAPIStats holds statistics information about
+// a given API in the requests.
+type HTTPAPIStats struct {
+ apiStats map[string]int
+ sync.RWMutex
+}
+
+// Inc increments the api stats counter.
+func (stats *HTTPAPIStats) Inc(api string) {
+ if stats == nil {
+ return
+ }
+ stats.Lock()
+ defer stats.Unlock()
+ if stats.apiStats == nil {
+ stats.apiStats = make(map[string]int)
+ }
+ stats.apiStats[api]++
+}
+
+// Dec increments the api stats counter.
+func (stats *HTTPAPIStats) Dec(api string) {
+ if stats == nil {
+ return
+ }
+ stats.Lock()
+ defer stats.Unlock()
+ if val, ok := stats.apiStats[api]; ok && val > 0 {
+ stats.apiStats[api]--
+ }
+}
+
+// Get returns the current counter on input API string
+func (stats *HTTPAPIStats) Get(api string) int {
+ if stats == nil {
+ return 0
+ }
+
+ stats.RLock()
+ defer stats.RUnlock()
+
+ val, ok := stats.apiStats[api]
+ if ok {
+ return val
+ }
+
+ return 0
+}
+
+// Load returns the recorded stats.
+func (stats *HTTPAPIStats) Load(toLower bool) map[string]int {
+ if stats == nil {
+ return map[string]int{}
+ }
+
+ stats.RLock()
+ defer stats.RUnlock()
+
+ apiStats := make(map[string]int, len(stats.apiStats))
+ for k, v := range stats.apiStats {
+ if toLower {
+ k = strings.ToLower(k)
+ }
+ apiStats[k] = v
+ }
+ return apiStats
+}
+
+// HTTPStats holds statistics information about
+// HTTP requests made by all clients
+type HTTPStats struct {
+ s3RequestsInQueue int32 // ref: https://golang.org/pkg/sync/atomic/#pkg-note-BUG
+ _ int32 // For 64 bits alignment
+ s3RequestsIncoming uint64
+ rejectedRequestsAuth uint64
+ rejectedRequestsTime uint64
+ rejectedRequestsHeader uint64
+ rejectedRequestsInvalid uint64
+ currentS3Requests HTTPAPIStats
+ totalS3Requests HTTPAPIStats
+ totalS3Errors HTTPAPIStats
+ totalS34xxErrors HTTPAPIStats
+ totalS35xxErrors HTTPAPIStats
+ totalS3Canceled HTTPAPIStats
+}
+
+func (st *HTTPStats) loadRequestsInQueue() int32 {
+ return atomic.LoadInt32(&st.s3RequestsInQueue)
+}
+
+func (st *HTTPStats) addRequestsInQueue(i int32) {
+ atomic.AddInt32(&st.s3RequestsInQueue, i)
+}
+
+func (st *HTTPStats) incS3RequestsIncoming() {
+ // Golang automatically resets to zero if this overflows
+ atomic.AddUint64(&st.s3RequestsIncoming, 1)
+}
+
+// Converts http stats into struct to be sent back to the client.
+func (st *HTTPStats) toServerHTTPStats(toLowerKeys bool) ServerHTTPStats {
+ serverStats := ServerHTTPStats{}
+ serverStats.S3RequestsIncoming = atomic.SwapUint64(&st.s3RequestsIncoming, 0)
+ serverStats.S3RequestsInQueue = atomic.LoadInt32(&st.s3RequestsInQueue)
+ serverStats.TotalS3RejectedAuth = atomic.LoadUint64(&st.rejectedRequestsAuth)
+ serverStats.TotalS3RejectedTime = atomic.LoadUint64(&st.rejectedRequestsTime)
+ serverStats.TotalS3RejectedHeader = atomic.LoadUint64(&st.rejectedRequestsHeader)
+ serverStats.TotalS3RejectedInvalid = atomic.LoadUint64(&st.rejectedRequestsInvalid)
+ serverStats.CurrentS3Requests = ServerHTTPAPIStats{
+ APIStats: st.currentS3Requests.Load(toLowerKeys),
+ }
+ serverStats.TotalS3Requests = ServerHTTPAPIStats{
+ APIStats: st.totalS3Requests.Load(toLowerKeys),
+ }
+ serverStats.TotalS3Errors = ServerHTTPAPIStats{
+ APIStats: st.totalS3Errors.Load(toLowerKeys),
+ }
+ serverStats.TotalS34xxErrors = ServerHTTPAPIStats{
+ APIStats: st.totalS34xxErrors.Load(toLowerKeys),
+ }
+ serverStats.TotalS35xxErrors = ServerHTTPAPIStats{
+ APIStats: st.totalS35xxErrors.Load(toLowerKeys),
+ }
+ serverStats.TotalS3Canceled = ServerHTTPAPIStats{
+ APIStats: st.totalS3Canceled.Load(toLowerKeys),
+ }
+ return serverStats
+}
+
+// Update statistics from http request and response data
+func (st *HTTPStats) updateStats(api string, w *xhttp.ResponseRecorder) {
+ st.totalS3Requests.Inc(api)
+
+ // Increment the prometheus http request response histogram with appropriate label
+ httpRequestsDuration.With(prometheus.Labels{"api": api}).Observe(w.TTFB().Seconds())
+
+ code := w.StatusCode
+
+ switch {
+ case code == 0:
+ case code == 499:
+ // 499 is a good error, shall be counted as canceled.
+ st.totalS3Canceled.Inc(api)
+ case code >= http.StatusBadRequest:
+ st.totalS3Errors.Inc(api)
+ if code >= http.StatusInternalServerError {
+ st.totalS35xxErrors.Inc(api)
+ } else {
+ st.totalS34xxErrors.Inc(api)
+ }
+ }
+}
+
+// Prepare new HTTPStats structure
+func newHTTPStats() *HTTPStats {
+ return &HTTPStats{}
+}
diff --git a/cmd/http-tracer.go b/cmd/http-tracer.go
new file mode 100644
index 00000000..e7ad7462
--- /dev/null
+++ b/cmd/http-tracer.go
@@ -0,0 +1,200 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "net"
+ "net/http"
+ "reflect"
+ "regexp"
+ "runtime"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/handlers"
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/minio/internal/mcontext"
+)
+
+var ldapPwdRegex = regexp.MustCompile("(^.*?)LDAPPassword=([^&]*?)(&(.*?))?$")
+
+// redact LDAP password if part of string
+func redactLDAPPwd(s string) string {
+ parts := ldapPwdRegex.FindStringSubmatch(s)
+ if len(parts) > 3 {
+ return parts[1] + "LDAPPassword=*REDACTED*" + parts[3]
+ }
+ return s
+}
+
+// getOpName sanitizes the operation name for mc
+func getOpName(name string) (op string) {
+ op = strings.TrimPrefix(name, "github.com/minio/minio/cmd.")
+ op = strings.TrimSuffix(op, "Handler-fm")
+ op = strings.Replace(op, "objectAPIHandlers", "s3", 1)
+ op = strings.Replace(op, "adminAPIHandlers", "admin", 1)
+ op = strings.Replace(op, "(*storageRESTServer)", "storageR", 1)
+ op = strings.Replace(op, "(*peerRESTServer)", "peer", 1)
+ op = strings.Replace(op, "(*lockRESTServer)", "lockR", 1)
+ op = strings.Replace(op, "(*stsAPIHandlers)", "sts", 1)
+ op = strings.Replace(op, "(*peerS3Server)", "s3", 1)
+ op = strings.Replace(op, "ClusterCheckHandler", "health.Cluster", 1)
+ op = strings.Replace(op, "ClusterReadCheckHandler", "health.ClusterRead", 1)
+ op = strings.Replace(op, "LivenessCheckHandler", "health.Liveness", 1)
+ op = strings.Replace(op, "ReadinessCheckHandler", "health.Readiness", 1)
+ op = strings.Replace(op, "-fm", "", 1)
+ return op
+}
+
+// If trace is enabled, execute the request if it is traced by other handlers
+// otherwise, generate a trace event with request information but no response.
+func httpTracerMiddleware(h http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Setup a http request response recorder - this is needed for
+ // http stats requests and audit if enabled.
+ respRecorder := xhttp.NewResponseRecorder(w)
+
+ // Setup a http request body recorder
+ reqRecorder := &xhttp.RequestRecorder{Reader: r.Body}
+ r.Body = reqRecorder
+
+ // Create tracing data structure and associate it to the request context
+ tc := mcontext.TraceCtxt{
+ AmzReqID: w.Header().Get(xhttp.AmzRequestID),
+ RequestRecorder: reqRecorder,
+ ResponseRecorder: respRecorder,
+ }
+
+ r = r.WithContext(context.WithValue(r.Context(), mcontext.ContextTraceKey, &tc))
+
+ reqStartTime := time.Now().UTC()
+ h.ServeHTTP(respRecorder, r)
+ reqEndTime := time.Now().UTC()
+
+ if globalTrace.NumSubscribers(madmin.TraceS3|madmin.TraceInternal) == 0 {
+ // no subscribers nothing to trace.
+ return
+ }
+
+ tt := madmin.TraceInternal
+ if strings.HasPrefix(tc.FuncName, "s3.") {
+ tt = madmin.TraceS3
+ }
+
+ // Calculate input body size with headers
+ reqHeaders := r.Header.Clone()
+ reqHeaders.Set("Host", r.Host)
+ if len(r.TransferEncoding) == 0 {
+ reqHeaders.Set("Content-Length", strconv.Itoa(int(r.ContentLength)))
+ } else {
+ reqHeaders.Set("Transfer-Encoding", strings.Join(r.TransferEncoding, ","))
+ }
+ inputBytes := reqRecorder.Size()
+ for k, v := range reqHeaders {
+ inputBytes += len(k) + len(v)
+ }
+
+ // Calculate node name
+ nodeName := r.Host
+ if globalIsDistErasure {
+ nodeName = globalLocalNodeName
+ }
+ if host, port, err := net.SplitHostPort(nodeName); err == nil {
+ if port == "443" || port == "80" {
+ nodeName = host
+ }
+ }
+
+ // Calculate reqPath
+ reqPath := r.URL.RawPath
+ if reqPath == "" {
+ reqPath = r.URL.Path
+ }
+
+ // Calculate function name
+ funcName := tc.FuncName
+ if funcName == "" {
+ funcName = ""
+ }
+
+ t := madmin.TraceInfo{
+ TraceType: tt,
+ FuncName: funcName,
+ NodeName: nodeName,
+ Time: reqStartTime,
+ Duration: reqEndTime.Sub(respRecorder.StartTime),
+ Path: reqPath,
+ Bytes: int64(inputBytes + respRecorder.Size()),
+ HTTP: &madmin.TraceHTTPStats{
+ ReqInfo: madmin.TraceRequestInfo{
+ Time: reqStartTime,
+ Proto: r.Proto,
+ Method: r.Method,
+ RawQuery: redactLDAPPwd(r.URL.RawQuery),
+ Client: handlers.GetSourceIP(r),
+ Headers: reqHeaders,
+ Path: reqPath,
+ Body: reqRecorder.Data(),
+ },
+ RespInfo: madmin.TraceResponseInfo{
+ Time: reqEndTime,
+ Headers: respRecorder.Header().Clone(),
+ StatusCode: respRecorder.StatusCode,
+ Body: respRecorder.Body(),
+ },
+ CallStats: madmin.TraceCallStats{
+ Latency: reqEndTime.Sub(respRecorder.StartTime),
+ InputBytes: inputBytes,
+ OutputBytes: respRecorder.Size(),
+ TimeToFirstByte: respRecorder.TTFB(),
+ },
+ },
+ }
+
+ globalTrace.Publish(t)
+ })
+}
+
+func httpTrace(f http.HandlerFunc, logBody bool) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
+ if !ok {
+ // Tracing is not enabled for this request
+ f.ServeHTTP(w, r)
+ return
+ }
+
+ tc.FuncName = getOpName(runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name())
+ tc.RequestRecorder.LogBody = logBody
+ tc.ResponseRecorder.LogAllBody = logBody
+ tc.ResponseRecorder.LogErrBody = true
+
+ f.ServeHTTP(w, r)
+ }
+}
+
+func httpTraceAll(f http.HandlerFunc) http.HandlerFunc {
+ return httpTrace(f, true)
+}
+
+func httpTraceHdrs(f http.HandlerFunc) http.HandlerFunc {
+ return httpTrace(f, false)
+}
diff --git a/cmd/http-tracer_test.go b/cmd/http-tracer_test.go
new file mode 100644
index 00000000..a05e1de7
--- /dev/null
+++ b/cmd/http-tracer_test.go
@@ -0,0 +1,184 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "sync"
+ "testing"
+ "time"
+
+ xhttp "github.com/minio/minio/internal/http"
+)
+
+// Test redactLDAPPwd()
+func TestRedactLDAPPwd(t *testing.T) {
+ testCases := []struct {
+ query string
+ expectedQuery string
+ }{
+ {"", ""},
+ {
+ "?Action=AssumeRoleWithLDAPIdentity&LDAPUsername=myusername&LDAPPassword=can+youreadthis%3F&Version=2011-06-15",
+ "?Action=AssumeRoleWithLDAPIdentity&LDAPUsername=myusername&LDAPPassword=*REDACTED*&Version=2011-06-15",
+ },
+ {
+ "LDAPPassword=can+youreadthis%3F&Version=2011-06-15&?Action=AssumeRoleWithLDAPIdentity&LDAPUsername=myusername",
+ "LDAPPassword=*REDACTED*&Version=2011-06-15&?Action=AssumeRoleWithLDAPIdentity&LDAPUsername=myusername",
+ },
+ {
+ "?Action=AssumeRoleWithLDAPIdentity&LDAPUsername=myusername&Version=2011-06-15&LDAPPassword=can+youreadthis%3F",
+ "?Action=AssumeRoleWithLDAPIdentity&LDAPUsername=myusername&Version=2011-06-15&LDAPPassword=*REDACTED*",
+ },
+ {
+ "?x=y&a=b",
+ "?x=y&a=b",
+ },
+ }
+ for i, test := range testCases {
+ gotQuery := redactLDAPPwd(test.query)
+ if gotQuery != test.expectedQuery {
+ t.Fatalf("test %d: expected %s got %s", i+1, test.expectedQuery, gotQuery)
+ }
+ }
+}
+
+// TestHTTPStatsRaceCondition tests the race condition fix for HTTPStats.
+// This test specifically addresses the race between:
+// - Write operations via updateStats.
+// - Read operations via toServerHTTPStats(false).
+func TestRaulStatsRaceCondition(t *testing.T) {
+ httpStats := newHTTPStats()
+ // Simulate the concurrent scenario from the original race condition:
+ // Multiple HTTP request handlers updating stats concurrently,
+ // while background processes are reading the stats for persistence.
+ const numWriters = 100 // Simulate many HTTP request handlers.
+ const numReaders = 50 // Simulate background stats readers.
+ const opsPerGoroutine = 100
+
+ var wg sync.WaitGroup
+ for i := range numWriters {
+ wg.Add(1)
+ go func(writerID int) {
+ defer wg.Done()
+ for j := 0; j < opsPerGoroutine; j++ {
+ switch j % 4 {
+ case 0:
+ httpStats.updateStats("GetObject", &xhttp.ResponseRecorder{})
+ case 1:
+ httpStats.totalS3Requests.Inc("PutObject")
+ case 2:
+ httpStats.totalS3Errors.Inc("DeleteObject")
+ case 3:
+ httpStats.currentS3Requests.Inc("ListObjects")
+ }
+ }
+ }(i)
+ }
+
+ for i := range numReaders {
+ wg.Add(1)
+ go func(readerID int) {
+ defer wg.Done()
+ for range opsPerGoroutine {
+ _ = httpStats.toServerHTTPStats(false)
+ _ = httpStats.totalS3Requests.Load(false)
+ _ = httpStats.currentS3Requests.Load(false)
+ time.Sleep(1 * time.Microsecond)
+ }
+ }(i)
+ }
+ wg.Wait()
+
+ finalStats := httpStats.toServerHTTPStats(false)
+ totalRequests := 0
+ for _, v := range finalStats.TotalS3Requests.APIStats {
+ totalRequests += v
+ }
+ if totalRequests == 0 {
+ t.Error("Expected some total requests to be recorded, but got zero")
+ }
+ t.Logf("Total requests recorded: %d", totalRequests)
+ t.Logf("Race condition test passed - no races detected")
+}
+
+// TestHTTPAPIStatsRaceCondition tests concurrent access to HTTPAPIStats specifically.
+func TestRaulHTTPAPIStatsRaceCondition(t *testing.T) {
+ stats := &HTTPAPIStats{}
+ const numGoroutines = 50
+ const opsPerGoroutine = 1000
+
+ var wg sync.WaitGroup
+ for i := range numGoroutines {
+ wg.Add(1)
+ go func(id int) {
+ defer wg.Done()
+ for j := 0; j < opsPerGoroutine; j++ {
+ stats.Inc("TestAPI")
+ }
+ }(i)
+ }
+
+ for i := range numGoroutines / 2 {
+ wg.Add(1)
+ go func(id int) {
+ defer wg.Done()
+ for range opsPerGoroutine / 2 {
+ _ = stats.Load(false)
+ }
+ }(i)
+ }
+ wg.Wait()
+
+ finalStats := stats.Load(false)
+ expected := numGoroutines * opsPerGoroutine
+ actual := finalStats["TestAPI"]
+ if actual != expected {
+ t.Errorf("Race condition detected: expected %d, got %d (lost %d increments)",
+ expected, actual, expected-actual)
+ }
+}
+
+// TestBucketHTTPStatsRaceCondition tests concurrent access to bucket-level HTTP stats.
+func TestRaulBucketHTTPStatsRaceCondition(t *testing.T) {
+ bucketStats := newBucketHTTPStats()
+ const numGoroutines = 50
+ const opsPerGoroutine = 100
+
+ var wg sync.WaitGroup
+ for i := range numGoroutines {
+ wg.Add(1)
+ go func(id int) {
+ defer wg.Done()
+ bucketName := "test-bucket"
+
+ for range opsPerGoroutine {
+ bucketStats.updateHTTPStats(bucketName, "GetObject", nil)
+ recorder := &xhttp.ResponseRecorder{}
+ bucketStats.updateHTTPStats(bucketName, "GetObject", recorder)
+ _ = bucketStats.load(bucketName)
+ }
+ }(i)
+ }
+ wg.Wait()
+
+ stats := bucketStats.load("test-bucket")
+ if stats.totalS3Requests == nil {
+ t.Error("Expected bucket stats to be initialized")
+ }
+ t.Logf("Bucket HTTP stats race test passed")
+}
diff --git a/cmd/httprange.go b/cmd/httprange.go
new file mode 100644
index 00000000..80e64f76
--- /dev/null
+++ b/cmd/httprange.go
@@ -0,0 +1,197 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+const (
+ byteRangePrefix = "bytes="
+)
+
+// HTTPRangeSpec represents a range specification as supported by S3 GET
+// object request.
+//
+// Case 1: Not present -> represented by a nil RangeSpec
+// Case 2: bytes=1-10 (absolute start and end offsets) -> RangeSpec{false, 1, 10}
+// Case 3: bytes=10- (absolute start offset with end offset unspecified) -> RangeSpec{false, 10, -1}
+// Case 4: bytes=-30 (suffix length specification) -> RangeSpec{true, -30, -1}
+type HTTPRangeSpec struct {
+ // Does the range spec refer to a suffix of the object?
+ IsSuffixLength bool
+
+ // Start and end offset specified in range spec
+ Start, End int64
+}
+
+// GetLength - get length of range
+func (h *HTTPRangeSpec) GetLength(resourceSize int64) (rangeLength int64, err error) {
+ switch {
+ case resourceSize < 0:
+ return 0, errors.New("Resource size cannot be negative")
+
+ case h == nil:
+ rangeLength = resourceSize
+
+ case h.IsSuffixLength:
+ specifiedLen := -h.Start
+ rangeLength = min(specifiedLen, resourceSize)
+
+ case h.Start >= resourceSize:
+ return 0, InvalidRange{
+ OffsetBegin: h.Start,
+ OffsetEnd: h.End,
+ ResourceSize: resourceSize,
+ }
+
+ case h.End > -1:
+ end := h.End
+ if resourceSize <= end {
+ end = resourceSize - 1
+ }
+ rangeLength = end - h.Start + 1
+
+ case h.End == -1:
+ rangeLength = resourceSize - h.Start
+
+ default:
+ return 0, errors.New("Unexpected range specification case")
+ }
+
+ return rangeLength, nil
+}
+
+// GetOffsetLength computes the start offset and length of the range
+// given the size of the resource
+func (h *HTTPRangeSpec) GetOffsetLength(resourceSize int64) (start, length int64, err error) {
+ if h == nil {
+ // No range specified, implies whole object.
+ return 0, resourceSize, nil
+ }
+
+ length, err = h.GetLength(resourceSize)
+ if err != nil {
+ return 0, 0, err
+ }
+
+ start = h.Start
+ if h.IsSuffixLength {
+ start = max(resourceSize+h.Start, 0)
+ }
+ return start, length, nil
+}
+
+// Parse a HTTP range header value into a HTTPRangeSpec
+func parseRequestRangeSpec(rangeString string) (hrange *HTTPRangeSpec, err error) {
+ // Return error if given range string doesn't start with byte range prefix.
+ if !strings.HasPrefix(rangeString, byteRangePrefix) {
+ return nil, fmt.Errorf("'%s' does not start with '%s'", rangeString, byteRangePrefix)
+ }
+
+ // Trim byte range prefix.
+ byteRangeString := strings.TrimPrefix(rangeString, byteRangePrefix)
+
+ // Check if range string contains delimiter '-', else return error. eg. "bytes=8"
+ sepIndex := strings.Index(byteRangeString, "-")
+ if sepIndex == -1 {
+ return nil, fmt.Errorf("'%s' does not have a valid range value", rangeString)
+ }
+
+ offsetBeginString := byteRangeString[:sepIndex]
+ offsetBegin := int64(-1)
+ // Convert offsetBeginString only if its not empty.
+ if len(offsetBeginString) > 0 {
+ if offsetBeginString[0] == '+' {
+ return nil, fmt.Errorf("Byte position ('%s') must not have a sign", offsetBeginString)
+ } else if offsetBegin, err = strconv.ParseInt(offsetBeginString, 10, 64); err != nil {
+ return nil, fmt.Errorf("'%s' does not have a valid first byte position value", rangeString)
+ } else if offsetBegin < 0 {
+ return nil, fmt.Errorf("First byte position is negative ('%d')", offsetBegin)
+ }
+ }
+
+ offsetEndString := byteRangeString[sepIndex+1:]
+ offsetEnd := int64(-1)
+ // Convert offsetEndString only if its not empty.
+ if len(offsetEndString) > 0 {
+ if offsetEndString[0] == '+' {
+ return nil, fmt.Errorf("Byte position ('%s') must not have a sign", offsetEndString)
+ } else if offsetEnd, err = strconv.ParseInt(offsetEndString, 10, 64); err != nil {
+ return nil, fmt.Errorf("'%s' does not have a valid last byte position value", rangeString)
+ } else if offsetEnd < 0 {
+ return nil, fmt.Errorf("Last byte position is negative ('%d')", offsetEnd)
+ }
+ }
+
+ switch {
+ case offsetBegin > -1 && offsetEnd > -1:
+ if offsetBegin > offsetEnd {
+ return nil, errInvalidRange
+ }
+ return &HTTPRangeSpec{false, offsetBegin, offsetEnd}, nil
+ case offsetBegin > -1:
+ return &HTTPRangeSpec{false, offsetBegin, -1}, nil
+ case offsetEnd > -1:
+ if offsetEnd == 0 {
+ return nil, errInvalidRange
+ }
+ return &HTTPRangeSpec{true, -offsetEnd, -1}, nil
+ default:
+ // rangeString contains first and last byte positions missing. eg. "bytes=-"
+ return nil, fmt.Errorf("'%s' does not have valid range value", rangeString)
+ }
+}
+
+// String returns stringified representation of range for a particular resource size.
+func (h *HTTPRangeSpec) String(resourceSize int64) string {
+ if h == nil {
+ return ""
+ }
+ off, length, err := h.GetOffsetLength(resourceSize)
+ if err != nil {
+ return ""
+ }
+ return fmt.Sprintf("%d-%d", off, off+length-1)
+}
+
+// ToHeader returns the Range header value.
+func (h *HTTPRangeSpec) ToHeader() (string, error) {
+ if h == nil {
+ return "", nil
+ }
+ start := strconv.Itoa(int(h.Start))
+ end := strconv.Itoa(int(h.End))
+ switch {
+ case h.Start >= 0 && h.End >= 0:
+ if h.Start > h.End {
+ return "", errInvalidRange
+ }
+ case h.IsSuffixLength:
+ end = strconv.Itoa(int(h.Start * -1))
+ start = ""
+ case h.Start > -1:
+ end = ""
+ default:
+ return "", errors.New("does not have valid range value")
+ }
+ return fmt.Sprintf("bytes=%s-%s", start, end), nil
+}
diff --git a/cmd/httprange_test.go b/cmd/httprange_test.go
new file mode 100644
index 00000000..ea13a380
--- /dev/null
+++ b/cmd/httprange_test.go
@@ -0,0 +1,146 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "testing"
+)
+
+func TestHTTPRequestRangeSpec(t *testing.T) {
+ resourceSize := int64(10)
+ validRangeSpecs := []struct {
+ spec string
+ expOffset, expLength int64
+ }{
+ {"bytes=0-", 0, 10},
+ {"bytes=1-", 1, 9},
+ {"bytes=0-9", 0, 10},
+ {"bytes=1-10", 1, 9},
+ {"bytes=1-1", 1, 1},
+ {"bytes=2-5", 2, 4},
+ {"bytes=-5", 5, 5},
+ {"bytes=-1", 9, 1},
+ {"bytes=-1000", 0, 10},
+ }
+ for i, testCase := range validRangeSpecs {
+ rs, err := parseRequestRangeSpec(testCase.spec)
+ if err != nil {
+ t.Errorf("unexpected err: %v", err)
+ }
+ o, l, err := rs.GetOffsetLength(resourceSize)
+ if err != nil {
+ t.Errorf("unexpected err: %v", err)
+ }
+ if o != testCase.expOffset || l != testCase.expLength {
+ t.Errorf("Case %d: got bad offset/length: %d,%d expected: %d,%d",
+ i, o, l, testCase.expOffset, testCase.expLength)
+ }
+ }
+
+ unparsableRangeSpecs := []string{
+ "bytes=-",
+ "bytes==",
+ "bytes==1-10",
+ "bytes=",
+ "bytes=aa",
+ "aa",
+ "",
+ "bytes=1-10-",
+ "bytes=1--10",
+ "bytes=-1-10",
+ "bytes=0-+3",
+ "bytes=+3-+5",
+ "bytes=10-11,12-10", // Unsupported by S3/MinIO (valid in RFC)
+ }
+ for i, urs := range unparsableRangeSpecs {
+ rs, err := parseRequestRangeSpec(urs)
+ if err == nil {
+ t.Errorf("Case %d: Did not get an expected error - got %v", i, rs)
+ }
+ if isErrInvalidRange(err) {
+ t.Errorf("Case %d: Got invalid range error instead of a parse error", i)
+ }
+ if rs != nil {
+ t.Errorf("Case %d: Got non-nil rs though err != nil: %v", i, rs)
+ }
+ }
+
+ invalidRangeSpecs := []string{
+ "bytes=5-3",
+ "bytes=10-10",
+ "bytes=10-",
+ "bytes=100-",
+ "bytes=-0",
+ }
+ for i, irs := range invalidRangeSpecs {
+ var err1, err2 error
+ var rs *HTTPRangeSpec
+ var o, l int64
+ rs, err1 = parseRequestRangeSpec(irs)
+ if err1 == nil {
+ o, l, err2 = rs.GetOffsetLength(resourceSize)
+ }
+ if isErrInvalidRange(err1) || (err1 == nil && isErrInvalidRange(err2)) {
+ continue
+ }
+ t.Errorf("Case %d: Expected errInvalidRange but: %v %v %d %d %v", i, rs, err1, o, l, err2)
+ }
+}
+
+func TestHTTPRequestRangeToHeader(t *testing.T) {
+ validRangeSpecs := []struct {
+ spec string
+ errExpected bool
+ }{
+ {"bytes=0-", false},
+ {"bytes=1-", false},
+
+ {"bytes=0-9", false},
+ {"bytes=1-10", false},
+ {"bytes=1-1", false},
+ {"bytes=2-5", false},
+
+ {"bytes=-5", false},
+ {"bytes=-1", false},
+ {"bytes=-1000", false},
+ {"bytes=", true},
+ {"bytes= ", true},
+ {"byte=", true},
+ {"bytes=A-B", true},
+ {"bytes=1-B", true},
+ {"bytes=B-1", true},
+ {"bytes=-1-1", true},
+ }
+ for i, testCase := range validRangeSpecs {
+ rs, err := parseRequestRangeSpec(testCase.spec)
+ if err != nil {
+ if !testCase.errExpected || err == nil && testCase.errExpected {
+ t.Errorf("unexpected err: %v", err)
+ }
+ continue
+ }
+ h, err := rs.ToHeader()
+ if err != nil && !testCase.errExpected || err == nil && testCase.errExpected {
+ t.Errorf("expected error with invalid range: %v", err)
+ }
+ if h != testCase.spec {
+ t.Errorf("Case %d: translated to incorrect header: %s expected: %s",
+ i, h, testCase.spec)
+ }
+ }
+}
diff --git a/cmd/iam-etcd-store.go b/cmd/iam-etcd-store.go
new file mode 100644
index 00000000..850606fd
--- /dev/null
+++ b/cmd/iam-etcd-store.go
@@ -0,0 +1,502 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "path"
+ "strings"
+ "sync"
+ "time"
+
+ jsoniter "github.com/json-iterator/go"
+ "github.com/minio/minio-go/v7/pkg/set"
+ "github.com/minio/minio/internal/config"
+ "github.com/minio/minio/internal/kms"
+ "github.com/puzpuzpuz/xsync/v3"
+ "go.etcd.io/etcd/api/v3/mvccpb"
+ etcd "go.etcd.io/etcd/client/v3"
+)
+
+var defaultContextTimeout = 30 * time.Second
+
+func etcdKvsToSet(prefix string, kvs []*mvccpb.KeyValue) set.StringSet {
+ users := set.NewStringSet()
+ for _, kv := range kvs {
+ user := extractPathPrefixAndSuffix(string(kv.Key), prefix, path.Base(string(kv.Key)))
+ users.Add(user)
+ }
+ return users
+}
+
+// Extract path string by stripping off the `prefix` value and the suffix,
+// value, usually in the following form.
+//
+// s := "config/iam/users/foo/config.json"
+// prefix := "config/iam/users/"
+// suffix := "config.json"
+// result is foo
+func extractPathPrefixAndSuffix(s string, prefix string, suffix string) string {
+ return pathClean(strings.TrimSuffix(strings.TrimPrefix(s, prefix), suffix))
+}
+
+// IAMEtcdStore implements IAMStorageAPI
+type IAMEtcdStore struct {
+ sync.RWMutex
+
+ *iamCache
+
+ usersSysType UsersSysType
+
+ client *etcd.Client
+}
+
+func newIAMEtcdStore(client *etcd.Client, usersSysType UsersSysType) *IAMEtcdStore {
+ return &IAMEtcdStore{
+ iamCache: newIamCache(),
+ client: client,
+ usersSysType: usersSysType,
+ }
+}
+
+func (ies *IAMEtcdStore) rlock() *iamCache {
+ ies.RLock()
+ return ies.iamCache
+}
+
+func (ies *IAMEtcdStore) runlock() {
+ ies.RUnlock()
+}
+
+func (ies *IAMEtcdStore) lock() *iamCache {
+ ies.Lock()
+ return ies.iamCache
+}
+
+func (ies *IAMEtcdStore) unlock() {
+ ies.Unlock()
+}
+
+func (ies *IAMEtcdStore) getUsersSysType() UsersSysType {
+ return ies.usersSysType
+}
+
+func (ies *IAMEtcdStore) saveIAMConfig(ctx context.Context, item any, itemPath string, opts ...options) error {
+ data, err := json.Marshal(item)
+ if err != nil {
+ return err
+ }
+ if GlobalKMS != nil {
+ data, err = config.EncryptBytes(GlobalKMS, data, kms.Context{
+ minioMetaBucket: path.Join(minioMetaBucket, itemPath),
+ })
+ if err != nil {
+ return err
+ }
+ }
+ return saveKeyEtcd(ctx, ies.client, itemPath, data, opts...)
+}
+
+func getIAMConfig(item any, data []byte, itemPath string) error {
+ data, err := decryptData(data, itemPath)
+ if err != nil {
+ return err
+ }
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ return json.Unmarshal(data, item)
+}
+
+func (ies *IAMEtcdStore) loadIAMConfig(ctx context.Context, item any, path string) error {
+ data, err := readKeyEtcd(ctx, ies.client, path)
+ if err != nil {
+ return err
+ }
+ return getIAMConfig(item, data, path)
+}
+
+func (ies *IAMEtcdStore) loadIAMConfigBytes(ctx context.Context, path string) ([]byte, error) {
+ data, err := readKeyEtcd(ctx, ies.client, path)
+ if err != nil {
+ return nil, err
+ }
+ return decryptData(data, path)
+}
+
+func (ies *IAMEtcdStore) deleteIAMConfig(ctx context.Context, path string) error {
+ return deleteKeyEtcd(ctx, ies.client, path)
+}
+
+func (ies *IAMEtcdStore) loadPolicyDocWithRetry(ctx context.Context, policy string, m map[string]PolicyDoc, _ int) error {
+ return ies.loadPolicyDoc(ctx, policy, m)
+}
+
+func (ies *IAMEtcdStore) loadPolicyDoc(ctx context.Context, policy string, m map[string]PolicyDoc) error {
+ data, err := ies.loadIAMConfigBytes(ctx, getPolicyDocPath(policy))
+ if err != nil {
+ if err == errConfigNotFound {
+ return errNoSuchPolicy
+ }
+ return err
+ }
+
+ var p PolicyDoc
+ err = p.parseJSON(data)
+ if err != nil {
+ return err
+ }
+
+ m[policy] = p
+ return nil
+}
+
+func (ies *IAMEtcdStore) getPolicyDocKV(ctx context.Context, kvs *mvccpb.KeyValue, m map[string]PolicyDoc) error {
+ data, err := decryptData(kvs.Value, string(kvs.Key))
+ if err != nil {
+ if err == errConfigNotFound {
+ return errNoSuchPolicy
+ }
+ return err
+ }
+
+ var p PolicyDoc
+ err = p.parseJSON(data)
+ if err != nil {
+ return err
+ }
+
+ policy := extractPathPrefixAndSuffix(string(kvs.Key), iamConfigPoliciesPrefix, path.Base(string(kvs.Key)))
+ m[policy] = p
+ return nil
+}
+
+func (ies *IAMEtcdStore) loadPolicyDocs(ctx context.Context, m map[string]PolicyDoc) error {
+ ctx, cancel := context.WithTimeout(ctx, defaultContextTimeout)
+ defer cancel()
+ // Retrieve all keys and values to avoid too many calls to etcd in case of
+ // a large number of policies
+ r, err := ies.client.Get(ctx, iamConfigPoliciesPrefix, etcd.WithPrefix())
+ if err != nil {
+ return err
+ }
+
+ // Parse all values to construct the policies data model.
+ for _, kvs := range r.Kvs {
+ if err = ies.getPolicyDocKV(ctx, kvs, m); err != nil && !errors.Is(err, errNoSuchPolicy) {
+ return err
+ }
+ }
+ return nil
+}
+
+func (ies *IAMEtcdStore) getUserKV(ctx context.Context, userkv *mvccpb.KeyValue, userType IAMUserType, m map[string]UserIdentity, basePrefix string) error {
+ var u UserIdentity
+ err := getIAMConfig(&u, userkv.Value, string(userkv.Key))
+ if err != nil {
+ if err == errConfigNotFound {
+ return errNoSuchUser
+ }
+ return err
+ }
+ user := extractPathPrefixAndSuffix(string(userkv.Key), basePrefix, path.Base(string(userkv.Key)))
+ return ies.addUser(ctx, user, userType, u, m)
+}
+
+func (ies *IAMEtcdStore) addUser(ctx context.Context, user string, userType IAMUserType, u UserIdentity, m map[string]UserIdentity) error {
+ if u.Credentials.IsExpired() {
+ // Delete expired identity.
+ deleteKeyEtcd(ctx, ies.client, getUserIdentityPath(user, userType))
+ deleteKeyEtcd(ctx, ies.client, getMappedPolicyPath(user, userType, false))
+ return nil
+ }
+ if u.Credentials.AccessKey == "" {
+ u.Credentials.AccessKey = user
+ }
+ if u.Credentials.SessionToken != "" {
+ jwtClaims, err := extractJWTClaims(u)
+ if err != nil {
+ if u.Credentials.IsTemp() {
+ // We should delete such that the client can re-request
+ // for the expiring credentials.
+ deleteKeyEtcd(ctx, ies.client, getUserIdentityPath(user, userType))
+ deleteKeyEtcd(ctx, ies.client, getMappedPolicyPath(user, userType, false))
+ }
+ return nil
+ }
+ u.Credentials.Claims = jwtClaims.Map()
+ }
+ if u.Credentials.Description == "" {
+ u.Credentials.Description = u.Credentials.Comment
+ }
+
+ m[user] = u
+ return nil
+}
+
+func (ies *IAMEtcdStore) loadSecretKey(ctx context.Context, user string, userType IAMUserType) (string, error) {
+ var u UserIdentity
+ err := ies.loadIAMConfig(ctx, &u, getUserIdentityPath(user, userType))
+ if err != nil {
+ if errors.Is(err, errConfigNotFound) {
+ return "", errNoSuchUser
+ }
+ return "", err
+ }
+ return u.Credentials.SecretKey, nil
+}
+
+func (ies *IAMEtcdStore) loadUser(ctx context.Context, user string, userType IAMUserType, m map[string]UserIdentity) error {
+ var u UserIdentity
+ err := ies.loadIAMConfig(ctx, &u, getUserIdentityPath(user, userType))
+ if err != nil {
+ if err == errConfigNotFound {
+ return errNoSuchUser
+ }
+ return err
+ }
+ return ies.addUser(ctx, user, userType, u, m)
+}
+
+func (ies *IAMEtcdStore) loadUsers(ctx context.Context, userType IAMUserType, m map[string]UserIdentity) error {
+ var basePrefix string
+ switch userType {
+ case svcUser:
+ basePrefix = iamConfigServiceAccountsPrefix
+ case stsUser:
+ basePrefix = iamConfigSTSPrefix
+ default:
+ basePrefix = iamConfigUsersPrefix
+ }
+
+ cctx, cancel := context.WithTimeout(ctx, defaultContextTimeout)
+ defer cancel()
+
+ // Retrieve all keys and values to avoid too many calls to etcd in case of
+ // a large number of users
+ r, err := ies.client.Get(cctx, basePrefix, etcd.WithPrefix())
+ if err != nil {
+ return err
+ }
+
+ // Parse all users values to create the proper data model
+ for _, userKv := range r.Kvs {
+ if err = ies.getUserKV(ctx, userKv, userType, m, basePrefix); err != nil && err != errNoSuchUser {
+ return err
+ }
+ }
+ return nil
+}
+
+func (ies *IAMEtcdStore) loadGroup(ctx context.Context, group string, m map[string]GroupInfo) error {
+ var gi GroupInfo
+ err := ies.loadIAMConfig(ctx, &gi, getGroupInfoPath(group))
+ if err != nil {
+ if err == errConfigNotFound {
+ return errNoSuchGroup
+ }
+ return err
+ }
+ m[group] = gi
+ return nil
+}
+
+func (ies *IAMEtcdStore) loadGroups(ctx context.Context, m map[string]GroupInfo) error {
+ cctx, cancel := context.WithTimeout(ctx, defaultContextTimeout)
+ defer cancel()
+
+ r, err := ies.client.Get(cctx, iamConfigGroupsPrefix, etcd.WithPrefix(), etcd.WithKeysOnly())
+ if err != nil {
+ return err
+ }
+
+ groups := etcdKvsToSet(iamConfigGroupsPrefix, r.Kvs)
+
+ // Reload config for all groups.
+ for _, group := range groups.ToSlice() {
+ if err = ies.loadGroup(ctx, group, m); err != nil && err != errNoSuchGroup {
+ return err
+ }
+ }
+ return nil
+}
+
+func (ies *IAMEtcdStore) loadMappedPolicyWithRetry(ctx context.Context, name string, userType IAMUserType, isGroup bool, m *xsync.MapOf[string, MappedPolicy], retries int) error {
+ return ies.loadMappedPolicy(ctx, name, userType, isGroup, m)
+}
+
+func (ies *IAMEtcdStore) loadMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool, m *xsync.MapOf[string, MappedPolicy]) error {
+ var p MappedPolicy
+ err := ies.loadIAMConfig(ctx, &p, getMappedPolicyPath(name, userType, isGroup))
+ if err != nil {
+ if err == errConfigNotFound {
+ return errNoSuchPolicy
+ }
+ return err
+ }
+ m.Store(name, p)
+ return nil
+}
+
+func getMappedPolicy(kv *mvccpb.KeyValue, m *xsync.MapOf[string, MappedPolicy], basePrefix string) error {
+ var p MappedPolicy
+ err := getIAMConfig(&p, kv.Value, string(kv.Key))
+ if err != nil {
+ if err == errConfigNotFound {
+ return errNoSuchPolicy
+ }
+ return err
+ }
+ name := extractPathPrefixAndSuffix(string(kv.Key), basePrefix, ".json")
+ m.Store(name, p)
+ return nil
+}
+
+func (ies *IAMEtcdStore) loadMappedPolicies(ctx context.Context, userType IAMUserType, isGroup bool, m *xsync.MapOf[string, MappedPolicy]) error {
+ cctx, cancel := context.WithTimeout(ctx, defaultContextTimeout)
+ defer cancel()
+ var basePrefix string
+ if isGroup {
+ basePrefix = iamConfigPolicyDBGroupsPrefix
+ } else {
+ switch userType {
+ case svcUser:
+ basePrefix = iamConfigPolicyDBServiceAccountsPrefix
+ case stsUser:
+ basePrefix = iamConfigPolicyDBSTSUsersPrefix
+ default:
+ basePrefix = iamConfigPolicyDBUsersPrefix
+ }
+ }
+ // Retrieve all keys and values to avoid too many calls to etcd in case of
+ // a large number of policy mappings
+ r, err := ies.client.Get(cctx, basePrefix, etcd.WithPrefix())
+ if err != nil {
+ return err
+ }
+
+ // Parse all policies mapping to create the proper data model
+ for _, kv := range r.Kvs {
+ if err = getMappedPolicy(kv, m, basePrefix); err != nil && !errors.Is(err, errNoSuchPolicy) {
+ return err
+ }
+ }
+ return nil
+}
+
+func (ies *IAMEtcdStore) savePolicyDoc(ctx context.Context, policyName string, p PolicyDoc) error {
+ return ies.saveIAMConfig(ctx, &p, getPolicyDocPath(policyName))
+}
+
+func (ies *IAMEtcdStore) saveMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool, mp MappedPolicy, opts ...options) error {
+ return ies.saveIAMConfig(ctx, mp, getMappedPolicyPath(name, userType, isGroup), opts...)
+}
+
+func (ies *IAMEtcdStore) saveUserIdentity(ctx context.Context, name string, userType IAMUserType, u UserIdentity, opts ...options) error {
+ return ies.saveIAMConfig(ctx, u, getUserIdentityPath(name, userType), opts...)
+}
+
+func (ies *IAMEtcdStore) saveGroupInfo(ctx context.Context, name string, gi GroupInfo) error {
+ return ies.saveIAMConfig(ctx, gi, getGroupInfoPath(name))
+}
+
+func (ies *IAMEtcdStore) deletePolicyDoc(ctx context.Context, name string) error {
+ err := ies.deleteIAMConfig(ctx, getPolicyDocPath(name))
+ if err == errConfigNotFound {
+ err = errNoSuchPolicy
+ }
+ return err
+}
+
+func (ies *IAMEtcdStore) deleteMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool) error {
+ err := ies.deleteIAMConfig(ctx, getMappedPolicyPath(name, userType, isGroup))
+ if err == errConfigNotFound {
+ err = errNoSuchPolicy
+ }
+ return err
+}
+
+func (ies *IAMEtcdStore) deleteUserIdentity(ctx context.Context, name string, userType IAMUserType) error {
+ err := ies.deleteIAMConfig(ctx, getUserIdentityPath(name, userType))
+ if err == errConfigNotFound {
+ err = errNoSuchUser
+ }
+ return err
+}
+
+func (ies *IAMEtcdStore) deleteGroupInfo(ctx context.Context, name string) error {
+ err := ies.deleteIAMConfig(ctx, getGroupInfoPath(name))
+ if err == errConfigNotFound {
+ err = errNoSuchGroup
+ }
+ return err
+}
+
+func (ies *IAMEtcdStore) watch(ctx context.Context, keyPath string) <-chan iamWatchEvent {
+ ch := make(chan iamWatchEvent)
+
+ // go routine to read events from the etcd watch channel and send them
+ // down `ch`
+ go func() {
+ for {
+ outerLoop:
+ watchCh := ies.client.Watch(ctx,
+ keyPath, etcd.WithPrefix(), etcd.WithKeysOnly())
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case watchResp, ok := <-watchCh:
+ if !ok {
+ time.Sleep(1 * time.Second)
+ // Upon an error on watch channel
+ // re-init the watch channel.
+ goto outerLoop
+ }
+ if err := watchResp.Err(); err != nil {
+ iamLogIf(ctx, err)
+ // log and retry.
+ time.Sleep(1 * time.Second)
+ // Upon an error on watch channel
+ // re-init the watch channel.
+ goto outerLoop
+ }
+ for _, event := range watchResp.Events {
+ isCreateEvent := event.IsModify() || event.IsCreate()
+ isDeleteEvent := event.Type == etcd.EventTypeDelete
+
+ switch {
+ case isCreateEvent:
+ ch <- iamWatchEvent{
+ isCreated: true,
+ keyPath: string(event.Kv.Key),
+ }
+ case isDeleteEvent:
+ ch <- iamWatchEvent{
+ isCreated: false,
+ keyPath: string(event.Kv.Key),
+ }
+ }
+ }
+ }
+ }
+ }
+ }()
+ return ch
+}
diff --git a/cmd/iam-etcd-store_test.go b/cmd/iam-etcd-store_test.go
new file mode 100644
index 00000000..75958abb
--- /dev/null
+++ b/cmd/iam-etcd-store_test.go
@@ -0,0 +1,40 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "testing"
+)
+
+func TestExtractPrefixAndSuffix(t *testing.T) {
+ specs := []struct {
+ path, prefix, suffix string
+ expected string
+ }{
+ {"config/iam/groups/foo.json", "config/iam/groups/", ".json", "foo"},
+ {"config/iam/groups/./foo.json", "config/iam/groups/", ".json", "foo"},
+ {"config/iam/groups/foo/config.json", "config/iam/groups/", "/config.json", "foo"},
+ {"config/iam/groups/foo/config.json", "config/iam/groups/", "config.json", "foo"},
+ }
+ for i, test := range specs {
+ result := extractPathPrefixAndSuffix(test.path, test.prefix, test.suffix)
+ if result != test.expected {
+ t.Errorf("unexpected result on test[%v]: expected[%s] but had [%s]", i, test.expected, result)
+ }
+ }
+}
diff --git a/cmd/iam-object-store.go b/cmd/iam-object-store.go
new file mode 100644
index 00000000..89931148
--- /dev/null
+++ b/cmd/iam-object-store.go
@@ -0,0 +1,913 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "maps"
+ "path"
+ "strings"
+ "sync"
+ "time"
+ "unicode/utf8"
+
+ jsoniter "github.com/json-iterator/go"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/config"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/minio/internal/kms"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/sync/errgroup"
+ "github.com/puzpuzpuz/xsync/v3"
+)
+
+// IAMObjectStore implements IAMStorageAPI
+type IAMObjectStore struct {
+ // Protect access to storage within the current server.
+ sync.RWMutex
+
+ *iamCache
+
+ usersSysType UsersSysType
+
+ objAPI ObjectLayer
+}
+
+func newIAMObjectStore(objAPI ObjectLayer, usersSysType UsersSysType) *IAMObjectStore {
+ return &IAMObjectStore{
+ iamCache: newIamCache(),
+ objAPI: objAPI,
+ usersSysType: usersSysType,
+ }
+}
+
+func (iamOS *IAMObjectStore) rlock() *iamCache {
+ iamOS.RLock()
+ return iamOS.iamCache
+}
+
+func (iamOS *IAMObjectStore) runlock() {
+ iamOS.RUnlock()
+}
+
+func (iamOS *IAMObjectStore) lock() *iamCache {
+ iamOS.Lock()
+ return iamOS.iamCache
+}
+
+func (iamOS *IAMObjectStore) unlock() {
+ iamOS.Unlock()
+}
+
+func (iamOS *IAMObjectStore) getUsersSysType() UsersSysType {
+ return iamOS.usersSysType
+}
+
+func (iamOS *IAMObjectStore) saveIAMConfig(ctx context.Context, item any, objPath string, opts ...options) error {
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ data, err := json.Marshal(item)
+ if err != nil {
+ return err
+ }
+ if GlobalKMS != nil {
+ data, err = config.EncryptBytes(GlobalKMS, data, kms.Context{
+ minioMetaBucket: path.Join(minioMetaBucket, objPath),
+ })
+ if err != nil {
+ return err
+ }
+ }
+ return saveConfig(ctx, iamOS.objAPI, objPath, data)
+}
+
+func decryptData(data []byte, objPath string) ([]byte, error) {
+ if utf8.Valid(data) {
+ return data, nil
+ }
+
+ pdata, err := madmin.DecryptData(globalActiveCred.String(), bytes.NewReader(data))
+ if err == nil {
+ return pdata, nil
+ }
+ if GlobalKMS != nil {
+ pdata, err = config.DecryptBytes(GlobalKMS, data, kms.Context{
+ minioMetaBucket: path.Join(minioMetaBucket, objPath),
+ })
+ if err == nil {
+ return pdata, nil
+ }
+ pdata, err = config.DecryptBytes(GlobalKMS, data, kms.Context{
+ minioMetaBucket: objPath,
+ })
+ if err == nil {
+ return pdata, nil
+ }
+ }
+ return nil, err
+}
+
+func (iamOS *IAMObjectStore) loadIAMConfigBytesWithMetadata(ctx context.Context, objPath string) ([]byte, ObjectInfo, error) {
+ data, meta, err := readConfigWithMetadata(ctx, iamOS.objAPI, objPath, ObjectOptions{})
+ if err != nil {
+ return nil, meta, err
+ }
+ data, err = decryptData(data, objPath)
+ if err != nil {
+ return nil, meta, err
+ }
+ return data, meta, nil
+}
+
+func (iamOS *IAMObjectStore) loadIAMConfig(ctx context.Context, item any, objPath string) error {
+ data, _, err := iamOS.loadIAMConfigBytesWithMetadata(ctx, objPath)
+ if err != nil {
+ return err
+ }
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ return json.Unmarshal(data, item)
+}
+
+func (iamOS *IAMObjectStore) deleteIAMConfig(ctx context.Context, path string) error {
+ return deleteConfig(ctx, iamOS.objAPI, path)
+}
+
+func (iamOS *IAMObjectStore) loadPolicyDocWithRetry(ctx context.Context, policy string, m map[string]PolicyDoc, retries int) error {
+ for {
+ retry:
+ data, objInfo, err := iamOS.loadIAMConfigBytesWithMetadata(ctx, getPolicyDocPath(policy))
+ if err != nil {
+ if err == errConfigNotFound {
+ return errNoSuchPolicy
+ }
+ retries--
+ if retries <= 0 {
+ return err
+ }
+ time.Sleep(500 * time.Millisecond)
+ goto retry
+ }
+
+ var p PolicyDoc
+ err = p.parseJSON(data)
+ if err != nil {
+ return err
+ }
+
+ if p.Version == 0 {
+ // This means that policy was in the old version (without any
+ // timestamp info). We fetch the mod time of the file and save
+ // that as create and update date.
+ p.CreateDate = objInfo.ModTime
+ p.UpdateDate = objInfo.ModTime
+ }
+
+ m[policy] = p
+ return nil
+ }
+}
+
+func (iamOS *IAMObjectStore) loadPolicy(ctx context.Context, policy string) (PolicyDoc, error) {
+ var p PolicyDoc
+
+ data, objInfo, err := iamOS.loadIAMConfigBytesWithMetadata(ctx, getPolicyDocPath(policy))
+ if err != nil {
+ if err == errConfigNotFound {
+ return p, errNoSuchPolicy
+ }
+ return p, err
+ }
+
+ err = p.parseJSON(data)
+ if err != nil {
+ return p, err
+ }
+
+ if p.Version == 0 {
+ // This means that policy was in the old version (without any
+ // timestamp info). We fetch the mod time of the file and save
+ // that as create and update date.
+ p.CreateDate = objInfo.ModTime
+ p.UpdateDate = objInfo.ModTime
+ }
+
+ return p, nil
+}
+
+func (iamOS *IAMObjectStore) loadPolicyDoc(ctx context.Context, policy string, m map[string]PolicyDoc) error {
+ p, err := iamOS.loadPolicy(ctx, policy)
+ if err != nil {
+ return err
+ }
+ m[policy] = p
+ return nil
+}
+
+func (iamOS *IAMObjectStore) loadPolicyDocs(ctx context.Context, m map[string]PolicyDoc) error {
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+ for item := range listIAMConfigItems(ctx, iamOS.objAPI, iamConfigPoliciesPrefix) {
+ if item.Err != nil {
+ return item.Err
+ }
+
+ policyName := path.Dir(item.Item)
+ if err := iamOS.loadPolicyDoc(ctx, policyName, m); err != nil && !errors.Is(err, errNoSuchPolicy) {
+ return err
+ }
+ }
+ return nil
+}
+
+func (iamOS *IAMObjectStore) loadSecretKey(ctx context.Context, user string, userType IAMUserType) (string, error) {
+ var u UserIdentity
+ err := iamOS.loadIAMConfig(ctx, &u, getUserIdentityPath(user, userType))
+ if err != nil {
+ if errors.Is(err, errConfigNotFound) {
+ return "", errNoSuchUser
+ }
+ return "", err
+ }
+ return u.Credentials.SecretKey, nil
+}
+
+func (iamOS *IAMObjectStore) loadUserIdentity(ctx context.Context, user string, userType IAMUserType) (UserIdentity, error) {
+ var u UserIdentity
+ err := iamOS.loadIAMConfig(ctx, &u, getUserIdentityPath(user, userType))
+ if err != nil {
+ if err == errConfigNotFound {
+ return u, errNoSuchUser
+ }
+ return u, err
+ }
+
+ if u.Credentials.IsExpired() {
+ // Delete expired identity - ignoring errors here.
+ iamOS.deleteIAMConfig(ctx, getUserIdentityPath(user, userType))
+ iamOS.deleteIAMConfig(ctx, getMappedPolicyPath(user, userType, false))
+ return u, errNoSuchUser
+ }
+
+ if u.Credentials.AccessKey == "" {
+ u.Credentials.AccessKey = user
+ }
+
+ if u.Credentials.SessionToken != "" {
+ jwtClaims, err := extractJWTClaims(u)
+ if err != nil {
+ if u.Credentials.IsTemp() {
+ // We should delete such that the client can re-request
+ // for the expiring credentials.
+ iamOS.deleteIAMConfig(ctx, getUserIdentityPath(user, userType))
+ iamOS.deleteIAMConfig(ctx, getMappedPolicyPath(user, userType, false))
+ }
+ return u, errNoSuchUser
+ }
+ u.Credentials.Claims = jwtClaims.Map()
+ }
+
+ if u.Credentials.Description == "" {
+ u.Credentials.Description = u.Credentials.Comment
+ }
+
+ return u, nil
+}
+
+func (iamOS *IAMObjectStore) loadUserConcurrent(ctx context.Context, userType IAMUserType, users ...string) ([]UserIdentity, error) {
+ userIdentities := make([]UserIdentity, len(users))
+ g := errgroup.WithNErrs(len(users))
+
+ for index := range users {
+ g.Go(func() error {
+ userName := path.Dir(users[index])
+ user, err := iamOS.loadUserIdentity(ctx, userName, userType)
+ if err != nil && !errors.Is(err, errNoSuchUser) {
+ return fmt.Errorf("unable to load the user `%s`: %w", userName, err)
+ }
+ userIdentities[index] = user
+ return nil
+ }, index)
+ }
+
+ err := errors.Join(g.Wait()...)
+ return userIdentities, err
+}
+
+func (iamOS *IAMObjectStore) loadUser(ctx context.Context, user string, userType IAMUserType, m map[string]UserIdentity) error {
+ u, err := iamOS.loadUserIdentity(ctx, user, userType)
+ if err != nil {
+ return err
+ }
+ m[user] = u
+ return nil
+}
+
+func (iamOS *IAMObjectStore) loadUsers(ctx context.Context, userType IAMUserType, m map[string]UserIdentity) error {
+ var basePrefix string
+ switch userType {
+ case svcUser:
+ basePrefix = iamConfigServiceAccountsPrefix
+ case stsUser:
+ basePrefix = iamConfigSTSPrefix
+ default:
+ basePrefix = iamConfigUsersPrefix
+ }
+
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+ for item := range listIAMConfigItems(ctx, iamOS.objAPI, basePrefix) {
+ if item.Err != nil {
+ return item.Err
+ }
+
+ userName := path.Dir(item.Item)
+ if err := iamOS.loadUser(ctx, userName, userType, m); err != nil && err != errNoSuchUser {
+ return err
+ }
+ }
+ return nil
+}
+
+func (iamOS *IAMObjectStore) loadGroup(ctx context.Context, group string, m map[string]GroupInfo) error {
+ var g GroupInfo
+ err := iamOS.loadIAMConfig(ctx, &g, getGroupInfoPath(group))
+ if err != nil {
+ if err == errConfigNotFound {
+ return errNoSuchGroup
+ }
+ return err
+ }
+ m[group] = g
+ return nil
+}
+
+func (iamOS *IAMObjectStore) loadGroups(ctx context.Context, m map[string]GroupInfo) error {
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+ for item := range listIAMConfigItems(ctx, iamOS.objAPI, iamConfigGroupsPrefix) {
+ if item.Err != nil {
+ return item.Err
+ }
+
+ group := path.Dir(item.Item)
+ if err := iamOS.loadGroup(ctx, group, m); err != nil && err != errNoSuchGroup {
+ return err
+ }
+ }
+ return nil
+}
+
+func (iamOS *IAMObjectStore) loadMappedPolicyWithRetry(ctx context.Context, name string, userType IAMUserType, isGroup bool, m *xsync.MapOf[string, MappedPolicy], retries int) error {
+ for {
+ retry:
+ var p MappedPolicy
+ err := iamOS.loadIAMConfig(ctx, &p, getMappedPolicyPath(name, userType, isGroup))
+ if err != nil {
+ if err == errConfigNotFound {
+ return errNoSuchPolicy
+ }
+ retries--
+ if retries <= 0 {
+ return err
+ }
+ time.Sleep(500 * time.Millisecond)
+ goto retry
+ }
+
+ m.Store(name, p)
+ return nil
+ }
+}
+
+func (iamOS *IAMObjectStore) loadMappedPolicyInternal(ctx context.Context, name string, userType IAMUserType, isGroup bool) (MappedPolicy, error) {
+ var p MappedPolicy
+ err := iamOS.loadIAMConfig(ctx, &p, getMappedPolicyPath(name, userType, isGroup))
+ if err != nil {
+ if err == errConfigNotFound {
+ return p, errNoSuchPolicy
+ }
+ return p, err
+ }
+ return p, nil
+}
+
+func (iamOS *IAMObjectStore) loadMappedPolicyConcurrent(ctx context.Context, userType IAMUserType, isGroup bool, users ...string) ([]MappedPolicy, error) {
+ mappedPolicies := make([]MappedPolicy, len(users))
+ g := errgroup.WithNErrs(len(users))
+
+ for index := range users {
+ g.Go(func() error {
+ userName := strings.TrimSuffix(users[index], ".json")
+ userMP, err := iamOS.loadMappedPolicyInternal(ctx, userName, userType, isGroup)
+ if err != nil && !errors.Is(err, errNoSuchPolicy) {
+ return fmt.Errorf("unable to load the user policy map `%s`: %w", userName, err)
+ }
+ mappedPolicies[index] = userMP
+ return nil
+ }, index)
+ }
+
+ err := errors.Join(g.Wait()...)
+ return mappedPolicies, err
+}
+
+func (iamOS *IAMObjectStore) loadMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool, m *xsync.MapOf[string, MappedPolicy]) error {
+ p, err := iamOS.loadMappedPolicyInternal(ctx, name, userType, isGroup)
+ if err != nil {
+ return err
+ }
+ m.Store(name, p)
+ return nil
+}
+
+func (iamOS *IAMObjectStore) loadMappedPolicies(ctx context.Context, userType IAMUserType, isGroup bool, m *xsync.MapOf[string, MappedPolicy]) error {
+ var basePath string
+ if isGroup {
+ basePath = iamConfigPolicyDBGroupsPrefix
+ } else {
+ switch userType {
+ case svcUser:
+ basePath = iamConfigPolicyDBServiceAccountsPrefix
+ case stsUser:
+ basePath = iamConfigPolicyDBSTSUsersPrefix
+ default:
+ basePath = iamConfigPolicyDBUsersPrefix
+ }
+ }
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+ for item := range listIAMConfigItems(ctx, iamOS.objAPI, basePath) {
+ if item.Err != nil {
+ return item.Err
+ }
+
+ policyFile := item.Item
+ userOrGroupName := strings.TrimSuffix(policyFile, ".json")
+ if err := iamOS.loadMappedPolicy(ctx, userOrGroupName, userType, isGroup, m); err != nil && !errors.Is(err, errNoSuchPolicy) {
+ return err
+ }
+ }
+ return nil
+}
+
+var (
+ usersListKey = "users/"
+ svcAccListKey = "service-accounts/"
+ groupsListKey = "groups/"
+ policiesListKey = "policies/"
+ stsListKey = "sts/"
+ policyDBPrefix = "policydb/"
+ policyDBUsersListKey = "policydb/users/"
+ policyDBSTSUsersListKey = "policydb/sts-users/"
+ policyDBGroupsListKey = "policydb/groups/"
+)
+
+func findSecondIndex(s string, substr string) int {
+ first := strings.Index(s, substr)
+ if first == -1 {
+ return -1
+ }
+ second := strings.Index(s[first+1:], substr)
+ if second == -1 {
+ return -1
+ }
+ return first + second + 1
+}
+
+// splitPath splits a path into a top-level directory and a child item. The
+// parent directory retains the trailing slash.
+func splitPath(s string, secondIndex bool) (string, string) {
+ var i int
+ if secondIndex {
+ i = findSecondIndex(s, "/")
+ } else {
+ i = strings.Index(s, "/")
+ }
+ if i == -1 {
+ return s, ""
+ }
+ // Include the trailing slash in the parent directory.
+ return s[:i+1], s[i+1:]
+}
+
+func (iamOS *IAMObjectStore) listAllIAMConfigItems(ctx context.Context) (res map[string][]string, err error) {
+ res = make(map[string][]string)
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+ for item := range listIAMConfigItems(ctx, iamOS.objAPI, iamConfigPrefix+SlashSeparator) {
+ if item.Err != nil {
+ return nil, item.Err
+ }
+
+ secondIndex := strings.HasPrefix(item.Item, policyDBPrefix)
+ listKey, trimmedItem := splitPath(item.Item, secondIndex)
+ if listKey == iamFormatFile {
+ continue
+ }
+
+ res[listKey] = append(res[listKey], trimmedItem)
+ }
+
+ return res, nil
+}
+
+const (
+ maxIAMLoadOpTime = 5 * time.Second
+)
+
+func (iamOS *IAMObjectStore) loadPolicyDocConcurrent(ctx context.Context, policies ...string) ([]PolicyDoc, error) {
+ policyDocs := make([]PolicyDoc, len(policies))
+ g := errgroup.WithNErrs(len(policies))
+
+ for index := range policies {
+ g.Go(func() error {
+ policyName := path.Dir(policies[index])
+ policyDoc, err := iamOS.loadPolicy(ctx, policyName)
+ if err != nil && !errors.Is(err, errNoSuchPolicy) {
+ return fmt.Errorf("unable to load the policy doc `%s`: %w", policyName, err)
+ }
+ policyDocs[index] = policyDoc
+ return nil
+ }, index)
+ }
+
+ err := errors.Join(g.Wait()...)
+ return policyDocs, err
+}
+
+// Assumes cache is locked by caller.
+func (iamOS *IAMObjectStore) loadAllFromObjStore(ctx context.Context, cache *iamCache, firstTime bool) error {
+ bootstrapTraceMsgFirstTime := func(s string) {
+ if firstTime {
+ bootstrapTraceMsg(s)
+ }
+ }
+
+ if iamOS.objAPI == nil {
+ return errServerNotInitialized
+ }
+
+ bootstrapTraceMsgFirstTime("loading all IAM items")
+
+ setDefaultCannedPolicies(cache.iamPolicyDocsMap)
+
+ listStartTime := UTCNow()
+ listedConfigItems, err := iamOS.listAllIAMConfigItems(ctx)
+ if err != nil {
+ return fmt.Errorf("unable to list IAM data: %w", err)
+ }
+ if took := time.Since(listStartTime); took > maxIAMLoadOpTime {
+ var s strings.Builder
+ for k, v := range listedConfigItems {
+ s.WriteString(fmt.Sprintf(" %s: %d items\n", k, len(v)))
+ }
+ logger.Info("listAllIAMConfigItems took %.2fs with contents:\n%s", took.Seconds(), s.String())
+ }
+
+ // Loads things in the same order as `LoadIAMCache()`
+
+ bootstrapTraceMsgFirstTime("loading policy documents")
+
+ policyLoadStartTime := UTCNow()
+ policiesList := listedConfigItems[policiesListKey]
+ count := 32 // number of parallel IAM loaders
+ for {
+ if len(policiesList) < count {
+ policyDocs, err := iamOS.loadPolicyDocConcurrent(ctx, policiesList...)
+ if err != nil {
+ return err
+ }
+ for index := range policiesList {
+ if policyDocs[index].Policy.Version != "" {
+ policyName := path.Dir(policiesList[index])
+ cache.iamPolicyDocsMap[policyName] = policyDocs[index]
+ }
+ }
+ break
+ }
+
+ policyDocs, err := iamOS.loadPolicyDocConcurrent(ctx, policiesList[:count]...)
+ if err != nil {
+ return err
+ }
+
+ for index := range policiesList[:count] {
+ if policyDocs[index].Policy.Version != "" {
+ policyName := path.Dir(policiesList[index])
+ cache.iamPolicyDocsMap[policyName] = policyDocs[index]
+ }
+ }
+
+ policiesList = policiesList[count:]
+ }
+
+ if took := time.Since(policyLoadStartTime); took > maxIAMLoadOpTime {
+ logger.Info("Policy docs load took %.2fs (for %d items)", took.Seconds(), len(policiesList))
+ }
+
+ if iamOS.usersSysType == MinIOUsersSysType {
+ bootstrapTraceMsgFirstTime("loading regular IAM users")
+ regUsersLoadStartTime := UTCNow()
+ regUsersList := listedConfigItems[usersListKey]
+
+ for {
+ if len(regUsersList) < count {
+ users, err := iamOS.loadUserConcurrent(ctx, regUser, regUsersList...)
+ if err != nil {
+ return err
+ }
+ for index := range regUsersList {
+ if users[index].Credentials.AccessKey != "" {
+ userName := path.Dir(regUsersList[index])
+ cache.iamUsersMap[userName] = users[index]
+ }
+ }
+ break
+ }
+
+ users, err := iamOS.loadUserConcurrent(ctx, regUser, regUsersList[:count]...)
+ if err != nil {
+ return err
+ }
+
+ for index := range regUsersList[:count] {
+ if users[index].Credentials.AccessKey != "" {
+ userName := path.Dir(regUsersList[index])
+ cache.iamUsersMap[userName] = users[index]
+ }
+ }
+
+ regUsersList = regUsersList[count:]
+ }
+
+ if took := time.Since(regUsersLoadStartTime); took > maxIAMLoadOpTime {
+ actualLoaded := len(cache.iamUsersMap)
+ logger.Info("Reg. users load took %.2fs (for %d items with %d expired items)", took.Seconds(),
+ len(regUsersList), len(regUsersList)-actualLoaded)
+ }
+
+ bootstrapTraceMsgFirstTime("loading regular IAM groups")
+ groupsLoadStartTime := UTCNow()
+ groupsList := listedConfigItems[groupsListKey]
+ for _, item := range groupsList {
+ group := path.Dir(item)
+ if err := iamOS.loadGroup(ctx, group, cache.iamGroupsMap); err != nil && err != errNoSuchGroup {
+ return fmt.Errorf("unable to load the group: %w", err)
+ }
+ }
+ if took := time.Since(groupsLoadStartTime); took > maxIAMLoadOpTime {
+ logger.Info("Groups load took %.2fs (for %d items)", took.Seconds(), len(groupsList))
+ }
+ }
+
+ bootstrapTraceMsgFirstTime("loading user policy mapping")
+ userPolicyMappingLoadStartTime := UTCNow()
+ userPolicyMappingsList := listedConfigItems[policyDBUsersListKey]
+ for {
+ if len(userPolicyMappingsList) < count {
+ mappedPolicies, err := iamOS.loadMappedPolicyConcurrent(ctx, regUser, false, userPolicyMappingsList...)
+ if err != nil {
+ return err
+ }
+
+ for index := range userPolicyMappingsList {
+ if mappedPolicies[index].Policies != "" {
+ userName := strings.TrimSuffix(userPolicyMappingsList[index], ".json")
+ cache.iamUserPolicyMap.Store(userName, mappedPolicies[index])
+ }
+ }
+
+ break
+ }
+
+ mappedPolicies, err := iamOS.loadMappedPolicyConcurrent(ctx, regUser, false, userPolicyMappingsList[:count]...)
+ if err != nil {
+ return err
+ }
+
+ for index := range userPolicyMappingsList[:count] {
+ if mappedPolicies[index].Policies != "" {
+ userName := strings.TrimSuffix(userPolicyMappingsList[index], ".json")
+ cache.iamUserPolicyMap.Store(userName, mappedPolicies[index])
+ }
+ }
+
+ userPolicyMappingsList = userPolicyMappingsList[count:]
+ }
+
+ if took := time.Since(userPolicyMappingLoadStartTime); took > maxIAMLoadOpTime {
+ logger.Info("User policy mappings load took %.2fs (for %d items)", took.Seconds(), len(userPolicyMappingsList))
+ }
+
+ bootstrapTraceMsgFirstTime("loading group policy mapping")
+ groupPolicyMappingLoadStartTime := UTCNow()
+ groupPolicyMappingsList := listedConfigItems[policyDBGroupsListKey]
+ for _, item := range groupPolicyMappingsList {
+ groupName := strings.TrimSuffix(item, ".json")
+ if err := iamOS.loadMappedPolicy(ctx, groupName, regUser, true, cache.iamGroupPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) {
+ return fmt.Errorf("unable to load the policy mapping for the group: %w", err)
+ }
+ }
+ if took := time.Since(groupPolicyMappingLoadStartTime); took > maxIAMLoadOpTime {
+ logger.Info("Group policy mappings load took %.2fs (for %d items)", took.Seconds(), len(groupPolicyMappingsList))
+ }
+
+ bootstrapTraceMsgFirstTime("loading service accounts")
+ svcAccLoadStartTime := UTCNow()
+ svcAccList := listedConfigItems[svcAccListKey]
+ svcUsersMap := make(map[string]UserIdentity, len(svcAccList))
+ for _, item := range svcAccList {
+ userName := path.Dir(item)
+ if err := iamOS.loadUser(ctx, userName, svcUser, svcUsersMap); err != nil && err != errNoSuchUser {
+ return fmt.Errorf("unable to load the service account: %w", err)
+ }
+ }
+ if took := time.Since(svcAccLoadStartTime); took > maxIAMLoadOpTime {
+ logger.Info("Service accounts load took %.2fs (for %d items with %d expired items)", took.Seconds(),
+ len(svcAccList), len(svcAccList)-len(svcUsersMap))
+ }
+
+ bootstrapTraceMsg("loading STS account policy mapping")
+ stsPolicyMappingLoadStartTime := UTCNow()
+ var stsPolicyMappingsCount int
+ for _, svcAcc := range svcUsersMap {
+ svcParent := svcAcc.Credentials.ParentUser
+ if _, ok := cache.iamUsersMap[svcParent]; !ok {
+ stsPolicyMappingsCount++
+ // If a service account's parent user is not in iamUsersMap, the
+ // parent is an STS account. Such accounts may have a policy mapped
+ // on the parent user, so we load them. This is not needed for the
+ // initial server startup, however, it is needed for the case where
+ // the STS account's policy mapping (for example in LDAP mode) may
+ // be changed and the user's policy mapping in memory is stale
+ // (because the policy change notification was missed by the current
+ // server).
+ //
+ // The "policy not found" error is ignored because the STS account may
+ // not have a policy mapped via its parent (for e.g. in
+ // OIDC/AssumeRoleWithCustomToken/AssumeRoleWithCertificate).
+ err := iamOS.loadMappedPolicy(ctx, svcParent, stsUser, false, cache.iamSTSPolicyMap)
+ if err != nil && !errors.Is(err, errNoSuchPolicy) {
+ return fmt.Errorf("unable to load the policy mapping for the STS user: %w", err)
+ }
+ }
+ }
+ if took := time.Since(stsPolicyMappingLoadStartTime); took > maxIAMLoadOpTime {
+ logger.Info("STS policy mappings load took %.2fs (for %d items)", took.Seconds(), stsPolicyMappingsCount)
+ }
+
+ // Copy svcUsersMap to cache.iamUsersMap
+ maps.Copy(cache.iamUsersMap, svcUsersMap)
+
+ cache.buildUserGroupMemberships()
+
+ purgeStart := time.Now()
+
+ // Purge expired STS credentials.
+
+ // Scan STS users on disk and purge expired ones.
+ stsAccountsFromStore := map[string]UserIdentity{}
+ stsAccPoliciesFromStore := xsync.NewMapOf[string, MappedPolicy]()
+ for _, item := range listedConfigItems[stsListKey] {
+ userName := path.Dir(item)
+ // loadUser() will delete expired user during the load.
+ err := iamOS.loadUser(ctx, userName, stsUser, stsAccountsFromStore)
+ if err != nil && !errors.Is(err, errNoSuchUser) {
+ iamLogIf(ctx, err)
+ }
+ // No need to return errors for failed expiration of STS users
+ }
+
+ // Loading the STS policy mappings from disk ensures that stale entries
+ // (removed during loadUser() in the loop above) are removed from memory.
+ for _, item := range listedConfigItems[policyDBSTSUsersListKey] {
+ stsName := strings.TrimSuffix(item, ".json")
+ err := iamOS.loadMappedPolicy(ctx, stsName, stsUser, false, stsAccPoliciesFromStore)
+ if err != nil && !errors.Is(err, errNoSuchPolicy) {
+ iamLogIf(ctx, err)
+ }
+ // No need to return errors for failed expiration of STS users
+ }
+
+ took := time.Since(purgeStart).Seconds()
+ if took > maxDurationSecondsForLog {
+ // Log if we took a lot of time to load.
+ logger.Info("IAM expired STS purge took %.2fs", took)
+ }
+
+ // Store the newly populated map in the iam cache. This takes care of
+ // removing stale entries from the existing map.
+ cache.iamSTSAccountsMap = stsAccountsFromStore
+
+ stsAccPoliciesFromStore.Range(func(k string, v MappedPolicy) bool {
+ cache.iamSTSPolicyMap.Store(k, v)
+ return true
+ })
+
+ return nil
+}
+
+func (iamOS *IAMObjectStore) savePolicyDoc(ctx context.Context, policyName string, p PolicyDoc) error {
+ return iamOS.saveIAMConfig(ctx, &p, getPolicyDocPath(policyName))
+}
+
+func (iamOS *IAMObjectStore) saveMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool, mp MappedPolicy, opts ...options) error {
+ return iamOS.saveIAMConfig(ctx, mp, getMappedPolicyPath(name, userType, isGroup), opts...)
+}
+
+func (iamOS *IAMObjectStore) saveUserIdentity(ctx context.Context, name string, userType IAMUserType, u UserIdentity, opts ...options) error {
+ return iamOS.saveIAMConfig(ctx, u, getUserIdentityPath(name, userType), opts...)
+}
+
+func (iamOS *IAMObjectStore) saveGroupInfo(ctx context.Context, name string, gi GroupInfo) error {
+ return iamOS.saveIAMConfig(ctx, gi, getGroupInfoPath(name))
+}
+
+func (iamOS *IAMObjectStore) deletePolicyDoc(ctx context.Context, name string) error {
+ err := iamOS.deleteIAMConfig(ctx, getPolicyDocPath(name))
+ if err == errConfigNotFound {
+ err = errNoSuchPolicy
+ }
+ return err
+}
+
+func (iamOS *IAMObjectStore) deleteMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool) error {
+ err := iamOS.deleteIAMConfig(ctx, getMappedPolicyPath(name, userType, isGroup))
+ if err == errConfigNotFound {
+ err = errNoSuchPolicy
+ }
+ return err
+}
+
+func (iamOS *IAMObjectStore) deleteUserIdentity(ctx context.Context, name string, userType IAMUserType) error {
+ err := iamOS.deleteIAMConfig(ctx, getUserIdentityPath(name, userType))
+ if err == errConfigNotFound {
+ err = errNoSuchUser
+ }
+ return err
+}
+
+func (iamOS *IAMObjectStore) deleteGroupInfo(ctx context.Context, name string) error {
+ err := iamOS.deleteIAMConfig(ctx, getGroupInfoPath(name))
+ if err == errConfigNotFound {
+ err = errNoSuchGroup
+ }
+ return err
+}
+
+// Lists objects in the minioMetaBucket at the given path prefix. All returned
+// items have the pathPrefix removed from their names.
+func listIAMConfigItems(ctx context.Context, objAPI ObjectLayer, pathPrefix string) <-chan itemOrErr[string] {
+ ch := make(chan itemOrErr[string])
+
+ go func() {
+ defer xioutil.SafeClose(ch)
+
+ // Allocate new results channel to receive ObjectInfo.
+ objInfoCh := make(chan itemOrErr[ObjectInfo])
+
+ if err := objAPI.Walk(ctx, minioMetaBucket, pathPrefix, objInfoCh, WalkOptions{}); err != nil {
+ select {
+ case ch <- itemOrErr[string]{Err: err}:
+ case <-ctx.Done():
+ }
+ return
+ }
+
+ for obj := range objInfoCh {
+ if obj.Err != nil {
+ select {
+ case ch <- itemOrErr[string]{Err: obj.Err}:
+ case <-ctx.Done():
+ return
+ }
+ }
+ item := strings.TrimPrefix(obj.Item.Name, pathPrefix)
+ item = strings.TrimSuffix(item, SlashSeparator)
+ select {
+ case ch <- itemOrErr[string]{Item: item}:
+ case <-ctx.Done():
+ return
+ }
+ }
+ }()
+
+ return ch
+}
diff --git a/cmd/iam-object-store_test.go b/cmd/iam-object-store_test.go
new file mode 100644
index 00000000..dc0ce132
--- /dev/null
+++ b/cmd/iam-object-store_test.go
@@ -0,0 +1,53 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "testing"
+)
+
+func TestSplitPath(t *testing.T) {
+ cases := []struct {
+ path string
+ secondIndex bool
+ expectedListKey, expectedItem string
+ }{
+ {"format.json", false, "format.json", ""},
+ {"users/tester.json", false, "users/", "tester.json"},
+ {"groups/test/group.json", false, "groups/", "test/group.json"},
+ {"policydb/groups/testgroup.json", true, "policydb/groups/", "testgroup.json"},
+ {
+ "policydb/sts-users/uid=slash/user,ou=people,ou=swengg,dc=min,dc=io.json", true,
+ "policydb/sts-users/", "uid=slash/user,ou=people,ou=swengg,dc=min,dc=io.json",
+ },
+ {
+ "policydb/sts-users/uid=slash/user/twice,ou=people,ou=swengg,dc=min,dc=io.json", true,
+ "policydb/sts-users/", "uid=slash/user/twice,ou=people,ou=swengg,dc=min,dc=io.json",
+ },
+ {
+ "policydb/groups/cn=project/d,ou=groups,ou=swengg,dc=min,dc=io.json", true,
+ "policydb/groups/", "cn=project/d,ou=groups,ou=swengg,dc=min,dc=io.json",
+ },
+ }
+ for i, test := range cases {
+ listKey, item := splitPath(test.path, test.secondIndex)
+ if listKey != test.expectedListKey || item != test.expectedItem {
+ t.Errorf("unexpected result on test[%v]: expected[%s, %s] but got [%s, %s]", i, test.expectedListKey, test.expectedItem, listKey, item)
+ }
+ }
+}
diff --git a/cmd/iam-store.go b/cmd/iam-store.go
new file mode 100644
index 00000000..46465e00
--- /dev/null
+++ b/cmd/iam-store.go
@@ -0,0 +1,3072 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "maps"
+ "path"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ jsoniter "github.com/json-iterator/go"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio-go/v7/pkg/set"
+ "github.com/minio/minio/internal/auth"
+ "github.com/minio/minio/internal/config"
+ "github.com/minio/minio/internal/config/identity/openid"
+ "github.com/minio/minio/internal/jwt"
+ "github.com/minio/pkg/v3/env"
+ "github.com/minio/pkg/v3/policy"
+ "github.com/minio/pkg/v3/sync/errgroup"
+ "github.com/puzpuzpuz/xsync/v3"
+ "golang.org/x/sync/singleflight"
+)
+
+const (
+ // IAM configuration directory.
+ iamConfigPrefix = minioConfigPrefix + "/iam"
+
+ // IAM users directory.
+ iamConfigUsersPrefix = iamConfigPrefix + "/users/"
+
+ // IAM service accounts directory.
+ iamConfigServiceAccountsPrefix = iamConfigPrefix + "/service-accounts/"
+
+ // IAM groups directory.
+ iamConfigGroupsPrefix = iamConfigPrefix + "/groups/"
+
+ // IAM policies directory.
+ iamConfigPoliciesPrefix = iamConfigPrefix + "/policies/"
+
+ // IAM sts directory.
+ iamConfigSTSPrefix = iamConfigPrefix + "/sts/"
+
+ // IAM Policy DB prefixes.
+ iamConfigPolicyDBPrefix = iamConfigPrefix + "/policydb/"
+ iamConfigPolicyDBUsersPrefix = iamConfigPolicyDBPrefix + "users/"
+ iamConfigPolicyDBSTSUsersPrefix = iamConfigPolicyDBPrefix + "sts-users/"
+ iamConfigPolicyDBServiceAccountsPrefix = iamConfigPolicyDBPrefix + "service-accounts/"
+ iamConfigPolicyDBGroupsPrefix = iamConfigPolicyDBPrefix + "groups/"
+
+ // IAM identity file which captures identity credentials.
+ iamIdentityFile = "identity.json"
+
+ // IAM policy file which provides policies for each users.
+ iamPolicyFile = "policy.json"
+
+ // IAM group members file
+ iamGroupMembersFile = "members.json"
+
+ // IAM format file
+ iamFormatFile = "format.json"
+
+ iamFormatVersion1 = 1
+
+ minServiceAccountExpiry time.Duration = 15 * time.Minute
+ maxServiceAccountExpiry time.Duration = 365 * 24 * time.Hour
+)
+
+var errInvalidSvcAcctExpiration = errors.New("invalid service account expiration")
+
+type iamFormat struct {
+ Version int `json:"version"`
+}
+
+func newIAMFormatVersion1() iamFormat {
+ return iamFormat{Version: iamFormatVersion1}
+}
+
+func getIAMFormatFilePath() string {
+ return iamConfigPrefix + SlashSeparator + iamFormatFile
+}
+
+func getUserIdentityPath(user string, userType IAMUserType) string {
+ var basePath string
+ switch userType {
+ case svcUser:
+ basePath = iamConfigServiceAccountsPrefix
+ case stsUser:
+ basePath = iamConfigSTSPrefix
+ default:
+ basePath = iamConfigUsersPrefix
+ }
+ return pathJoin(basePath, user, iamIdentityFile)
+}
+
+func saveIAMFormat(ctx context.Context, store IAMStorageAPI) error {
+ bootstrapTraceMsg("Load IAM format file")
+ var iamFmt iamFormat
+ path := getIAMFormatFilePath()
+ if err := store.loadIAMConfig(ctx, &iamFmt, path); err != nil && !errors.Is(err, errConfigNotFound) {
+ // if IAM format
+ return err
+ }
+
+ if iamFmt.Version >= iamFormatVersion1 {
+ // Nothing to do.
+ return nil
+ }
+
+ bootstrapTraceMsg("Write IAM format file")
+ // Save iam format to version 1.
+ return store.saveIAMConfig(ctx, newIAMFormatVersion1(), path)
+}
+
+func getGroupInfoPath(group string) string {
+ return pathJoin(iamConfigGroupsPrefix, group, iamGroupMembersFile)
+}
+
+func getPolicyDocPath(name string) string {
+ return pathJoin(iamConfigPoliciesPrefix, name, iamPolicyFile)
+}
+
+func getMappedPolicyPath(name string, userType IAMUserType, isGroup bool) string {
+ if isGroup {
+ return pathJoin(iamConfigPolicyDBGroupsPrefix, name+".json")
+ }
+ switch userType {
+ case svcUser:
+ return pathJoin(iamConfigPolicyDBServiceAccountsPrefix, name+".json")
+ case stsUser:
+ return pathJoin(iamConfigPolicyDBSTSUsersPrefix, name+".json")
+ default:
+ return pathJoin(iamConfigPolicyDBUsersPrefix, name+".json")
+ }
+}
+
+// UserIdentity represents a user's secret key and their status
+type UserIdentity struct {
+ Version int `json:"version"`
+ Credentials auth.Credentials `json:"credentials"`
+ UpdatedAt time.Time `json:"updatedAt"`
+}
+
+func newUserIdentity(cred auth.Credentials) UserIdentity {
+ return UserIdentity{Version: 1, Credentials: cred, UpdatedAt: UTCNow()}
+}
+
+// GroupInfo contains info about a group
+type GroupInfo struct {
+ Version int `json:"version"`
+ Status string `json:"status"`
+ Members []string `json:"members"`
+ UpdatedAt time.Time `json:"updatedAt"`
+}
+
+func newGroupInfo(members []string) GroupInfo {
+ return GroupInfo{Version: 1, Status: statusEnabled, Members: members, UpdatedAt: UTCNow()}
+}
+
+// MappedPolicy represents a policy name mapped to a user or group
+type MappedPolicy struct {
+ Version int `json:"version"`
+ Policies string `json:"policy"`
+ UpdatedAt time.Time `json:"updatedAt"`
+}
+
+// mappedPoliciesToMap copies the map of mapped policies to a regular map.
+func mappedPoliciesToMap(m *xsync.MapOf[string, MappedPolicy]) map[string]MappedPolicy {
+ policies := make(map[string]MappedPolicy, m.Size())
+ m.Range(func(k string, v MappedPolicy) bool {
+ policies[k] = v
+ return true
+ })
+ return policies
+}
+
+// converts a mapped policy into a slice of distinct policies
+func (mp MappedPolicy) toSlice() []string {
+ var policies []string
+ for policy := range strings.SplitSeq(mp.Policies, ",") {
+ if strings.TrimSpace(policy) == "" {
+ continue
+ }
+ policies = append(policies, policy)
+ }
+ return policies
+}
+
+func (mp MappedPolicy) policySet() set.StringSet {
+ return set.CreateStringSet(mp.toSlice()...)
+}
+
+func newMappedPolicy(policy string) MappedPolicy {
+ return MappedPolicy{Version: 1, Policies: policy, UpdatedAt: UTCNow()}
+}
+
+// PolicyDoc represents an IAM policy with some metadata.
+type PolicyDoc struct {
+ Version int `json:",omitempty"`
+ Policy policy.Policy
+ CreateDate time.Time
+ UpdateDate time.Time
+}
+
+func newPolicyDoc(p policy.Policy) PolicyDoc {
+ now := UTCNow().Round(time.Millisecond)
+ return PolicyDoc{
+ Version: 1,
+ Policy: p,
+ CreateDate: now,
+ UpdateDate: now,
+ }
+}
+
+// defaultPolicyDoc - used to wrap a default policy as PolicyDoc.
+func defaultPolicyDoc(p policy.Policy) PolicyDoc {
+ return PolicyDoc{
+ Version: 1,
+ Policy: p,
+ }
+}
+
+func (d *PolicyDoc) update(p policy.Policy) {
+ now := UTCNow().Round(time.Millisecond)
+ d.UpdateDate = now
+ if d.CreateDate.IsZero() {
+ d.CreateDate = now
+ }
+ d.Policy = p
+}
+
+// parseJSON parses both the old and the new format for storing policy
+// definitions.
+//
+// The on-disk format of policy definitions has changed (around early 12/2021)
+// from policy.Policy to PolicyDoc. To avoid a migration, loading supports
+// both the old and the new formats.
+func (d *PolicyDoc) parseJSON(data []byte) error {
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ var doc PolicyDoc
+ err := json.Unmarshal(data, &doc)
+ if err != nil {
+ err2 := json.Unmarshal(data, &doc.Policy)
+ if err2 != nil {
+ // Just return the first error.
+ return err
+ }
+ d.Policy = doc.Policy
+ return nil
+ }
+ *d = doc
+ return nil
+}
+
+// key options
+type options struct {
+ ttl int64 // expiry in seconds
+}
+
+type iamWatchEvent struct {
+ isCreated bool // !isCreated implies a delete event.
+ keyPath string
+}
+
+// iamCache contains in-memory cache of IAM data.
+type iamCache struct {
+ updatedAt time.Time
+
+ // map of policy names to policy definitions
+ iamPolicyDocsMap map[string]PolicyDoc
+
+ // map of regular username to credentials
+ iamUsersMap map[string]UserIdentity
+ // map of regular username to policy names
+ iamUserPolicyMap *xsync.MapOf[string, MappedPolicy]
+
+ // STS accounts are loaded on demand and not via the periodic IAM reload.
+ // map of STS access key to credentials
+ iamSTSAccountsMap map[string]UserIdentity
+ // map of STS access key to policy names
+ iamSTSPolicyMap *xsync.MapOf[string, MappedPolicy]
+
+ // map of group names to group info
+ iamGroupsMap map[string]GroupInfo
+ // map of user names to groups they are a member of
+ iamUserGroupMemberships map[string]set.StringSet
+ // map of group names to policy names
+ iamGroupPolicyMap *xsync.MapOf[string, MappedPolicy]
+}
+
+func newIamCache() *iamCache {
+ return &iamCache{
+ iamPolicyDocsMap: map[string]PolicyDoc{},
+ iamUsersMap: map[string]UserIdentity{},
+ iamUserPolicyMap: xsync.NewMapOf[string, MappedPolicy](),
+ iamSTSAccountsMap: map[string]UserIdentity{},
+ iamSTSPolicyMap: xsync.NewMapOf[string, MappedPolicy](),
+ iamGroupsMap: map[string]GroupInfo{},
+ iamUserGroupMemberships: map[string]set.StringSet{},
+ iamGroupPolicyMap: xsync.NewMapOf[string, MappedPolicy](),
+ }
+}
+
+// buildUserGroupMemberships - builds the memberships map. IMPORTANT:
+// Assumes that c.Lock is held by caller.
+func (c *iamCache) buildUserGroupMemberships() {
+ for group, gi := range c.iamGroupsMap {
+ c.updateGroupMembershipsMap(group, &gi)
+ }
+}
+
+// updateGroupMembershipsMap - updates the memberships map for a
+// group. IMPORTANT: Assumes c.Lock() is held by caller.
+func (c *iamCache) updateGroupMembershipsMap(group string, gi *GroupInfo) {
+ if gi == nil {
+ return
+ }
+ for _, member := range gi.Members {
+ v := c.iamUserGroupMemberships[member]
+ if v == nil {
+ v = set.CreateStringSet(group)
+ } else {
+ v.Add(group)
+ }
+ c.iamUserGroupMemberships[member] = v
+ }
+}
+
+// removeGroupFromMembershipsMap - removes the group from every member
+// in the cache. IMPORTANT: Assumes c.Lock() is held by caller.
+func (c *iamCache) removeGroupFromMembershipsMap(group string) {
+ for member, groups := range c.iamUserGroupMemberships {
+ if !groups.Contains(group) {
+ continue
+ }
+ groups.Remove(group)
+ c.iamUserGroupMemberships[member] = groups
+ }
+}
+
+func (c *iamCache) policyDBGetGroups(store *IAMStoreSys, userPolicyPresent bool, groups ...string) ([]string, error) {
+ var policies []string
+ for _, group := range groups {
+ if store.getUsersSysType() == MinIOUsersSysType {
+ g, ok := c.iamGroupsMap[group]
+ if !ok {
+ continue
+ }
+
+ // Group is disabled, so we return no policy - this
+ // ensures the request is denied.
+ if g.Status == statusDisabled {
+ continue
+ }
+ }
+
+ policy, ok := c.iamGroupPolicyMap.Load(group)
+ if !ok {
+ continue
+ }
+
+ policies = append(policies, policy.toSlice()...)
+ }
+
+ found := len(policies) > 0
+ if found {
+ return policies, nil
+ }
+
+ if userPolicyPresent {
+ // if user mapping present and no group policies found
+ // rely on user policy for access, instead of fallback.
+ return nil, nil
+ }
+
+ var mu sync.Mutex
+
+ // no mappings found, fallback for all groups.
+ g := errgroup.WithNErrs(len(groups)).WithConcurrency(10) // load like 10 groups at a time.
+
+ for index := range groups {
+ g.Go(func() error {
+ err := store.loadMappedPolicy(context.TODO(), groups[index], regUser, true, c.iamGroupPolicyMap)
+ if err != nil && !errors.Is(err, errNoSuchPolicy) {
+ return err
+ }
+ if errors.Is(err, errNoSuchPolicy) {
+ return nil
+ }
+ policy, _ := c.iamGroupPolicyMap.Load(groups[index])
+ mu.Lock()
+ policies = append(policies, policy.toSlice()...)
+ mu.Unlock()
+ return nil
+ }, index)
+ }
+
+ err := errors.Join(g.Wait()...)
+ return policies, err
+}
+
+// policyDBGet - lower-level helper; does not take locks.
+//
+// If a group is passed, it returns policies associated with the group.
+//
+// If a user is passed, it returns policies of the user along with any groups
+// that the server knows the user is a member of.
+//
+// In LDAP users mode, the server does not store any group membership
+// information in IAM (i.e sys.iam*Map) - this info is stored only in the STS
+// generated credentials. Thus we skip looking up group memberships, user map,
+// and group map and check the appropriate policy maps directly.
+func (c *iamCache) policyDBGet(store *IAMStoreSys, name string, isGroup bool, policyPresent bool) ([]string, time.Time, error) {
+ if isGroup {
+ if store.getUsersSysType() == MinIOUsersSysType {
+ g, ok := c.iamGroupsMap[name]
+ if !ok {
+ if err := store.loadGroup(context.Background(), name, c.iamGroupsMap); err != nil {
+ return nil, time.Time{}, err
+ }
+ g, ok = c.iamGroupsMap[name]
+ if !ok {
+ return nil, time.Time{}, errNoSuchGroup
+ }
+ }
+
+ // Group is disabled, so we return no policy - this
+ // ensures the request is denied.
+ if g.Status == statusDisabled {
+ return nil, time.Time{}, nil
+ }
+ }
+
+ policy, ok := c.iamGroupPolicyMap.Load(name)
+ if ok {
+ return policy.toSlice(), policy.UpdatedAt, nil
+ }
+ if !policyPresent {
+ if err := store.loadMappedPolicy(context.TODO(), name, regUser, true, c.iamGroupPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) {
+ return nil, time.Time{}, err
+ }
+ policy, _ = c.iamGroupPolicyMap.Load(name)
+ return policy.toSlice(), policy.UpdatedAt, nil
+ }
+ return nil, time.Time{}, nil
+ }
+
+ // returned policy could be empty, we use set to de-duplicate.
+ var policies set.StringSet
+ var updatedAt time.Time
+
+ if store.getUsersSysType() == LDAPUsersSysType {
+ // For LDAP policy mapping is part of STS users, we only need to lookup
+ // those mappings.
+ mp, ok := c.iamSTSPolicyMap.Load(name)
+ if !ok {
+ // Attempt to load parent user mapping for STS accounts
+ if err := store.loadMappedPolicy(context.TODO(), name, stsUser, false, c.iamSTSPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) {
+ return nil, time.Time{}, err
+ }
+ mp, _ = c.iamSTSPolicyMap.Load(name)
+ }
+ policies = set.CreateStringSet(mp.toSlice()...)
+ updatedAt = mp.UpdatedAt
+ } else {
+ // When looking for a user's policies, we also check if the user
+ // and the groups they are member of are enabled.
+ u, ok := c.iamUsersMap[name]
+ if ok {
+ if !u.Credentials.IsValid() {
+ return nil, time.Time{}, nil
+ }
+ }
+
+ // For internal IDP regular/service account user accounts, the policy
+ // mapping is iamUserPolicyMap. For STS accounts, the parent user would be
+ // passed here and we lookup the mapping in iamSTSPolicyMap.
+ mp, ok := c.iamUserPolicyMap.Load(name)
+ if !ok {
+ if err := store.loadMappedPolicy(context.TODO(), name, regUser, false, c.iamUserPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) {
+ return nil, time.Time{}, err
+ }
+ mp, ok = c.iamUserPolicyMap.Load(name)
+ if !ok {
+ // Since user "name" could be a parent user of an STS account, we look up
+ // mappings for those too.
+ mp, ok = c.iamSTSPolicyMap.Load(name)
+ if !ok {
+ // Attempt to load parent user mapping for STS accounts
+ if err := store.loadMappedPolicy(context.TODO(), name, stsUser, false, c.iamSTSPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) {
+ return nil, time.Time{}, err
+ }
+ mp, _ = c.iamSTSPolicyMap.Load(name)
+ }
+ }
+ }
+ policies = set.CreateStringSet(mp.toSlice()...)
+
+ for _, group := range u.Credentials.Groups {
+ g, ok := c.iamGroupsMap[group]
+ if ok {
+ // Group is disabled, so we return no policy - this
+ // ensures the request is denied.
+ if g.Status == statusDisabled {
+ return nil, time.Time{}, nil
+ }
+ }
+
+ policy, ok := c.iamGroupPolicyMap.Load(group)
+ if !ok {
+ if err := store.loadMappedPolicy(context.TODO(), group, regUser, true, c.iamGroupPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) {
+ return nil, time.Time{}, err
+ }
+ policy, _ = c.iamGroupPolicyMap.Load(group)
+ }
+
+ for _, p := range policy.toSlice() {
+ policies.Add(p)
+ }
+ }
+ updatedAt = mp.UpdatedAt
+ }
+
+ for _, group := range c.iamUserGroupMemberships[name].ToSlice() {
+ if store.getUsersSysType() == MinIOUsersSysType {
+ g, ok := c.iamGroupsMap[group]
+ if ok {
+ // Group is disabled, so we return no policy - this
+ // ensures the request is denied.
+ if g.Status == statusDisabled {
+ return nil, time.Time{}, nil
+ }
+ }
+ }
+
+ policy, ok := c.iamGroupPolicyMap.Load(group)
+ if !ok {
+ if err := store.loadMappedPolicy(context.TODO(), group, regUser, true, c.iamGroupPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) {
+ return nil, time.Time{}, err
+ }
+ policy, _ = c.iamGroupPolicyMap.Load(group)
+ }
+
+ for _, p := range policy.toSlice() {
+ policies.Add(p)
+ }
+ }
+
+ return policies.ToSlice(), updatedAt, nil
+}
+
+func (c *iamCache) updateUserWithClaims(key string, u UserIdentity) error {
+ if u.Credentials.SessionToken != "" {
+ jwtClaims, err := extractJWTClaims(u)
+ if err != nil {
+ return err
+ }
+ u.Credentials.Claims = jwtClaims.Map()
+ }
+ if u.Credentials.IsTemp() && !u.Credentials.IsServiceAccount() {
+ c.iamSTSAccountsMap[key] = u
+ } else {
+ c.iamUsersMap[key] = u
+ }
+ c.updatedAt = time.Now()
+ return nil
+}
+
+// IAMStorageAPI defines an interface for the IAM persistence layer
+type IAMStorageAPI interface {
+ // The role of the read-write lock is to prevent go routines from
+ // concurrently reading and writing the IAM storage. The (r)lock()
+ // functions return the iamCache. The cache can be safely written to
+ // only when returned by `lock()`.
+ lock() *iamCache
+ unlock()
+ rlock() *iamCache
+ runlock()
+ getUsersSysType() UsersSysType
+ loadPolicyDoc(ctx context.Context, policy string, m map[string]PolicyDoc) error
+ loadPolicyDocWithRetry(ctx context.Context, policy string, m map[string]PolicyDoc, retries int) error
+ loadPolicyDocs(ctx context.Context, m map[string]PolicyDoc) error
+ loadUser(ctx context.Context, user string, userType IAMUserType, m map[string]UserIdentity) error
+ loadSecretKey(ctx context.Context, user string, userType IAMUserType) (string, error)
+ loadUsers(ctx context.Context, userType IAMUserType, m map[string]UserIdentity) error
+ loadGroup(ctx context.Context, group string, m map[string]GroupInfo) error
+ loadGroups(ctx context.Context, m map[string]GroupInfo) error
+ loadMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool, m *xsync.MapOf[string, MappedPolicy]) error
+ loadMappedPolicyWithRetry(ctx context.Context, name string, userType IAMUserType, isGroup bool, m *xsync.MapOf[string, MappedPolicy], retries int) error
+ loadMappedPolicies(ctx context.Context, userType IAMUserType, isGroup bool, m *xsync.MapOf[string, MappedPolicy]) error
+ saveIAMConfig(ctx context.Context, item any, path string, opts ...options) error
+ loadIAMConfig(ctx context.Context, item any, path string) error
+ deleteIAMConfig(ctx context.Context, path string) error
+ savePolicyDoc(ctx context.Context, policyName string, p PolicyDoc) error
+ saveMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool, mp MappedPolicy, opts ...options) error
+ saveUserIdentity(ctx context.Context, name string, userType IAMUserType, u UserIdentity, opts ...options) error
+ saveGroupInfo(ctx context.Context, group string, gi GroupInfo) error
+ deletePolicyDoc(ctx context.Context, policyName string) error
+ deleteMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool) error
+ deleteUserIdentity(ctx context.Context, name string, userType IAMUserType) error
+ deleteGroupInfo(ctx context.Context, name string) error
+}
+
+// iamStorageWatcher is implemented by `IAMStorageAPI` implementers that
+// additionally support watching storage for changes.
+type iamStorageWatcher interface {
+ watch(ctx context.Context, keyPath string) <-chan iamWatchEvent
+}
+
+// Set default canned policies only if not already overridden by users.
+func setDefaultCannedPolicies(policies map[string]PolicyDoc) {
+ for _, v := range policy.DefaultPolicies {
+ if _, ok := policies[v.Name]; !ok {
+ policies[v.Name] = defaultPolicyDoc(v.Definition)
+ }
+ }
+}
+
+// LoadIAMCache reads all IAM items and populates a new iamCache object and
+// replaces the in-memory cache object.
+func (store *IAMStoreSys) LoadIAMCache(ctx context.Context, firstTime bool) error {
+ bootstrapTraceMsgFirstTime := func(s string) {
+ if firstTime {
+ bootstrapTraceMsg(s)
+ }
+ }
+ bootstrapTraceMsgFirstTime("loading IAM data")
+
+ newCache := newIamCache()
+
+ loadedAt := time.Now()
+
+ if iamOS, ok := store.IAMStorageAPI.(*IAMObjectStore); ok {
+ err := iamOS.loadAllFromObjStore(ctx, newCache, firstTime)
+ if err != nil {
+ return err
+ }
+ } else {
+ // Only non-object IAM store (i.e. only etcd backend).
+ bootstrapTraceMsgFirstTime("loading policy documents")
+ if err := store.loadPolicyDocs(ctx, newCache.iamPolicyDocsMap); err != nil {
+ return err
+ }
+
+ // Sets default canned policies, if none are set.
+ setDefaultCannedPolicies(newCache.iamPolicyDocsMap)
+
+ if store.getUsersSysType() == MinIOUsersSysType {
+ bootstrapTraceMsgFirstTime("loading regular users")
+ if err := store.loadUsers(ctx, regUser, newCache.iamUsersMap); err != nil {
+ return err
+ }
+ bootstrapTraceMsgFirstTime("loading regular groups")
+ if err := store.loadGroups(ctx, newCache.iamGroupsMap); err != nil {
+ return err
+ }
+ }
+
+ bootstrapTraceMsgFirstTime("loading user policy mapping")
+ // load polices mapped to users
+ if err := store.loadMappedPolicies(ctx, regUser, false, newCache.iamUserPolicyMap); err != nil {
+ return err
+ }
+
+ bootstrapTraceMsgFirstTime("loading group policy mapping")
+ // load policies mapped to groups
+ if err := store.loadMappedPolicies(ctx, regUser, true, newCache.iamGroupPolicyMap); err != nil {
+ return err
+ }
+
+ bootstrapTraceMsgFirstTime("loading service accounts")
+ // load service accounts
+ if err := store.loadUsers(ctx, svcUser, newCache.iamUsersMap); err != nil {
+ return err
+ }
+
+ newCache.buildUserGroupMemberships()
+ }
+
+ cache := store.lock()
+ defer store.unlock()
+
+ // We should only update the in-memory cache if there were no changes
+ // to the in-memory cache since the disk loading began. If there
+ // were changes to the in-memory cache we should wait for the next
+ // cycle until we can safely update the in-memory cache.
+ //
+ // An in-memory cache must be replaced only if we know for sure that the
+ // values loaded from disk are not stale. They might be stale if the
+ // cached.updatedAt is more recent than the refresh cycle began.
+ if cache.updatedAt.Before(loadedAt) || firstTime {
+ // No one has updated anything since the config was loaded,
+ // so we just replace whatever is on the disk into memory.
+ cache.iamGroupPolicyMap = newCache.iamGroupPolicyMap
+ cache.iamGroupsMap = newCache.iamGroupsMap
+ cache.iamPolicyDocsMap = newCache.iamPolicyDocsMap
+ cache.iamUserGroupMemberships = newCache.iamUserGroupMemberships
+ cache.iamUserPolicyMap = newCache.iamUserPolicyMap
+ cache.iamUsersMap = newCache.iamUsersMap
+ // For STS policy map, we need to merge the new cache with the existing
+ // cache because the periodic IAM reload is partial. The periodic load
+ // here is to account for STS policy mapping changes that should apply
+ // for service accounts derived from such STS accounts (i.e. LDAP STS
+ // accounts).
+ newCache.iamSTSPolicyMap.Range(func(k string, v MappedPolicy) bool {
+ cache.iamSTSPolicyMap.Store(k, v)
+ return true
+ })
+
+ cache.updatedAt = time.Now()
+ }
+
+ return nil
+}
+
+// IAMStoreSys contains IAMStorageAPI to add higher-level methods on the storage
+// layer.
+type IAMStoreSys struct {
+ IAMStorageAPI
+
+ group *singleflight.Group
+ policy *singleflight.Group
+}
+
+// HasWatcher - returns if the storage system has a watcher.
+func (store *IAMStoreSys) HasWatcher() bool {
+ _, ok := store.IAMStorageAPI.(iamStorageWatcher)
+ return ok
+}
+
+// GetUser - fetches credential from memory.
+func (store *IAMStoreSys) GetUser(user string) (UserIdentity, bool) {
+ cache := store.rlock()
+ defer store.runlock()
+
+ u, ok := cache.iamUsersMap[user]
+ if !ok {
+ // Check the sts map
+ u, ok = cache.iamSTSAccountsMap[user]
+ }
+ return u, ok
+}
+
+// GetMappedPolicy - fetches mapped policy from memory.
+func (store *IAMStoreSys) GetMappedPolicy(name string, isGroup bool) (MappedPolicy, bool) {
+ cache := store.rlock()
+ defer store.runlock()
+
+ if isGroup {
+ v, ok := cache.iamGroupPolicyMap.Load(name)
+ return v, ok
+ }
+ return cache.iamUserPolicyMap.Load(name)
+}
+
+// GroupNotificationHandler - updates in-memory cache on notification of
+// change (e.g. peer notification for object storage and etcd watch
+// notification).
+func (store *IAMStoreSys) GroupNotificationHandler(ctx context.Context, group string) error {
+ cache := store.lock()
+ defer store.unlock()
+
+ err := store.loadGroup(ctx, group, cache.iamGroupsMap)
+ if err != nil && err != errNoSuchGroup {
+ return err
+ }
+
+ if err == errNoSuchGroup {
+ // group does not exist - so remove from memory.
+ cache.removeGroupFromMembershipsMap(group)
+ delete(cache.iamGroupsMap, group)
+ cache.iamGroupPolicyMap.Delete(group)
+
+ cache.updatedAt = time.Now()
+ return nil
+ }
+
+ gi := cache.iamGroupsMap[group]
+
+ // Updating the group memberships cache happens in two steps:
+ //
+ // 1. Remove the group from each user's list of memberships.
+ // 2. Add the group to each member's list of memberships.
+ //
+ // This ensures that regardless of members being added or
+ // removed, the cache stays current.
+ cache.removeGroupFromMembershipsMap(group)
+ cache.updateGroupMembershipsMap(group, &gi)
+ cache.updatedAt = time.Now()
+ return nil
+}
+
+// PolicyDBGet - fetches policies associated with the given user or group, and
+// additional groups if provided.
+func (store *IAMStoreSys) PolicyDBGet(name string, groups ...string) ([]string, error) {
+ if name == "" {
+ return nil, errInvalidArgument
+ }
+
+ cache := store.rlock()
+ defer store.runlock()
+
+ getPolicies := func() ([]string, error) {
+ policies, _, err := cache.policyDBGet(store, name, false, false)
+ if err != nil {
+ return nil, err
+ }
+
+ userPolicyPresent := len(policies) > 0
+
+ groupPolicies, err := cache.policyDBGetGroups(store, userPolicyPresent, groups...)
+ if err != nil {
+ return nil, err
+ }
+
+ policies = append(policies, groupPolicies...)
+ return policies, nil
+ }
+ if store.policy != nil {
+ val, err, _ := store.policy.Do(name, func() (any, error) {
+ return getPolicies()
+ })
+ if err != nil {
+ return nil, err
+ }
+ res, ok := val.([]string)
+ if !ok {
+ return nil, errors.New("unexpected policy type")
+ }
+ return res, nil
+ }
+ return getPolicies()
+}
+
+// AddUsersToGroup - adds users to group, creating the group if needed.
+func (store *IAMStoreSys) AddUsersToGroup(ctx context.Context, group string, members []string) (updatedAt time.Time, err error) {
+ if group == "" {
+ return updatedAt, errInvalidArgument
+ }
+
+ cache := store.lock()
+ defer store.unlock()
+
+ // Validate that all members exist.
+ for _, member := range members {
+ u, ok := cache.iamUsersMap[member]
+ if !ok {
+ return updatedAt, errNoSuchUser
+ }
+ cr := u.Credentials
+ if cr.IsTemp() || cr.IsServiceAccount() {
+ return updatedAt, errIAMActionNotAllowed
+ }
+ }
+
+ gi, ok := cache.iamGroupsMap[group]
+ if !ok {
+ // Set group as enabled by default when it doesn't
+ // exist.
+ gi = newGroupInfo(members)
+ } else {
+ gi.Members = set.CreateStringSet(append(gi.Members, members...)...).ToSlice()
+ gi.UpdatedAt = UTCNow()
+ }
+
+ if err := store.saveGroupInfo(ctx, group, gi); err != nil {
+ return updatedAt, err
+ }
+
+ cache.iamGroupsMap[group] = gi
+
+ // update user-group membership map
+ for _, member := range members {
+ gset := cache.iamUserGroupMemberships[member]
+ if gset == nil {
+ gset = set.CreateStringSet(group)
+ } else {
+ gset.Add(group)
+ }
+ cache.iamUserGroupMemberships[member] = gset
+ }
+
+ cache.updatedAt = time.Now()
+ return gi.UpdatedAt, nil
+}
+
+// helper function - does not take any locks. Updates only cache if
+// updateCacheOnly is set.
+func removeMembersFromGroup(ctx context.Context, store *IAMStoreSys, cache *iamCache, group string, members []string, updateCacheOnly bool) (updatedAt time.Time, err error) {
+ gi, ok := cache.iamGroupsMap[group]
+ if !ok {
+ return updatedAt, errNoSuchGroup
+ }
+
+ s := set.CreateStringSet(gi.Members...)
+ d := set.CreateStringSet(members...)
+ gi.Members = s.Difference(d).ToSlice()
+
+ if !updateCacheOnly {
+ err := store.saveGroupInfo(ctx, group, gi)
+ if err != nil {
+ return updatedAt, err
+ }
+ }
+ gi.UpdatedAt = UTCNow()
+ cache.iamGroupsMap[group] = gi
+
+ // update user-group membership map
+ for _, member := range members {
+ gset := cache.iamUserGroupMemberships[member]
+ if gset == nil {
+ continue
+ }
+ gset.Remove(group)
+ cache.iamUserGroupMemberships[member] = gset
+ }
+
+ cache.updatedAt = time.Now()
+ return gi.UpdatedAt, nil
+}
+
+// RemoveUsersFromGroup - removes users from group, deleting it if it is empty.
+func (store *IAMStoreSys) RemoveUsersFromGroup(ctx context.Context, group string, members []string) (updatedAt time.Time, err error) {
+ if group == "" {
+ return updatedAt, errInvalidArgument
+ }
+
+ cache := store.lock()
+ defer store.unlock()
+
+ // Validate that all members exist.
+ for _, member := range members {
+ u, ok := cache.iamUsersMap[member]
+ if !ok {
+ return updatedAt, errNoSuchUser
+ }
+ cr := u.Credentials
+ if cr.IsTemp() || cr.IsServiceAccount() {
+ return updatedAt, errIAMActionNotAllowed
+ }
+ }
+
+ gi, ok := cache.iamGroupsMap[group]
+ if !ok {
+ return updatedAt, errNoSuchGroup
+ }
+
+ // Check if attempting to delete a non-empty group.
+ if len(members) == 0 && len(gi.Members) != 0 {
+ return updatedAt, errGroupNotEmpty
+ }
+
+ if len(members) == 0 {
+ // len(gi.Members) == 0 here.
+
+ // Remove the group from storage. First delete the
+ // mapped policy. No-mapped-policy case is ignored.
+ if err := store.deleteMappedPolicy(ctx, group, regUser, true); err != nil && !errors.Is(err, errNoSuchPolicy) {
+ return updatedAt, err
+ }
+ if err := store.deleteGroupInfo(ctx, group); err != nil && err != errNoSuchGroup {
+ return updatedAt, err
+ }
+
+ // Delete from server memory
+ delete(cache.iamGroupsMap, group)
+ cache.iamGroupPolicyMap.Delete(group)
+ cache.updatedAt = time.Now()
+ return cache.updatedAt, nil
+ }
+
+ return removeMembersFromGroup(ctx, store, cache, group, members, false)
+}
+
+// SetGroupStatus - updates group status
+func (store *IAMStoreSys) SetGroupStatus(ctx context.Context, group string, enabled bool) (updatedAt time.Time, err error) {
+ if group == "" {
+ return updatedAt, errInvalidArgument
+ }
+
+ cache := store.lock()
+ defer store.unlock()
+
+ gi, ok := cache.iamGroupsMap[group]
+ if !ok {
+ return updatedAt, errNoSuchGroup
+ }
+
+ if enabled {
+ gi.Status = statusEnabled
+ } else {
+ gi.Status = statusDisabled
+ }
+ gi.UpdatedAt = UTCNow()
+ if err := store.saveGroupInfo(ctx, group, gi); err != nil {
+ return gi.UpdatedAt, err
+ }
+
+ cache.iamGroupsMap[group] = gi
+ cache.updatedAt = time.Now()
+
+ return gi.UpdatedAt, nil
+}
+
+// GetGroupDescription - builds up group description
+func (store *IAMStoreSys) GetGroupDescription(group string) (gd madmin.GroupDesc, err error) {
+ cache := store.rlock()
+ defer store.runlock()
+
+ ps, updatedAt, err := cache.policyDBGet(store, group, true, false)
+ if err != nil {
+ return gd, err
+ }
+
+ policy := strings.Join(ps, ",")
+
+ if store.getUsersSysType() != MinIOUsersSysType {
+ return madmin.GroupDesc{
+ Name: group,
+ Policy: policy,
+ UpdatedAt: updatedAt,
+ }, nil
+ }
+
+ gi, ok := cache.iamGroupsMap[group]
+ if !ok {
+ return gd, errNoSuchGroup
+ }
+
+ return madmin.GroupDesc{
+ Name: group,
+ Status: gi.Status,
+ Members: gi.Members,
+ Policy: policy,
+ UpdatedAt: gi.UpdatedAt,
+ }, nil
+}
+
+// updateGroups updates the group from the persistent store, and also related policy mapping if any.
+func (store *IAMStoreSys) updateGroups(ctx context.Context, cache *iamCache) (res []string, err error) {
+ groupSet := set.NewStringSet()
+ if iamOS, ok := store.IAMStorageAPI.(*IAMObjectStore); ok {
+ listedConfigItems, err := iamOS.listAllIAMConfigItems(ctx)
+ if err != nil {
+ return nil, err
+ }
+ if store.getUsersSysType() == MinIOUsersSysType {
+ groupsList := listedConfigItems[groupsListKey]
+ for _, item := range groupsList {
+ group := path.Dir(item)
+ if err = iamOS.loadGroup(ctx, group, cache.iamGroupsMap); err != nil && !errors.Is(err, errNoSuchGroup) {
+ return nil, fmt.Errorf("unable to load the group: %w", err)
+ }
+ groupSet.Add(group)
+ }
+ }
+
+ groupPolicyMappingsList := listedConfigItems[policyDBGroupsListKey]
+ for _, item := range groupPolicyMappingsList {
+ group := strings.TrimSuffix(item, ".json")
+ if err = iamOS.loadMappedPolicy(ctx, group, regUser, true, cache.iamGroupPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) {
+ return nil, fmt.Errorf("unable to load the policy mapping for the group: %w", err)
+ }
+ groupSet.Add(group)
+ }
+
+ return groupSet.ToSlice(), nil
+ }
+
+ // For etcd just return from cache.
+ for k := range cache.iamGroupsMap {
+ groupSet.Add(k)
+ }
+
+ cache.iamGroupPolicyMap.Range(func(k string, v MappedPolicy) bool {
+ groupSet.Add(k)
+ return true
+ })
+
+ return groupSet.ToSlice(), nil
+}
+
+// ListGroups - lists groups. Since this is not going to be a frequent
+// operation, we fetch this info from storage, and refresh the cache as well.
+func (store *IAMStoreSys) ListGroups(ctx context.Context) (res []string, err error) {
+ cache := store.lock()
+ defer store.unlock()
+
+ return store.updateGroups(ctx, cache)
+}
+
+// listGroups - lists groups - fetch groups from cache
+func (store *IAMStoreSys) listGroups(ctx context.Context) (res []string, err error) {
+ cache := store.rlock()
+ defer store.runlock()
+
+ if store.getUsersSysType() == MinIOUsersSysType {
+ for k := range cache.iamGroupsMap {
+ res = append(res, k)
+ }
+ }
+
+ if store.getUsersSysType() == LDAPUsersSysType {
+ cache.iamGroupPolicyMap.Range(func(k string, _ MappedPolicy) bool {
+ res = append(res, k)
+ return true
+ })
+ }
+ return res, err
+}
+
+// PolicyDBUpdate - adds or removes given policies to/from the user or group's
+// policy associations.
+func (store *IAMStoreSys) PolicyDBUpdate(ctx context.Context, name string, isGroup bool,
+ userType IAMUserType, policies []string, isAttach bool) (updatedAt time.Time,
+ addedOrRemoved, effectivePolicies []string, err error,
+) {
+ if name == "" {
+ err = errInvalidArgument
+ return updatedAt, addedOrRemoved, effectivePolicies, err
+ }
+
+ cache := store.lock()
+ defer store.unlock()
+
+ // Load existing policy mapping
+ var mp MappedPolicy
+ if !isGroup {
+ if userType == stsUser {
+ stsMap := xsync.NewMapOf[string, MappedPolicy]()
+
+ // Attempt to load parent user mapping for STS accounts
+ store.loadMappedPolicy(context.TODO(), name, stsUser, false, stsMap)
+
+ mp, _ = stsMap.Load(name)
+ } else {
+ mp, _ = cache.iamUserPolicyMap.Load(name)
+ }
+ } else {
+ if store.getUsersSysType() == MinIOUsersSysType {
+ g, ok := cache.iamGroupsMap[name]
+ if !ok {
+ err = errNoSuchGroup
+ return updatedAt, addedOrRemoved, effectivePolicies, err
+ }
+
+ if g.Status == statusDisabled {
+ err = errGroupDisabled
+ return updatedAt, addedOrRemoved, effectivePolicies, err
+ }
+ }
+ mp, _ = cache.iamGroupPolicyMap.Load(name)
+ }
+
+ // Compute net policy change effect and updated policy mapping
+ existingPolicySet := mp.policySet()
+ policiesToUpdate := set.CreateStringSet(policies...)
+ var newPolicySet set.StringSet
+ newPolicyMapping := mp
+ if isAttach {
+ // new policies to attach => inputPolicies - existing (set difference)
+ policiesToUpdate = policiesToUpdate.Difference(existingPolicySet)
+ // validate that new policies to add are defined.
+ for _, p := range policiesToUpdate.ToSlice() {
+ if _, found := cache.iamPolicyDocsMap[p]; !found {
+ err = errNoSuchPolicy
+ return updatedAt, addedOrRemoved, effectivePolicies, err
+ }
+ }
+ newPolicySet = existingPolicySet.Union(policiesToUpdate)
+ } else {
+ // policies to detach => inputPolicies ∩ existing (intersection)
+ policiesToUpdate = policiesToUpdate.Intersection(existingPolicySet)
+ newPolicySet = existingPolicySet.Difference(policiesToUpdate)
+ }
+ // We return an error if the requested policy update will have no effect.
+ if policiesToUpdate.IsEmpty() {
+ err = errNoPolicyToAttachOrDetach
+ return updatedAt, addedOrRemoved, effectivePolicies, err
+ }
+
+ newPolicies := newPolicySet.ToSlice()
+ newPolicyMapping.Policies = strings.Join(newPolicies, ",")
+ newPolicyMapping.UpdatedAt = UTCNow()
+ addedOrRemoved = policiesToUpdate.ToSlice()
+
+ // In case of detach operation, it is possible that no policies are mapped -
+ // in this case, we delete the mapping from the store.
+ if len(newPolicies) == 0 {
+ if err = store.deleteMappedPolicy(ctx, name, userType, isGroup); err != nil && !errors.Is(err, errNoSuchPolicy) {
+ return updatedAt, addedOrRemoved, effectivePolicies, err
+ }
+ if !isGroup {
+ if userType == stsUser {
+ cache.iamSTSPolicyMap.Delete(name)
+ } else {
+ cache.iamUserPolicyMap.Delete(name)
+ }
+ } else {
+ cache.iamGroupPolicyMap.Delete(name)
+ }
+ } else {
+ if err = store.saveMappedPolicy(ctx, name, userType, isGroup, newPolicyMapping); err != nil {
+ return updatedAt, addedOrRemoved, effectivePolicies, err
+ }
+ if !isGroup {
+ if userType == stsUser {
+ cache.iamSTSPolicyMap.Store(name, newPolicyMapping)
+ } else {
+ cache.iamUserPolicyMap.Store(name, newPolicyMapping)
+ }
+ } else {
+ cache.iamGroupPolicyMap.Store(name, newPolicyMapping)
+ }
+ }
+
+ cache.updatedAt = UTCNow()
+ return cache.updatedAt, addedOrRemoved, newPolicies, nil
+}
+
+// PolicyDBSet - update the policy mapping for the given user or group in
+// storage and in cache. We do not check for the existence of the user here
+// since users can be virtual, such as for:
+// - LDAP users
+// - CommonName for STS accounts generated by AssumeRoleWithCertificate
+func (store *IAMStoreSys) PolicyDBSet(ctx context.Context, name, policy string, userType IAMUserType, isGroup bool) (updatedAt time.Time, err error) {
+ if name == "" {
+ return updatedAt, errInvalidArgument
+ }
+
+ cache := store.lock()
+ defer store.unlock()
+
+ // Handle policy mapping removal.
+ if policy == "" {
+ if store.getUsersSysType() == LDAPUsersSysType {
+ // Add a fallback removal towards previous content that may come back
+ // as a ghost user due to lack of delete, this change occurred
+ // introduced in PR #11840
+ store.deleteMappedPolicy(ctx, name, regUser, false)
+ }
+ err := store.deleteMappedPolicy(ctx, name, userType, isGroup)
+ if err != nil && !errors.Is(err, errNoSuchPolicy) {
+ return updatedAt, err
+ }
+ if !isGroup {
+ if userType == stsUser {
+ cache.iamSTSPolicyMap.Delete(name)
+ } else {
+ cache.iamUserPolicyMap.Delete(name)
+ }
+ } else {
+ cache.iamGroupPolicyMap.Delete(name)
+ }
+ cache.updatedAt = time.Now()
+ return cache.updatedAt, nil
+ }
+
+ // Handle policy mapping set/update
+ mp := newMappedPolicy(policy)
+ for _, p := range mp.toSlice() {
+ if _, found := cache.iamPolicyDocsMap[p]; !found {
+ return updatedAt, errNoSuchPolicy
+ }
+ }
+
+ if err := store.saveMappedPolicy(ctx, name, userType, isGroup, mp); err != nil {
+ return updatedAt, err
+ }
+ if !isGroup {
+ if userType == stsUser {
+ cache.iamSTSPolicyMap.Store(name, mp)
+ } else {
+ cache.iamUserPolicyMap.Store(name, mp)
+ }
+ } else {
+ cache.iamGroupPolicyMap.Store(name, mp)
+ }
+ cache.updatedAt = time.Now()
+ return mp.UpdatedAt, nil
+}
+
+// PolicyNotificationHandler - loads given policy from storage. If not present,
+// deletes from cache. This notification only reads from storage, and updates
+// cache. When the notification is for a policy deletion, it updates the
+// user-policy and group-policy maps as well.
+func (store *IAMStoreSys) PolicyNotificationHandler(ctx context.Context, policy string) error {
+ if policy == "" {
+ return errInvalidArgument
+ }
+
+ cache := store.lock()
+ defer store.unlock()
+
+ err := store.loadPolicyDoc(ctx, policy, cache.iamPolicyDocsMap)
+ if errors.Is(err, errNoSuchPolicy) {
+ // policy was deleted, update cache.
+ delete(cache.iamPolicyDocsMap, policy)
+
+ // update user policy map
+ cache.iamUserPolicyMap.Range(func(u string, mp MappedPolicy) bool {
+ pset := mp.policySet()
+ if !pset.Contains(policy) {
+ return true
+ }
+ if store.getUsersSysType() == MinIOUsersSysType {
+ _, ok := cache.iamUsersMap[u]
+ if !ok {
+ // happens when account is deleted or
+ // expired.
+ cache.iamUserPolicyMap.Delete(u)
+ return true
+ }
+ }
+ pset.Remove(policy)
+ cache.iamUserPolicyMap.Store(u, newMappedPolicy(strings.Join(pset.ToSlice(), ",")))
+ return true
+ })
+
+ // update group policy map
+ cache.iamGroupPolicyMap.Range(func(g string, mp MappedPolicy) bool {
+ pset := mp.policySet()
+ if !pset.Contains(policy) {
+ return true
+ }
+ pset.Remove(policy)
+ cache.iamGroupPolicyMap.Store(g, newMappedPolicy(strings.Join(pset.ToSlice(), ",")))
+ return true
+ })
+
+ cache.updatedAt = time.Now()
+ return nil
+ }
+ return err
+}
+
+// DeletePolicy - deletes policy from storage and cache. When this called in
+// response to a notification (i.e. isFromNotification = true), it skips the
+// validation of policy usage and the attempt to delete in the backend as well
+// (as this is already done by the notifying node).
+func (store *IAMStoreSys) DeletePolicy(ctx context.Context, policy string, isFromNotification bool) error {
+ if policy == "" {
+ return errInvalidArgument
+ }
+
+ cache := store.lock()
+ defer store.unlock()
+
+ if !isFromNotification {
+ // Check if policy is mapped to any existing user or group. If so, we do not
+ // allow deletion of the policy. If the policy is mapped to an STS account,
+ // we do allow deletion.
+ users := []string{}
+ groups := []string{}
+ cache.iamUserPolicyMap.Range(func(u string, mp MappedPolicy) bool {
+ pset := mp.policySet()
+ if store.getUsersSysType() == MinIOUsersSysType {
+ if _, ok := cache.iamUsersMap[u]; !ok {
+ // This case can happen when a temporary account is
+ // deleted or expired - remove it from userPolicyMap.
+ cache.iamUserPolicyMap.Delete(u)
+ return true
+ }
+ }
+ if pset.Contains(policy) {
+ users = append(users, u)
+ }
+ return true
+ })
+ cache.iamGroupPolicyMap.Range(func(g string, mp MappedPolicy) bool {
+ pset := mp.policySet()
+ if pset.Contains(policy) {
+ groups = append(groups, g)
+ }
+ return true
+ })
+ if len(users) != 0 || len(groups) != 0 {
+ return errPolicyInUse
+ }
+
+ err := store.deletePolicyDoc(ctx, policy)
+ if errors.Is(err, errNoSuchPolicy) {
+ // Ignore error if policy is already deleted.
+ err = nil
+ }
+ if err != nil {
+ return err
+ }
+ }
+
+ delete(cache.iamPolicyDocsMap, policy)
+ cache.updatedAt = time.Now()
+
+ return nil
+}
+
+// GetPolicy - gets the policy definition. Allows specifying multiple comma
+// separated policies - returns a combined policy.
+func (store *IAMStoreSys) GetPolicy(name string) (policy.Policy, error) {
+ if name == "" {
+ return policy.Policy{}, errInvalidArgument
+ }
+
+ cache := store.rlock()
+ defer store.runlock()
+
+ policies := newMappedPolicy(name).toSlice()
+ var toMerge []policy.Policy
+ for _, policy := range policies {
+ if policy == "" {
+ continue
+ }
+ v, ok := cache.iamPolicyDocsMap[policy]
+ if !ok {
+ return v.Policy, errNoSuchPolicy
+ }
+ toMerge = append(toMerge, v.Policy)
+ }
+ if len(toMerge) == 0 {
+ return policy.Policy{}, errNoSuchPolicy
+ }
+ return policy.MergePolicies(toMerge...), nil
+}
+
+// GetPolicyDoc - gets the policy doc which has the policy and some metadata.
+// Exactly one policy must be specified here.
+func (store *IAMStoreSys) GetPolicyDoc(name string) (r PolicyDoc, err error) {
+ name = strings.TrimSpace(name)
+ if name == "" {
+ return r, errInvalidArgument
+ }
+
+ cache := store.rlock()
+ defer store.runlock()
+
+ v, ok := cache.iamPolicyDocsMap[name]
+ if !ok {
+ return r, errNoSuchPolicy
+ }
+ return v, nil
+}
+
+// SetPolicy - creates a policy with name.
+func (store *IAMStoreSys) SetPolicy(ctx context.Context, name string, policy policy.Policy) (time.Time, error) {
+ if policy.IsEmpty() || name == "" {
+ return time.Time{}, errInvalidArgument
+ }
+
+ cache := store.lock()
+ defer store.unlock()
+
+ var (
+ d PolicyDoc
+ ok bool
+ )
+ if d, ok = cache.iamPolicyDocsMap[name]; ok {
+ d.update(policy)
+ } else {
+ d = newPolicyDoc(policy)
+ }
+
+ if err := store.savePolicyDoc(ctx, name, d); err != nil {
+ return d.UpdateDate, err
+ }
+
+ cache.iamPolicyDocsMap[name] = d
+ cache.updatedAt = time.Now()
+
+ return d.UpdateDate, nil
+}
+
+// ListPolicies - fetches all policies from storage and updates cache as well.
+// If bucketName is non-empty, returns policies matching the bucket.
+func (store *IAMStoreSys) ListPolicies(ctx context.Context, bucketName string) (map[string]policy.Policy, error) {
+ cache := store.lock()
+ defer store.unlock()
+
+ m := map[string]PolicyDoc{}
+ err := store.loadPolicyDocs(ctx, m)
+ if err != nil {
+ return nil, err
+ }
+
+ // Sets default canned policies
+ setDefaultCannedPolicies(m)
+
+ cache.iamPolicyDocsMap = m
+ cache.updatedAt = time.Now()
+
+ ret := map[string]policy.Policy{}
+ for k, v := range m {
+ if bucketName == "" || v.Policy.MatchResource(bucketName) {
+ ret[k] = v.Policy
+ }
+ }
+
+ return ret, nil
+}
+
+// ListPolicyDocs - fetches all policy docs from storage and updates cache as well.
+// If bucketName is non-empty, returns policy docs matching the bucket.
+func (store *IAMStoreSys) ListPolicyDocs(ctx context.Context, bucketName string) (map[string]PolicyDoc, error) {
+ cache := store.lock()
+ defer store.unlock()
+
+ m := map[string]PolicyDoc{}
+ err := store.loadPolicyDocs(ctx, m)
+ if err != nil {
+ return nil, err
+ }
+
+ // Sets default canned policies
+ setDefaultCannedPolicies(m)
+
+ cache.iamPolicyDocsMap = m
+ cache.updatedAt = time.Now()
+
+ ret := map[string]PolicyDoc{}
+ for k, v := range m {
+ if bucketName == "" || v.Policy.MatchResource(bucketName) {
+ ret[k] = v
+ }
+ }
+
+ return ret, nil
+}
+
+// fetches all policy docs from cache.
+// If bucketName is non-empty, returns policy docs matching the bucket.
+func (store *IAMStoreSys) listPolicyDocs(ctx context.Context, bucketName string) (map[string]PolicyDoc, error) {
+ cache := store.rlock()
+ defer store.runlock()
+ ret := map[string]PolicyDoc{}
+ for k, v := range cache.iamPolicyDocsMap {
+ if bucketName == "" || v.Policy.MatchResource(bucketName) {
+ ret[k] = v
+ }
+ }
+ return ret, nil
+}
+
+// helper function - does not take locks.
+func filterPolicies(cache *iamCache, policyName string, bucketName string) (string, policy.Policy) {
+ var policies []string
+ mp := newMappedPolicy(policyName)
+ var toMerge []policy.Policy
+ for _, policy := range mp.toSlice() {
+ if policy == "" {
+ continue
+ }
+ p, found := cache.iamPolicyDocsMap[policy]
+ if !found {
+ continue
+ }
+ if bucketName == "" || p.Policy.MatchResource(bucketName) {
+ policies = append(policies, policy)
+ toMerge = append(toMerge, p.Policy)
+ }
+ }
+ return strings.Join(policies, ","), policy.MergePolicies(toMerge...)
+}
+
+// MergePolicies - accepts a comma separated list of policy names as a string
+// and returns only policies that currently exist in MinIO. It includes hot loading
+// of policies if not in the memory
+func (store *IAMStoreSys) MergePolicies(policyName string) (string, policy.Policy) {
+ var policies []string
+ var missingPolicies []string
+ var toMerge []policy.Policy
+
+ cache := store.rlock()
+ for _, policy := range newMappedPolicy(policyName).toSlice() {
+ if policy == "" {
+ continue
+ }
+ p, found := cache.iamPolicyDocsMap[policy]
+ if !found {
+ missingPolicies = append(missingPolicies, policy)
+ continue
+ }
+ policies = append(policies, policy)
+ toMerge = append(toMerge, p.Policy)
+ }
+ store.runlock()
+
+ if len(missingPolicies) > 0 {
+ m := make(map[string]PolicyDoc)
+ for _, policy := range missingPolicies {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ _ = store.loadPolicyDoc(ctx, policy, m)
+ cancel()
+ }
+
+ cache := store.lock()
+ maps.Copy(cache.iamPolicyDocsMap, m)
+ store.unlock()
+
+ for policy, p := range m {
+ policies = append(policies, policy)
+ toMerge = append(toMerge, p.Policy)
+ }
+ }
+
+ return strings.Join(policies, ","), policy.MergePolicies(toMerge...)
+}
+
+// GetBucketUsers - returns users (not STS or service accounts) that have access
+// to the bucket. User is included even if a group policy that grants access to
+// the bucket is disabled.
+func (store *IAMStoreSys) GetBucketUsers(bucket string) (map[string]madmin.UserInfo, error) {
+ if bucket == "" {
+ return nil, errInvalidArgument
+ }
+
+ cache := store.rlock()
+ defer store.runlock()
+
+ result := map[string]madmin.UserInfo{}
+ for k, v := range cache.iamUsersMap {
+ c := v.Credentials
+ if c.IsTemp() || c.IsServiceAccount() {
+ continue
+ }
+ var policies []string
+ mp, ok := cache.iamUserPolicyMap.Load(k)
+ if ok {
+ policies = append(policies, mp.Policies)
+ for _, group := range cache.iamUserGroupMemberships[k].ToSlice() {
+ if nmp, ok := cache.iamGroupPolicyMap.Load(group); ok {
+ policies = append(policies, nmp.Policies)
+ }
+ }
+ }
+ matchedPolicies, _ := filterPolicies(cache, strings.Join(policies, ","), bucket)
+ if len(matchedPolicies) > 0 {
+ result[k] = madmin.UserInfo{
+ PolicyName: matchedPolicies,
+ Status: func() madmin.AccountStatus {
+ if c.IsValid() {
+ return madmin.AccountEnabled
+ }
+ return madmin.AccountDisabled
+ }(),
+ MemberOf: cache.iamUserGroupMemberships[k].ToSlice(),
+ }
+ }
+ }
+
+ return result, nil
+}
+
+// GetUsers - returns all users (not STS or service accounts).
+func (store *IAMStoreSys) GetUsers() map[string]madmin.UserInfo {
+ cache := store.rlock()
+ defer store.runlock()
+
+ result := map[string]madmin.UserInfo{}
+ for k, u := range cache.iamUsersMap {
+ v := u.Credentials
+
+ if v.IsTemp() || v.IsServiceAccount() {
+ continue
+ }
+ pl, _ := cache.iamUserPolicyMap.Load(k)
+ result[k] = madmin.UserInfo{
+ PolicyName: pl.Policies,
+ Status: func() madmin.AccountStatus {
+ if v.IsValid() {
+ return madmin.AccountEnabled
+ }
+ return madmin.AccountDisabled
+ }(),
+ MemberOf: cache.iamUserGroupMemberships[k].ToSlice(),
+ UpdatedAt: pl.UpdatedAt,
+ }
+ }
+
+ return result
+}
+
+// GetUsersWithMappedPolicies - safely returns the name of access keys with associated policies
+func (store *IAMStoreSys) GetUsersWithMappedPolicies() map[string]string {
+ cache := store.rlock()
+ defer store.runlock()
+
+ result := make(map[string]string)
+ cache.iamUserPolicyMap.Range(func(k string, v MappedPolicy) bool {
+ result[k] = v.Policies
+ return true
+ })
+ cache.iamSTSPolicyMap.Range(func(k string, v MappedPolicy) bool {
+ result[k] = v.Policies
+ return true
+ })
+ return result
+}
+
+// GetUserInfo - get info on a user.
+func (store *IAMStoreSys) GetUserInfo(name string) (u madmin.UserInfo, err error) {
+ if name == "" {
+ return u, errInvalidArgument
+ }
+
+ cache := store.rlock()
+ defer store.runlock()
+
+ if store.getUsersSysType() != MinIOUsersSysType {
+ // If the user has a mapped policy or is a member of a group, we
+ // return that info. Otherwise we return error.
+ var groups []string
+ for _, v := range cache.iamUsersMap {
+ if v.Credentials.ParentUser == name {
+ groups = v.Credentials.Groups
+ break
+ }
+ }
+ for _, v := range cache.iamSTSAccountsMap {
+ if v.Credentials.ParentUser == name {
+ groups = v.Credentials.Groups
+ break
+ }
+ }
+ mappedPolicy, ok := cache.iamUserPolicyMap.Load(name)
+ if !ok {
+ mappedPolicy, ok = cache.iamSTSPolicyMap.Load(name)
+ }
+ if !ok {
+ // Attempt to load parent user mapping for STS accounts
+ store.loadMappedPolicy(context.TODO(), name, stsUser, false, cache.iamSTSPolicyMap)
+ mappedPolicy, ok = cache.iamSTSPolicyMap.Load(name)
+ if !ok {
+ return u, errNoSuchUser
+ }
+ }
+
+ return madmin.UserInfo{
+ PolicyName: mappedPolicy.Policies,
+ MemberOf: groups,
+ UpdatedAt: mappedPolicy.UpdatedAt,
+ }, nil
+ }
+
+ ui, found := cache.iamUsersMap[name]
+ if !found {
+ return u, errNoSuchUser
+ }
+ cred := ui.Credentials
+ if cred.IsTemp() || cred.IsServiceAccount() {
+ return u, errIAMActionNotAllowed
+ }
+ pl, _ := cache.iamUserPolicyMap.Load(name)
+ return madmin.UserInfo{
+ PolicyName: pl.Policies,
+ Status: func() madmin.AccountStatus {
+ if cred.IsValid() {
+ return madmin.AccountEnabled
+ }
+ return madmin.AccountDisabled
+ }(),
+ MemberOf: cache.iamUserGroupMemberships[name].ToSlice(),
+ UpdatedAt: pl.UpdatedAt,
+ }, nil
+}
+
+// PolicyMappingNotificationHandler - handles updating a policy mapping from storage.
+func (store *IAMStoreSys) PolicyMappingNotificationHandler(ctx context.Context, userOrGroup string, isGroup bool, userType IAMUserType) error {
+ if userOrGroup == "" {
+ return errInvalidArgument
+ }
+
+ cache := store.lock()
+ defer store.unlock()
+
+ var m *xsync.MapOf[string, MappedPolicy]
+ switch {
+ case isGroup:
+ m = cache.iamGroupPolicyMap
+ case userType == stsUser:
+ m = cache.iamSTSPolicyMap
+ default:
+ m = cache.iamUserPolicyMap
+ }
+ err := store.loadMappedPolicy(ctx, userOrGroup, userType, isGroup, m)
+ if errors.Is(err, errNoSuchPolicy) {
+ // This means that the policy mapping was deleted, so we update
+ // the cache.
+ m.Delete(userOrGroup)
+ cache.updatedAt = time.Now()
+
+ err = nil
+ }
+ return err
+}
+
+// UserNotificationHandler - handles updating a user/STS account/service account
+// from storage.
+func (store *IAMStoreSys) UserNotificationHandler(ctx context.Context, accessKey string, userType IAMUserType) error {
+ if accessKey == "" {
+ return errInvalidArgument
+ }
+
+ cache := store.lock()
+ defer store.unlock()
+
+ var m map[string]UserIdentity
+ switch userType {
+ case stsUser:
+ m = cache.iamSTSAccountsMap
+ default:
+ m = cache.iamUsersMap
+ }
+ err := store.loadUser(ctx, accessKey, userType, m)
+
+ if err == errNoSuchUser {
+ // User was deleted - we update the cache.
+ delete(m, accessKey)
+
+ // Since cache was updated, we update the timestamp.
+ defer func() {
+ cache.updatedAt = time.Now()
+ }()
+
+ // 1. Start with updating user-group memberships
+ if store.getUsersSysType() == MinIOUsersSysType {
+ memberOf := cache.iamUserGroupMemberships[accessKey].ToSlice()
+ for _, group := range memberOf {
+ _, removeErr := removeMembersFromGroup(ctx, store, cache, group, []string{accessKey}, true)
+ if removeErr == errNoSuchGroup {
+ removeErr = nil
+ }
+ if removeErr != nil {
+ return removeErr
+ }
+ }
+ }
+
+ // 2. Remove any derived credentials from memory
+ if userType == regUser {
+ for k, u := range cache.iamUsersMap {
+ if u.Credentials.IsServiceAccount() && u.Credentials.ParentUser == accessKey {
+ delete(cache.iamUsersMap, k)
+ }
+ }
+ for k, u := range cache.iamSTSAccountsMap {
+ if u.Credentials.ParentUser == accessKey {
+ delete(cache.iamSTSAccountsMap, k)
+ }
+ }
+ }
+
+ // 3. Delete any mapped policy
+ cache.iamUserPolicyMap.Delete(accessKey)
+
+ return nil
+ }
+
+ if err != nil {
+ return err
+ }
+
+ // Since cache was updated, we update the timestamp.
+ defer func() {
+ cache.updatedAt = time.Now()
+ }()
+
+ cred := m[accessKey].Credentials
+ switch userType {
+ case stsUser:
+ // For STS accounts a policy is mapped to the parent user (if a mapping exists).
+ err = store.loadMappedPolicy(ctx, cred.ParentUser, userType, false, cache.iamSTSPolicyMap)
+ case svcUser:
+ // For service accounts, the parent may be a regular (internal) IDP
+ // user or a "virtual" user (parent of an STS account).
+ //
+ // If parent is a regular user => policy mapping is done on that parent itself.
+ //
+ // If parent is "virtual" => policy mapping is done on the virtual
+ // parent and that virtual parent is an stsUser.
+ //
+ // To load the appropriate mapping, we check the parent user type.
+ _, parentIsRegularUser := cache.iamUsersMap[cred.ParentUser]
+ if parentIsRegularUser {
+ err = store.loadMappedPolicy(ctx, cred.ParentUser, regUser, false, cache.iamUserPolicyMap)
+ } else {
+ err = store.loadMappedPolicy(ctx, cred.ParentUser, stsUser, false, cache.iamSTSPolicyMap)
+ }
+ case regUser:
+ // For regular users, we load the mapped policy.
+ err = store.loadMappedPolicy(ctx, accessKey, userType, false, cache.iamUserPolicyMap)
+ default:
+ // This is just to ensure that we have covered all cases for new
+ // code in future.
+ panic("unknown user type")
+ }
+ // Ignore policy not mapped error
+ if err != nil && !errors.Is(err, errNoSuchPolicy) {
+ return err
+ }
+
+ return nil
+}
+
+// DeleteUser - deletes a user from storage and cache. This only used with
+// long-term users and service accounts, not STS.
+func (store *IAMStoreSys) DeleteUser(ctx context.Context, accessKey string, userType IAMUserType) error {
+ if accessKey == "" {
+ return errInvalidArgument
+ }
+
+ cache := store.lock()
+ defer store.unlock()
+
+ // first we remove the user from their groups.
+ if store.getUsersSysType() == MinIOUsersSysType && userType == regUser {
+ memberOf := cache.iamUserGroupMemberships[accessKey].ToSlice()
+ for _, group := range memberOf {
+ _, removeErr := removeMembersFromGroup(ctx, store, cache, group, []string{accessKey}, false)
+ if removeErr != nil {
+ return removeErr
+ }
+ }
+ }
+
+ // Now we can remove the user from memory and IAM store
+
+ // Delete any STS and service account derived from this credential
+ // first.
+ if userType == regUser {
+ for _, ui := range cache.iamUsersMap {
+ u := ui.Credentials
+ if u.ParentUser == accessKey {
+ switch {
+ case u.IsServiceAccount():
+ _ = store.deleteUserIdentity(ctx, u.AccessKey, svcUser)
+ delete(cache.iamUsersMap, u.AccessKey)
+ case u.IsTemp():
+ _ = store.deleteUserIdentity(ctx, u.AccessKey, stsUser)
+ delete(cache.iamSTSAccountsMap, u.AccessKey)
+ delete(cache.iamUsersMap, u.AccessKey)
+ }
+ if store.group != nil {
+ store.group.Forget(u.AccessKey)
+ }
+ }
+ }
+ }
+
+ // It is ok to ignore deletion error on the mapped policy
+ store.deleteMappedPolicy(ctx, accessKey, userType, false)
+ cache.iamUserPolicyMap.Delete(accessKey)
+
+ err := store.deleteUserIdentity(ctx, accessKey, userType)
+ if err == errNoSuchUser {
+ // ignore if user is already deleted.
+ err = nil
+ }
+ if userType == stsUser {
+ delete(cache.iamSTSAccountsMap, accessKey)
+ }
+ delete(cache.iamUsersMap, accessKey)
+ if store.group != nil {
+ store.group.Forget(accessKey)
+ }
+
+ cache.updatedAt = time.Now()
+
+ return err
+}
+
+// SetTempUser - saves temporary (STS) credential to storage and cache. If a
+// policy name is given, it is associated with the parent user specified in the
+// credential.
+func (store *IAMStoreSys) SetTempUser(ctx context.Context, accessKey string, cred auth.Credentials, policyName string) (time.Time, error) {
+ if accessKey == "" || !cred.IsTemp() || cred.IsExpired() || cred.ParentUser == "" {
+ return time.Time{}, errInvalidArgument
+ }
+
+ ttl := int64(cred.Expiration.Sub(UTCNow()).Seconds())
+
+ cache := store.lock()
+ defer store.unlock()
+
+ if policyName != "" {
+ mp := newMappedPolicy(policyName)
+ _, combinedPolicyStmt := filterPolicies(cache, mp.Policies, "")
+
+ if combinedPolicyStmt.IsEmpty() {
+ return time.Time{}, fmt.Errorf("specified policy %s, not found %w", policyName, errNoSuchPolicy)
+ }
+
+ err := store.saveMappedPolicy(ctx, cred.ParentUser, stsUser, false, mp, options{ttl: ttl})
+ if err != nil {
+ return time.Time{}, err
+ }
+
+ cache.iamSTSPolicyMap.Store(cred.ParentUser, mp)
+ }
+
+ u := newUserIdentity(cred)
+ err := store.saveUserIdentity(ctx, accessKey, stsUser, u, options{ttl: ttl})
+ if err != nil {
+ return time.Time{}, err
+ }
+
+ cache.iamSTSAccountsMap[accessKey] = u
+ cache.updatedAt = time.Now()
+
+ return u.UpdatedAt, nil
+}
+
+// RevokeTokens - revokes all temporary credentials, or those with matching type,
+// associated with the parent user.
+func (store *IAMStoreSys) RevokeTokens(ctx context.Context, parentUser string, tokenRevokeType string) error {
+ if parentUser == "" {
+ return errInvalidArgument
+ }
+
+ cache := store.lock()
+ defer store.unlock()
+
+ secret, err := getTokenSigningKey()
+ if err != nil {
+ return err
+ }
+
+ var revoked bool
+ for _, ui := range cache.iamSTSAccountsMap {
+ if ui.Credentials.ParentUser != parentUser {
+ continue
+ }
+ if tokenRevokeType != "" {
+ claims, err := getClaimsFromTokenWithSecret(ui.Credentials.SessionToken, secret)
+ if err != nil {
+ continue // skip if token is invalid
+ }
+ // skip if token type is given and does not match
+ if v, _ := claims.Lookup(tokenRevokeTypeClaim); v != tokenRevokeType {
+ continue
+ }
+ }
+ if err := store.deleteUserIdentity(ctx, ui.Credentials.AccessKey, stsUser); err != nil {
+ return err
+ }
+ delete(cache.iamSTSAccountsMap, ui.Credentials.AccessKey)
+ revoked = true
+ }
+
+ if revoked {
+ cache.updatedAt = time.Now()
+ }
+
+ return nil
+}
+
+// DeleteUsers - given a set of users or access keys, deletes them along with
+// any derived credentials (STS or service accounts) and any associated policy
+// mappings.
+func (store *IAMStoreSys) DeleteUsers(ctx context.Context, users []string) error {
+ cache := store.lock()
+ defer store.unlock()
+
+ var deleted bool
+ usersToDelete := set.CreateStringSet(users...)
+ for user, ui := range cache.iamUsersMap {
+ userType := regUser
+ cred := ui.Credentials
+
+ if cred.IsServiceAccount() {
+ userType = svcUser
+ } else if cred.IsTemp() {
+ userType = stsUser
+ }
+
+ if usersToDelete.Contains(user) || usersToDelete.Contains(cred.ParentUser) {
+ // Delete this user account and its policy mapping
+ store.deleteMappedPolicy(ctx, user, userType, false)
+ cache.iamUserPolicyMap.Delete(user)
+
+ // we are only logging errors, not handling them.
+ err := store.deleteUserIdentity(ctx, user, userType)
+ iamLogIf(GlobalContext, err)
+ if userType == stsUser {
+ delete(cache.iamSTSAccountsMap, user)
+ }
+ delete(cache.iamUsersMap, user)
+ if store.group != nil {
+ store.group.Forget(user)
+ }
+
+ deleted = true
+ }
+ }
+
+ if deleted {
+ cache.updatedAt = time.Now()
+ }
+
+ return nil
+}
+
+// ParentUserInfo contains extra info about a the parent user.
+type ParentUserInfo struct {
+ subClaimValue string
+ roleArns set.StringSet
+}
+
+// GetAllParentUsers - returns all distinct "parent-users" associated with STS
+// or service credentials, mapped to all distinct roleARNs associated with the
+// parent user. The dummy role ARN is associated with parent users from
+// policy-claim based OpenID providers. The root credential as a parent
+// user is not included in the result.
+func (store *IAMStoreSys) GetAllParentUsers() map[string]ParentUserInfo {
+ cache := store.rlock()
+ defer store.runlock()
+
+ return store.getParentUsers(cache)
+}
+
+// assumes store is locked by caller.
+func (store *IAMStoreSys) getParentUsers(cache *iamCache) map[string]ParentUserInfo {
+ res := map[string]ParentUserInfo{}
+ for _, ui := range cache.iamUsersMap {
+ cred := ui.Credentials
+ // Only consider service account or STS credentials with
+ // non-empty session tokens.
+ if (!cred.IsServiceAccount() && !cred.IsTemp()) ||
+ cred.SessionToken == "" {
+ continue
+ }
+
+ var (
+ err error
+ claims *jwt.MapClaims
+ )
+
+ if cred.IsServiceAccount() {
+ claims, err = getClaimsFromTokenWithSecret(cred.SessionToken, cred.SecretKey)
+ } else if cred.IsTemp() {
+ var secretKey string
+ secretKey, err = getTokenSigningKey()
+ if err != nil {
+ continue
+ }
+ claims, err = getClaimsFromTokenWithSecret(cred.SessionToken, secretKey)
+ }
+
+ if err != nil {
+ continue
+ }
+ if cred.ParentUser == "" || cred.ParentUser == globalActiveCred.AccessKey {
+ continue
+ }
+
+ subClaimValue := cred.ParentUser
+ if v, ok := claims.Lookup(subClaim); ok {
+ subClaimValue = v
+ }
+ if v, ok := claims.Lookup(ldapActualUser); ok {
+ subClaimValue = v
+ }
+
+ roleArn := openid.DummyRoleARN.String()
+ s, ok := claims.Lookup(roleArnClaim)
+ if ok {
+ roleArn = s
+ }
+ v, ok := res[cred.ParentUser]
+ if ok {
+ res[cred.ParentUser] = ParentUserInfo{
+ subClaimValue: subClaimValue,
+ roleArns: v.roleArns.Union(set.CreateStringSet(roleArn)),
+ }
+ } else {
+ res[cred.ParentUser] = ParentUserInfo{
+ subClaimValue: subClaimValue,
+ roleArns: set.CreateStringSet(roleArn),
+ }
+ }
+ }
+
+ return res
+}
+
+// GetAllSTSUserMappings - Loads all STS user policy mappings from storage and
+// returns them. Also gets any STS users that do not have policy mappings but have
+// Service Accounts or STS keys (This is useful if the user is part of a group)
+func (store *IAMStoreSys) GetAllSTSUserMappings(userPredicate func(string) bool) (map[string]string, error) {
+ cache := store.rlock()
+ defer store.runlock()
+
+ stsMap := make(map[string]string)
+ m := xsync.NewMapOf[string, MappedPolicy]()
+ if err := store.loadMappedPolicies(context.Background(), stsUser, false, m); err != nil {
+ return nil, err
+ }
+
+ m.Range(func(user string, mappedPolicy MappedPolicy) bool {
+ if userPredicate != nil && !userPredicate(user) {
+ return true
+ }
+ stsMap[user] = mappedPolicy.Policies
+ return true
+ })
+
+ for user := range store.getParentUsers(cache) {
+ if _, ok := stsMap[user]; !ok {
+ if userPredicate != nil && !userPredicate(user) {
+ continue
+ }
+ stsMap[user] = ""
+ }
+ }
+ return stsMap, nil
+}
+
+// Assumes store is locked by caller. If userMap is empty, returns all user mappings.
+func (store *IAMStoreSys) listUserPolicyMappings(cache *iamCache, userMap map[string]set.StringSet,
+ userPredicate func(string) bool, decodeFunc func(string) string,
+) []madmin.UserPolicyEntities {
+ stsMap := xsync.NewMapOf[string, MappedPolicy]()
+ resMap := make(map[string]madmin.UserPolicyEntities, len(userMap))
+
+ for user, groupSet := range userMap {
+ // Attempt to load parent user mapping for STS accounts
+ store.loadMappedPolicy(context.TODO(), user, stsUser, false, stsMap)
+ decodeUser := user
+ if decodeFunc != nil {
+ decodeUser = decodeFunc(user)
+ }
+ blankEntities := madmin.UserPolicyEntities{User: decodeUser}
+ if !groupSet.IsEmpty() {
+ blankEntities.MemberOfMappings = store.listGroupPolicyMappings(cache, groupSet, nil, decodeFunc)
+ }
+ resMap[user] = blankEntities
+ }
+
+ var r []madmin.UserPolicyEntities
+ cache.iamUserPolicyMap.Range(func(user string, mappedPolicy MappedPolicy) bool {
+ if userPredicate != nil && !userPredicate(user) {
+ return true
+ }
+
+ entitiesWithMemberOf, ok := resMap[user]
+ if !ok {
+ if len(userMap) > 0 {
+ return true
+ }
+ decodeUser := user
+ if decodeFunc != nil {
+ decodeUser = decodeFunc(user)
+ }
+ entitiesWithMemberOf = madmin.UserPolicyEntities{User: decodeUser}
+ }
+
+ ps := mappedPolicy.toSlice()
+ sort.Strings(ps)
+ entitiesWithMemberOf.Policies = ps
+ resMap[user] = entitiesWithMemberOf
+ return true
+ })
+
+ stsMap.Range(func(user string, mappedPolicy MappedPolicy) bool {
+ if userPredicate != nil && !userPredicate(user) {
+ return true
+ }
+
+ entitiesWithMemberOf := resMap[user]
+
+ ps := mappedPolicy.toSlice()
+ sort.Strings(ps)
+ entitiesWithMemberOf.Policies = ps
+ resMap[user] = entitiesWithMemberOf
+ return true
+ })
+
+ for _, v := range resMap {
+ if v.Policies != nil || v.MemberOfMappings != nil {
+ r = append(r, v)
+ }
+ }
+
+ sort.Slice(r, func(i, j int) bool {
+ return r[i].User < r[j].User
+ })
+
+ return r
+}
+
+// Assumes store is locked by caller. If groups is empty, returns all group mappings.
+func (store *IAMStoreSys) listGroupPolicyMappings(cache *iamCache, groupsSet set.StringSet,
+ groupPredicate func(string) bool, decodeFunc func(string) string,
+) []madmin.GroupPolicyEntities {
+ var r []madmin.GroupPolicyEntities
+
+ cache.iamGroupPolicyMap.Range(func(group string, mappedPolicy MappedPolicy) bool {
+ if groupPredicate != nil && !groupPredicate(group) {
+ return true
+ }
+
+ if !groupsSet.IsEmpty() && !groupsSet.Contains(group) {
+ return true
+ }
+
+ decodeGroup := group
+ if decodeFunc != nil {
+ decodeGroup = decodeFunc(group)
+ }
+
+ ps := mappedPolicy.toSlice()
+ sort.Strings(ps)
+ r = append(r, madmin.GroupPolicyEntities{
+ Group: decodeGroup,
+ Policies: ps,
+ })
+ return true
+ })
+
+ sort.Slice(r, func(i, j int) bool {
+ return r[i].Group < r[j].Group
+ })
+
+ return r
+}
+
+// Assumes store is locked by caller. If policies is empty, returns all policy mappings.
+func (store *IAMStoreSys) listPolicyMappings(cache *iamCache, queryPolSet set.StringSet,
+ userPredicate, groupPredicate func(string) bool, decodeFunc func(string) string,
+) []madmin.PolicyEntities {
+ policyToUsersMap := make(map[string]set.StringSet)
+ cache.iamUserPolicyMap.Range(func(user string, mappedPolicy MappedPolicy) bool {
+ if userPredicate != nil && !userPredicate(user) {
+ return true
+ }
+
+ decodeUser := user
+ if decodeFunc != nil {
+ decodeUser = decodeFunc(user)
+ }
+
+ commonPolicySet := mappedPolicy.policySet()
+ if !queryPolSet.IsEmpty() {
+ commonPolicySet = commonPolicySet.Intersection(queryPolSet)
+ }
+ for _, policy := range commonPolicySet.ToSlice() {
+ s, ok := policyToUsersMap[policy]
+ if !ok {
+ policyToUsersMap[policy] = set.CreateStringSet(decodeUser)
+ } else {
+ s.Add(decodeUser)
+ policyToUsersMap[policy] = s
+ }
+ }
+ return true
+ })
+
+ if iamOS, ok := store.IAMStorageAPI.(*IAMObjectStore); ok {
+ for item := range listIAMConfigItems(context.Background(), iamOS.objAPI, iamConfigPrefix+SlashSeparator+policyDBSTSUsersListKey) {
+ user := strings.TrimSuffix(item.Item, ".json")
+ if userPredicate != nil && !userPredicate(user) {
+ continue
+ }
+
+ decodeUser := user
+ if decodeFunc != nil {
+ decodeUser = decodeFunc(user)
+ }
+
+ var mappedPolicy MappedPolicy
+ store.loadIAMConfig(context.Background(), &mappedPolicy, getMappedPolicyPath(user, stsUser, false))
+
+ commonPolicySet := mappedPolicy.policySet()
+ if !queryPolSet.IsEmpty() {
+ commonPolicySet = commonPolicySet.Intersection(queryPolSet)
+ }
+ for _, policy := range commonPolicySet.ToSlice() {
+ s, ok := policyToUsersMap[policy]
+ if !ok {
+ policyToUsersMap[policy] = set.CreateStringSet(decodeUser)
+ } else {
+ s.Add(decodeUser)
+ policyToUsersMap[policy] = s
+ }
+ }
+ }
+ }
+ if iamOS, ok := store.IAMStorageAPI.(*IAMEtcdStore); ok {
+ m := xsync.NewMapOf[string, MappedPolicy]()
+ err := iamOS.loadMappedPolicies(context.Background(), stsUser, false, m)
+ if err == nil {
+ m.Range(func(user string, mappedPolicy MappedPolicy) bool {
+ if userPredicate != nil && !userPredicate(user) {
+ return true
+ }
+
+ decodeUser := user
+ if decodeFunc != nil {
+ decodeUser = decodeFunc(user)
+ }
+
+ commonPolicySet := mappedPolicy.policySet()
+ if !queryPolSet.IsEmpty() {
+ commonPolicySet = commonPolicySet.Intersection(queryPolSet)
+ }
+ for _, policy := range commonPolicySet.ToSlice() {
+ s, ok := policyToUsersMap[policy]
+ if !ok {
+ policyToUsersMap[policy] = set.CreateStringSet(decodeUser)
+ } else {
+ s.Add(decodeUser)
+ policyToUsersMap[policy] = s
+ }
+ }
+ return true
+ })
+ }
+ }
+
+ policyToGroupsMap := make(map[string]set.StringSet)
+ cache.iamGroupPolicyMap.Range(func(group string, mappedPolicy MappedPolicy) bool {
+ if groupPredicate != nil && !groupPredicate(group) {
+ return true
+ }
+
+ decodeGroup := group
+ if decodeFunc != nil {
+ decodeGroup = decodeFunc(group)
+ }
+
+ commonPolicySet := mappedPolicy.policySet()
+ if !queryPolSet.IsEmpty() {
+ commonPolicySet = commonPolicySet.Intersection(queryPolSet)
+ }
+ for _, policy := range commonPolicySet.ToSlice() {
+ s, ok := policyToGroupsMap[policy]
+ if !ok {
+ policyToGroupsMap[policy] = set.CreateStringSet(decodeGroup)
+ } else {
+ s.Add(decodeGroup)
+ policyToGroupsMap[policy] = s
+ }
+ }
+ return true
+ })
+
+ m := make(map[string]madmin.PolicyEntities, len(policyToGroupsMap))
+ for policy, groups := range policyToGroupsMap {
+ s := groups.ToSlice()
+ sort.Strings(s)
+ m[policy] = madmin.PolicyEntities{
+ Policy: policy,
+ Groups: s,
+ }
+ }
+ for policy, users := range policyToUsersMap {
+ s := users.ToSlice()
+ sort.Strings(s)
+
+ // Update existing value in map
+ pe := m[policy]
+ pe.Policy = policy
+ pe.Users = s
+ m[policy] = pe
+ }
+
+ policyEntities := make([]madmin.PolicyEntities, 0, len(m))
+ for _, v := range m {
+ policyEntities = append(policyEntities, v)
+ }
+
+ sort.Slice(policyEntities, func(i, j int) bool {
+ return policyEntities[i].Policy < policyEntities[j].Policy
+ })
+
+ return policyEntities
+}
+
+// ListPolicyMappings - return users/groups mapped to policies.
+func (store *IAMStoreSys) ListPolicyMappings(q cleanEntitiesQuery,
+ userPredicate, groupPredicate func(string) bool, decodeFunc func(string) string,
+) madmin.PolicyEntitiesResult {
+ cache := store.rlock()
+ defer store.runlock()
+
+ var result madmin.PolicyEntitiesResult
+
+ isAllPoliciesQuery := len(q.Users) == 0 && len(q.Groups) == 0 && len(q.Policies) == 0
+
+ if len(q.Users) > 0 {
+ result.UserMappings = store.listUserPolicyMappings(cache, q.Users, userPredicate, decodeFunc)
+ }
+ if len(q.Groups) > 0 {
+ result.GroupMappings = store.listGroupPolicyMappings(cache, q.Groups, groupPredicate, decodeFunc)
+ }
+ if len(q.Policies) > 0 || isAllPoliciesQuery {
+ result.PolicyMappings = store.listPolicyMappings(cache, q.Policies, userPredicate, groupPredicate, decodeFunc)
+ }
+ return result
+}
+
+// SetUserStatus - sets current user status.
+func (store *IAMStoreSys) SetUserStatus(ctx context.Context, accessKey string, status madmin.AccountStatus) (updatedAt time.Time, err error) {
+ if accessKey != "" && status != madmin.AccountEnabled && status != madmin.AccountDisabled {
+ return updatedAt, errInvalidArgument
+ }
+
+ cache := store.lock()
+ defer store.unlock()
+
+ ui, ok := cache.iamUsersMap[accessKey]
+ if !ok {
+ return updatedAt, errNoSuchUser
+ }
+ cred := ui.Credentials
+
+ if cred.IsTemp() || cred.IsServiceAccount() {
+ return updatedAt, errIAMActionNotAllowed
+ }
+
+ uinfo := newUserIdentity(auth.Credentials{
+ AccessKey: accessKey,
+ SecretKey: cred.SecretKey,
+ Status: func() string {
+ switch string(status) {
+ case string(madmin.AccountEnabled), string(auth.AccountOn):
+ return auth.AccountOn
+ }
+ return auth.AccountOff
+ }(),
+ })
+
+ if err := store.saveUserIdentity(ctx, accessKey, regUser, uinfo); err != nil {
+ return updatedAt, err
+ }
+
+ if err := cache.updateUserWithClaims(accessKey, uinfo); err != nil {
+ return updatedAt, err
+ }
+
+ return uinfo.UpdatedAt, nil
+}
+
+// AddServiceAccount - add a new service account
+func (store *IAMStoreSys) AddServiceAccount(ctx context.Context, cred auth.Credentials) (updatedAt time.Time, err error) {
+ cache := store.lock()
+ defer store.unlock()
+
+ accessKey := cred.AccessKey
+ parentUser := cred.ParentUser
+
+ // Found newly requested service account, to be an existing account -
+ // reject such operation (updates to the service account are handled in
+ // a different API).
+ if su, found := cache.iamUsersMap[accessKey]; found {
+ scred := su.Credentials
+ if scred.ParentUser != parentUser {
+ return updatedAt, fmt.Errorf("%w: the service account access key is taken by another user", errIAMServiceAccountNotAllowed)
+ }
+ return updatedAt, fmt.Errorf("%w: the service account access key already taken", errIAMServiceAccountNotAllowed)
+ }
+
+ // Parent user must not be a service account.
+ if u, found := cache.iamUsersMap[parentUser]; found && u.Credentials.IsServiceAccount() {
+ return updatedAt, fmt.Errorf("%w: unable to create a service account for another service account", errIAMServiceAccountNotAllowed)
+ }
+
+ u := newUserIdentity(cred)
+ err = store.saveUserIdentity(ctx, u.Credentials.AccessKey, svcUser, u)
+ if err != nil {
+ return updatedAt, err
+ }
+
+ cache.updateUserWithClaims(u.Credentials.AccessKey, u)
+
+ return u.UpdatedAt, nil
+}
+
+// UpdateServiceAccount - updates a service account on storage.
+func (store *IAMStoreSys) UpdateServiceAccount(ctx context.Context, accessKey string, opts updateServiceAccountOpts) (updatedAt time.Time, err error) {
+ cache := store.lock()
+ defer store.unlock()
+
+ ui, ok := cache.iamUsersMap[accessKey]
+ if !ok || !ui.Credentials.IsServiceAccount() {
+ return updatedAt, errNoSuchServiceAccount
+ }
+ cr := ui.Credentials
+ currentSecretKey := cr.SecretKey
+ if opts.secretKey != "" {
+ if !auth.IsSecretKeyValid(opts.secretKey) {
+ return updatedAt, auth.ErrInvalidSecretKeyLength
+ }
+ cr.SecretKey = opts.secretKey
+ }
+
+ if opts.name != "" {
+ cr.Name = opts.name
+ }
+
+ if opts.description != "" {
+ cr.Description = opts.description
+ }
+
+ if opts.expiration != nil {
+ expirationInUTC := opts.expiration.UTC()
+ if err := validateSvcExpirationInUTC(expirationInUTC); err != nil {
+ return updatedAt, err
+ }
+ cr.Expiration = expirationInUTC
+ }
+
+ switch opts.status {
+ // The caller did not ask to update status account, do nothing
+ case "":
+ case string(madmin.AccountEnabled):
+ cr.Status = auth.AccountOn
+ case string(madmin.AccountDisabled):
+ cr.Status = auth.AccountOff
+ // Update account status
+ case auth.AccountOn, auth.AccountOff:
+ cr.Status = opts.status
+ default:
+ return updatedAt, errors.New("unknown account status value")
+ }
+
+ m, err := getClaimsFromTokenWithSecret(cr.SessionToken, currentSecretKey)
+ if err != nil {
+ return updatedAt, fmt.Errorf("unable to get svc acc claims: %v", err)
+ }
+
+ // Extracted session policy name string can be removed as its not useful
+ // at this point.
+ m.Delete(sessionPolicyNameExtracted)
+
+ nosp := opts.sessionPolicy == nil || opts.sessionPolicy.Version == "" && len(opts.sessionPolicy.Statements) == 0
+
+ // sessionPolicy is nil and there is embedded policy attached we remove
+ // embedded policy at that point.
+ if _, ok := m.Lookup(policy.SessionPolicyName); ok && nosp {
+ m.Delete(policy.SessionPolicyName)
+ m.Set(iamPolicyClaimNameSA(), inheritedPolicyType)
+ }
+
+ if opts.sessionPolicy != nil { // session policies is being updated
+ if err := opts.sessionPolicy.Validate(); err != nil {
+ return updatedAt, err
+ }
+
+ if opts.sessionPolicy.Version != "" && len(opts.sessionPolicy.Statements) > 0 {
+ policyBuf, err := json.Marshal(opts.sessionPolicy)
+ if err != nil {
+ return updatedAt, err
+ }
+
+ if len(policyBuf) > maxSVCSessionPolicySize {
+ return updatedAt, errSessionPolicyTooLarge
+ }
+
+ // Overwrite session policy claims.
+ m.Set(policy.SessionPolicyName, base64.StdEncoding.EncodeToString(policyBuf))
+ m.Set(iamPolicyClaimNameSA(), embeddedPolicyType)
+ }
+ }
+
+ cr.SessionToken, err = auth.JWTSignWithAccessKey(accessKey, m.Map(), cr.SecretKey)
+ if err != nil {
+ return updatedAt, err
+ }
+
+ u := newUserIdentity(cr)
+ if err := store.saveUserIdentity(ctx, u.Credentials.AccessKey, svcUser, u); err != nil {
+ return updatedAt, err
+ }
+
+ if err := cache.updateUserWithClaims(u.Credentials.AccessKey, u); err != nil {
+ return updatedAt, err
+ }
+
+ return u.UpdatedAt, nil
+}
+
+// ListTempAccounts - lists only temporary accounts from the cache.
+func (store *IAMStoreSys) ListTempAccounts(ctx context.Context, accessKey string) ([]UserIdentity, error) {
+ cache := store.rlock()
+ defer store.runlock()
+
+ userExists := false
+ var tempAccounts []UserIdentity
+ for _, v := range cache.iamUsersMap {
+ isDerived := false
+ if v.Credentials.IsServiceAccount() || v.Credentials.IsTemp() {
+ isDerived = true
+ }
+
+ if !isDerived && v.Credentials.AccessKey == accessKey {
+ userExists = true
+ } else if isDerived && v.Credentials.ParentUser == accessKey {
+ userExists = true
+ if v.Credentials.IsTemp() {
+ // Hide secret key & session key here
+ v.Credentials.SecretKey = ""
+ v.Credentials.SessionToken = ""
+ tempAccounts = append(tempAccounts, v)
+ }
+ }
+ }
+
+ if !userExists {
+ return nil, errNoSuchUser
+ }
+
+ return tempAccounts, nil
+}
+
+// ListServiceAccounts - lists only service accounts from the cache.
+func (store *IAMStoreSys) ListServiceAccounts(ctx context.Context, accessKey string) ([]auth.Credentials, error) {
+ cache := store.rlock()
+ defer store.runlock()
+
+ var serviceAccounts []auth.Credentials
+ for _, u := range cache.iamUsersMap {
+ v := u.Credentials
+ if accessKey != "" && v.ParentUser == accessKey {
+ if v.IsServiceAccount() {
+ // Hide secret key & session key here
+ v.SecretKey = ""
+ v.SessionToken = ""
+ serviceAccounts = append(serviceAccounts, v)
+ }
+ }
+ }
+
+ return serviceAccounts, nil
+}
+
+// ListSTSAccounts - lists only STS accounts from the cache.
+func (store *IAMStoreSys) ListSTSAccounts(ctx context.Context, accessKey string) ([]auth.Credentials, error) {
+ cache := store.rlock()
+ defer store.runlock()
+
+ var stsAccounts []auth.Credentials
+ for _, u := range cache.iamSTSAccountsMap {
+ v := u.Credentials
+ if accessKey != "" && v.ParentUser == accessKey {
+ if v.IsTemp() {
+ // Hide secret key & session key here
+ v.SecretKey = ""
+ v.SessionToken = ""
+ stsAccounts = append(stsAccounts, v)
+ }
+ }
+ }
+
+ return stsAccounts, nil
+}
+
+// ListAccessKeys - lists all access keys (sts/service accounts)
+func (store *IAMStoreSys) ListAccessKeys(ctx context.Context) ([]auth.Credentials, error) {
+ cache := store.rlock()
+ defer store.runlock()
+
+ accessKeys := store.getSTSAndServiceAccounts(cache)
+ for i, accessKey := range accessKeys {
+ accessKeys[i].SecretKey = ""
+ if accessKey.IsTemp() {
+ secret, err := getTokenSigningKey()
+ if err != nil {
+ return nil, err
+ }
+ claims, err := getClaimsFromTokenWithSecret(accessKey.SessionToken, secret)
+ if err != nil {
+ continue // ignore invalid session tokens
+ }
+ accessKeys[i].Claims = claims.MapClaims
+ }
+ accessKeys[i].SessionToken = ""
+ }
+
+ return accessKeys, nil
+}
+
+// AddUser - adds/updates long term user account to storage.
+func (store *IAMStoreSys) AddUser(ctx context.Context, accessKey string, ureq madmin.AddOrUpdateUserReq) (updatedAt time.Time, err error) {
+ cache := store.lock()
+ defer store.unlock()
+
+ cache.updatedAt = time.Now()
+
+ ui, ok := cache.iamUsersMap[accessKey]
+
+ // It is not possible to update an STS account.
+ if ok && ui.Credentials.IsTemp() {
+ return updatedAt, errIAMActionNotAllowed
+ }
+
+ u := newUserIdentity(auth.Credentials{
+ AccessKey: accessKey,
+ SecretKey: ureq.SecretKey,
+ Status: func() string {
+ switch string(ureq.Status) {
+ case string(madmin.AccountEnabled), string(auth.AccountOn):
+ return auth.AccountOn
+ }
+ return auth.AccountOff
+ }(),
+ })
+
+ if err := store.saveUserIdentity(ctx, accessKey, regUser, u); err != nil {
+ return updatedAt, err
+ }
+ if err := cache.updateUserWithClaims(accessKey, u); err != nil {
+ return updatedAt, err
+ }
+
+ return u.UpdatedAt, nil
+}
+
+// UpdateUserSecretKey - sets user secret key to storage.
+func (store *IAMStoreSys) UpdateUserSecretKey(ctx context.Context, accessKey, secretKey string) error {
+ cache := store.lock()
+ defer store.unlock()
+
+ cache.updatedAt = time.Now()
+
+ ui, ok := cache.iamUsersMap[accessKey]
+ if !ok {
+ return errNoSuchUser
+ }
+ cred := ui.Credentials
+ cred.SecretKey = secretKey
+ u := newUserIdentity(cred)
+ if err := store.saveUserIdentity(ctx, accessKey, regUser, u); err != nil {
+ return err
+ }
+
+ return cache.updateUserWithClaims(accessKey, u)
+}
+
+// GetSTSAndServiceAccounts - returns all STS and Service account credentials.
+func (store *IAMStoreSys) GetSTSAndServiceAccounts() []auth.Credentials {
+ cache := store.rlock()
+ defer store.runlock()
+
+ return store.getSTSAndServiceAccounts(cache)
+}
+
+func (store *IAMStoreSys) getSTSAndServiceAccounts(cache *iamCache) []auth.Credentials {
+ var res []auth.Credentials
+ for _, u := range cache.iamUsersMap {
+ cred := u.Credentials
+ if cred.IsServiceAccount() {
+ res = append(res, cred)
+ }
+ }
+ for _, u := range cache.iamSTSAccountsMap {
+ res = append(res, u.Credentials)
+ }
+
+ return res
+}
+
+// UpdateUserIdentity - updates a user credential.
+func (store *IAMStoreSys) UpdateUserIdentity(ctx context.Context, cred auth.Credentials) error {
+ cache := store.lock()
+ defer store.unlock()
+
+ cache.updatedAt = time.Now()
+
+ userType := regUser
+ if cred.IsServiceAccount() {
+ userType = svcUser
+ } else if cred.IsTemp() {
+ userType = stsUser
+ }
+ ui := newUserIdentity(cred)
+ // Overwrite the user identity here. As store should be
+ // atomic, it shouldn't cause any corruption.
+ if err := store.saveUserIdentity(ctx, cred.AccessKey, userType, ui); err != nil {
+ return err
+ }
+
+ return cache.updateUserWithClaims(cred.AccessKey, ui)
+}
+
+// LoadUser - attempts to load user info from storage and updates cache.
+func (store *IAMStoreSys) LoadUser(ctx context.Context, accessKey string) error {
+ groupLoad := env.Get("_MINIO_IAM_GROUP_REFRESH", config.EnableOff) == config.EnableOn
+
+ newCachePopulate := func() (val any, err error) {
+ newCache := newIamCache()
+
+ // Check for service account first
+ store.loadUser(ctx, accessKey, svcUser, newCache.iamUsersMap)
+
+ svc, found := newCache.iamUsersMap[accessKey]
+ if found {
+ // Load parent user and mapped policies.
+ if store.getUsersSysType() == MinIOUsersSysType {
+ err = store.loadUser(ctx, svc.Credentials.ParentUser, regUser, newCache.iamUsersMap)
+ // NOTE: we are not worried about loading errors from policies.
+ store.loadMappedPolicyWithRetry(ctx, svc.Credentials.ParentUser, regUser, false, newCache.iamUserPolicyMap, 3)
+ } else {
+ // In case of LDAP the parent user's policy mapping needs to be loaded into sts map
+ // NOTE: we are not worried about loading errors from policies.
+ store.loadMappedPolicyWithRetry(ctx, svc.Credentials.ParentUser, stsUser, false, newCache.iamSTSPolicyMap, 3)
+ }
+ }
+
+ if !found {
+ err = store.loadUser(ctx, accessKey, regUser, newCache.iamUsersMap)
+ if _, found = newCache.iamUsersMap[accessKey]; found {
+ // NOTE: we are not worried about loading errors from policies.
+ store.loadMappedPolicyWithRetry(ctx, accessKey, regUser, false, newCache.iamUserPolicyMap, 3)
+ }
+ }
+
+ // Check for STS account
+ var stsUserCred UserIdentity
+ if !found {
+ err = store.loadUser(ctx, accessKey, stsUser, newCache.iamSTSAccountsMap)
+ if stsUserCred, found = newCache.iamSTSAccountsMap[accessKey]; found {
+ // Load mapped policy
+ // NOTE: we are not worried about loading errors from policies.
+ store.loadMappedPolicyWithRetry(ctx, stsUserCred.Credentials.ParentUser, stsUser, false, newCache.iamSTSPolicyMap, 3)
+ }
+ }
+
+ // Load any associated policy definitions
+ pols, _ := newCache.iamUserPolicyMap.Load(accessKey)
+ for _, policy := range pols.toSlice() {
+ if _, found = newCache.iamPolicyDocsMap[policy]; !found {
+ // NOTE: we are not worried about loading errors from policies.
+ store.loadPolicyDocWithRetry(ctx, policy, newCache.iamPolicyDocsMap, 3)
+ }
+ }
+
+ pols, _ = newCache.iamSTSPolicyMap.Load(stsUserCred.Credentials.AccessKey)
+ for _, policy := range pols.toSlice() {
+ if _, found = newCache.iamPolicyDocsMap[policy]; !found {
+ // NOTE: we are not worried about loading errors from policies.
+ store.loadPolicyDocWithRetry(ctx, policy, newCache.iamPolicyDocsMap, 3)
+ }
+ }
+
+ if groupLoad {
+ // NOTE: we are not worried about loading errors from groups.
+ store.updateGroups(ctx, newCache)
+ newCache.buildUserGroupMemberships()
+ }
+
+ return newCache, err
+ }
+
+ var (
+ val any
+ err error
+ )
+ if store.group != nil {
+ val, err, _ = store.group.Do(accessKey, newCachePopulate)
+ } else {
+ val, err = newCachePopulate()
+ }
+
+ // Return error right away if any.
+ if err != nil {
+ if errors.Is(err, errNoSuchUser) || errors.Is(err, errConfigNotFound) {
+ return nil
+ }
+ return err
+ }
+
+ newCache, ok := val.(*iamCache)
+ if !ok {
+ return nil
+ }
+
+ cache := store.lock()
+ defer store.unlock()
+
+ // We need to merge the new cache with the existing cache because the
+ // periodic IAM reload is partial. The periodic load here is to account.
+ newCache.iamGroupPolicyMap.Range(func(k string, v MappedPolicy) bool {
+ cache.iamGroupPolicyMap.Store(k, v)
+ return true
+ })
+
+ maps.Copy(cache.iamGroupsMap, newCache.iamGroupsMap)
+
+ maps.Copy(cache.iamPolicyDocsMap, newCache.iamPolicyDocsMap)
+
+ maps.Copy(cache.iamUserGroupMemberships, newCache.iamUserGroupMemberships)
+
+ newCache.iamUserPolicyMap.Range(func(k string, v MappedPolicy) bool {
+ cache.iamUserPolicyMap.Store(k, v)
+ return true
+ })
+
+ maps.Copy(cache.iamUsersMap, newCache.iamUsersMap)
+
+ maps.Copy(cache.iamSTSAccountsMap, newCache.iamSTSAccountsMap)
+
+ newCache.iamSTSPolicyMap.Range(func(k string, v MappedPolicy) bool {
+ cache.iamSTSPolicyMap.Store(k, v)
+ return true
+ })
+
+ cache.updatedAt = time.Now()
+
+ return nil
+}
+
+func extractJWTClaims(u UserIdentity) (jwtClaims *jwt.MapClaims, err error) {
+ keys := make([]string, 0, 3)
+
+ // Append credentials secret key itself
+ keys = append(keys, u.Credentials.SecretKey)
+
+ // Use site-replication credentials if found
+ if globalSiteReplicationSys.isEnabled() {
+ secretKey, err := getTokenSigningKey()
+ if err != nil {
+ return nil, err
+ }
+ keys = append(keys, secretKey)
+ }
+
+ // Iterate over all keys and return with the first successful claim extraction
+ for _, key := range keys {
+ jwtClaims, err = getClaimsFromTokenWithSecret(u.Credentials.SessionToken, key)
+ if err == nil {
+ break
+ }
+ }
+ return jwtClaims, err
+}
+
+func validateSvcExpirationInUTC(expirationInUTC time.Time) error {
+ if expirationInUTC.IsZero() || expirationInUTC.Equal(timeSentinel) {
+ // Service accounts might not have expiration in older releases.
+ return nil
+ }
+
+ currentTime := time.Now().UTC()
+ minExpiration := currentTime.Add(minServiceAccountExpiry)
+ maxExpiration := currentTime.Add(maxServiceAccountExpiry)
+ if expirationInUTC.Before(minExpiration) || expirationInUTC.After(maxExpiration) {
+ return errInvalidSvcAcctExpiration
+ }
+
+ return nil
+}
diff --git a/cmd/iam.go b/cmd/iam.go
new file mode 100644
index 00000000..18cdc148
--- /dev/null
+++ b/cmd/iam.go
@@ -0,0 +1,2556 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "maps"
+ "math/rand"
+ "path"
+ "sort"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio-go/v7/pkg/set"
+ "github.com/minio/minio/internal/arn"
+ "github.com/minio/minio/internal/auth"
+ "github.com/minio/minio/internal/color"
+ "github.com/minio/minio/internal/config"
+ xldap "github.com/minio/minio/internal/config/identity/ldap"
+ "github.com/minio/minio/internal/config/identity/openid"
+ idplugin "github.com/minio/minio/internal/config/identity/plugin"
+ xtls "github.com/minio/minio/internal/config/identity/tls"
+ "github.com/minio/minio/internal/config/policy/opa"
+ polplugin "github.com/minio/minio/internal/config/policy/plugin"
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/minio/internal/jwt"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/env"
+ "github.com/minio/pkg/v3/ldap"
+ "github.com/minio/pkg/v3/policy"
+ etcd "go.etcd.io/etcd/client/v3"
+ "golang.org/x/sync/singleflight"
+)
+
+// UsersSysType - defines the type of users and groups system that is
+// active on the server.
+type UsersSysType string
+
+// Types of users configured in the server.
+const (
+ // This mode uses the internal users system in MinIO.
+ MinIOUsersSysType UsersSysType = "MinIOUsersSys"
+
+ // This mode uses users and groups from a configured LDAP
+ // server.
+ LDAPUsersSysType UsersSysType = "LDAPUsersSys"
+)
+
+const (
+ statusEnabled = "enabled"
+ statusDisabled = "disabled"
+)
+
+const (
+ embeddedPolicyType = "embedded-policy"
+ inheritedPolicyType = "inherited-policy"
+)
+
+const (
+ maxSVCSessionPolicySize = 4096
+)
+
+// IAMSys - config system.
+type IAMSys struct {
+ // Need to keep them here to keep alignment - ref: https://golang.org/pkg/sync/atomic/#pkg-note-BUG
+ // metrics
+ LastRefreshTimeUnixNano uint64
+ LastRefreshDurationMilliseconds uint64
+ TotalRefreshSuccesses uint64
+ TotalRefreshFailures uint64
+
+ sync.Mutex
+
+ iamRefreshInterval time.Duration
+
+ LDAPConfig xldap.Config // only valid if usersSysType is LDAPUsers
+ OpenIDConfig openid.Config // only valid if OpenID is configured
+ STSTLSConfig xtls.Config // only valid if STS TLS is configured
+
+ usersSysType UsersSysType
+
+ rolesMap map[arn.ARN]string
+
+ // Persistence layer for IAM subsystem
+ store *IAMStoreSys
+
+ // configLoaded will be closed and remain so after first load.
+ configLoaded chan struct{}
+}
+
+// IAMUserType represents a user type inside MinIO server
+type IAMUserType int
+
+const (
+ unknownIAMUserType IAMUserType = iota - 1
+ regUser
+ stsUser
+ svcUser
+)
+
+// LoadGroup - loads a specific group from storage, and updates the
+// memberships cache. If the specified group does not exist in
+// storage, it is removed from in-memory maps as well - this
+// simplifies the implementation for group removal. This is called
+// only via IAM notifications.
+func (sys *IAMSys) LoadGroup(ctx context.Context, objAPI ObjectLayer, group string) error {
+ if !sys.Initialized() {
+ return errServerNotInitialized
+ }
+
+ return sys.store.GroupNotificationHandler(ctx, group)
+}
+
+// LoadPolicy - reloads a specific canned policy from backend disks or etcd.
+func (sys *IAMSys) LoadPolicy(ctx context.Context, objAPI ObjectLayer, policyName string) error {
+ if !sys.Initialized() {
+ return errServerNotInitialized
+ }
+
+ return sys.store.PolicyNotificationHandler(ctx, policyName)
+}
+
+// LoadPolicyMapping - loads the mapped policy for a user or group
+// from storage into server memory.
+func (sys *IAMSys) LoadPolicyMapping(ctx context.Context, objAPI ObjectLayer, userOrGroup string, userType IAMUserType, isGroup bool) error {
+ if !sys.Initialized() {
+ return errServerNotInitialized
+ }
+
+ return sys.store.PolicyMappingNotificationHandler(ctx, userOrGroup, isGroup, userType)
+}
+
+// LoadUser - reloads a specific user from backend disks or etcd.
+func (sys *IAMSys) LoadUser(ctx context.Context, objAPI ObjectLayer, accessKey string, userType IAMUserType) error {
+ if !sys.Initialized() {
+ return errServerNotInitialized
+ }
+
+ return sys.store.UserNotificationHandler(ctx, accessKey, userType)
+}
+
+// LoadServiceAccount - reloads a specific service account from backend disks or etcd.
+func (sys *IAMSys) LoadServiceAccount(ctx context.Context, accessKey string) error {
+ if !sys.Initialized() {
+ return errServerNotInitialized
+ }
+
+ return sys.store.UserNotificationHandler(ctx, accessKey, svcUser)
+}
+
+// initStore initializes IAM stores
+func (sys *IAMSys) initStore(objAPI ObjectLayer, etcdClient *etcd.Client) {
+ if sys.LDAPConfig.Enabled() {
+ sys.SetUsersSysType(LDAPUsersSysType)
+ }
+
+ if etcdClient == nil {
+ var (
+ group *singleflight.Group
+ policy *singleflight.Group
+ )
+ if env.Get("_MINIO_IAM_SINGLE_FLIGHT", config.EnableOn) == config.EnableOn {
+ group = &singleflight.Group{}
+ policy = &singleflight.Group{}
+ }
+ sys.store = &IAMStoreSys{
+ IAMStorageAPI: newIAMObjectStore(objAPI, sys.usersSysType),
+ group: group,
+ policy: policy,
+ }
+ } else {
+ sys.store = &IAMStoreSys{IAMStorageAPI: newIAMEtcdStore(etcdClient, sys.usersSysType)}
+ }
+}
+
+// Initialized checks if IAM is initialized
+func (sys *IAMSys) Initialized() bool {
+ if sys == nil {
+ return false
+ }
+ sys.Lock()
+ defer sys.Unlock()
+ return sys.store != nil
+}
+
+// Load - loads all credentials, policies and policy mappings.
+func (sys *IAMSys) Load(ctx context.Context, firstTime bool) error {
+ loadStartTime := time.Now()
+ err := sys.store.LoadIAMCache(ctx, firstTime)
+ if err != nil {
+ atomic.AddUint64(&sys.TotalRefreshFailures, 1)
+ return err
+ }
+ loadDuration := time.Since(loadStartTime)
+
+ atomic.StoreUint64(&sys.LastRefreshDurationMilliseconds, uint64(loadDuration.Milliseconds()))
+ atomic.StoreUint64(&sys.LastRefreshTimeUnixNano, uint64(loadStartTime.Add(loadDuration).UnixNano()))
+ atomic.AddUint64(&sys.TotalRefreshSuccesses, 1)
+
+ if !globalSiteReplicatorCred.IsValid() {
+ sa, _, err := sys.getServiceAccount(ctx, siteReplicatorSvcAcc)
+ if err == nil {
+ globalSiteReplicatorCred.Set(sa.Credentials.SecretKey)
+ }
+ }
+
+ if firstTime {
+ bootstrapTraceMsg(fmt.Sprintf("globalIAMSys.Load(): (duration: %s)", loadDuration))
+ if globalIsDistErasure {
+ logger.Info("IAM load(startup) finished. (duration: %s)", loadDuration)
+ }
+ }
+
+ select {
+ case <-sys.configLoaded:
+ default:
+ close(sys.configLoaded)
+ }
+ return nil
+}
+
+// Init - initializes config system by reading entries from config/iam
+func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etcd.Client, iamRefreshInterval time.Duration) {
+ bootstrapTraceMsg("IAM initialization started")
+ globalServerConfigMu.RLock()
+ s := globalServerConfig
+ globalServerConfigMu.RUnlock()
+
+ sys.Lock()
+ sys.iamRefreshInterval = iamRefreshInterval
+ sys.Unlock()
+
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+
+ var (
+ openidInit bool
+ ldapInit bool
+ authNInit bool
+ authZInit bool
+ )
+
+ stsTLSConfig, err := xtls.Lookup(s[config.IdentityTLSSubSys][config.Default])
+ if err != nil {
+ iamLogIf(ctx, fmt.Errorf("Unable to initialize X.509/TLS STS API: %w", err), logger.WarningKind)
+ } else {
+ if stsTLSConfig.InsecureSkipVerify {
+ iamLogIf(ctx, fmt.Errorf("Enabling %s is not recommended in a production environment", xtls.EnvIdentityTLSSkipVerify), logger.WarningKind)
+ }
+ sys.Lock()
+ sys.STSTLSConfig = stsTLSConfig
+ sys.Unlock()
+ }
+
+ for {
+ if !openidInit {
+ openidConfig, err := openid.LookupConfig(s,
+ xhttp.WithUserAgent(NewHTTPTransport(), func() string {
+ return getUserAgent(getMinioMode())
+ }), xhttp.DrainBody, globalSite.Region())
+ if err != nil {
+ iamLogIf(ctx, fmt.Errorf("Unable to initialize OpenID: %w", err), logger.WarningKind)
+ } else {
+ openidInit = true
+ sys.Lock()
+ sys.OpenIDConfig = openidConfig
+ sys.Unlock()
+ }
+ }
+
+ if !ldapInit {
+ // Initialize if LDAP is enabled
+ ldapConfig, err := xldap.Lookup(s, globalRootCAs)
+ if err != nil {
+ iamLogIf(ctx, fmt.Errorf("Unable to load LDAP configuration (LDAP configuration will be disabled!): %w", err), logger.WarningKind)
+ } else {
+ ldapInit = true
+ sys.Lock()
+ sys.LDAPConfig = ldapConfig
+ sys.Unlock()
+ }
+ }
+
+ if !authNInit {
+ authNPluginCfg, err := idplugin.LookupConfig(s[config.IdentityPluginSubSys][config.Default],
+ NewHTTPTransport(), xhttp.DrainBody, globalSite.Region())
+ if err != nil {
+ iamLogIf(ctx, fmt.Errorf("Unable to initialize AuthNPlugin: %w", err), logger.WarningKind)
+ } else {
+ authNInit = true
+ setGlobalAuthNPlugin(idplugin.New(GlobalContext, authNPluginCfg))
+ }
+ }
+
+ if !authZInit {
+ authZPluginCfg, err := polplugin.LookupConfig(s, GetDefaultConnSettings(), xhttp.DrainBody)
+ if err != nil {
+ iamLogIf(ctx, fmt.Errorf("Unable to initialize AuthZPlugin: %w", err), logger.WarningKind)
+ } else {
+ authZInit = true
+ }
+ if authZPluginCfg.URL == nil {
+ opaCfg, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default],
+ NewHTTPTransport(), xhttp.DrainBody)
+ if err != nil {
+ iamLogIf(ctx, fmt.Errorf("Unable to initialize AuthZPlugin from legacy OPA config: %w", err))
+ } else {
+ authZPluginCfg.URL = opaCfg.URL
+ authZPluginCfg.AuthToken = opaCfg.AuthToken
+ authZPluginCfg.Transport = opaCfg.Transport
+ authZPluginCfg.CloseRespFn = opaCfg.CloseRespFn
+ authZInit = true
+ }
+ }
+ if authZInit {
+ setGlobalAuthZPlugin(polplugin.New(authZPluginCfg))
+ }
+ }
+
+ if !openidInit || !ldapInit || !authNInit || !authZInit {
+ retryInterval := time.Duration(r.Float64() * float64(3*time.Second))
+ if !openidInit {
+ logger.Info("Waiting for OpenID to be initialized.. (retrying in %s)", retryInterval)
+ }
+ if !ldapInit {
+ logger.Info("Waiting for LDAP to be initialized.. (retrying in %s)", retryInterval)
+ }
+ if !authNInit {
+ logger.Info("Waiting for AuthN to be initialized.. (retrying in %s)", retryInterval)
+ }
+ if !authZInit {
+ logger.Info("Waiting for AuthZ to be initialized.. (retrying in %s)", retryInterval)
+ }
+ time.Sleep(retryInterval)
+ continue
+ }
+
+ break
+ }
+
+ // Initialize IAM store
+ sys.Lock()
+
+ sys.initStore(objAPI, etcdClient)
+
+ // Initialize RoleARNs
+ sys.rolesMap = make(map[arn.ARN]string)
+
+ // From OpenID
+ maps.Copy(sys.rolesMap, sys.OpenIDConfig.GetRoleInfo())
+
+ // From AuthN plugin if enabled.
+ if authn := newGlobalAuthNPluginFn(); authn != nil {
+ maps.Copy(sys.rolesMap, authn.GetRoleInfo())
+ }
+
+ sys.printIAMRoles()
+ sys.Unlock()
+
+ retryCtx, cancel := context.WithCancel(ctx)
+
+ // Indicate to our routine to exit cleanly upon return.
+ defer cancel()
+
+ // Migrate storage format if needed.
+ for {
+ // Migrate IAM configuration, if necessary.
+ if err := saveIAMFormat(retryCtx, sys.store); err != nil {
+ if configRetriableErrors(err) {
+ retryInterval := time.Duration(r.Float64() * float64(time.Second))
+ logger.Info("Waiting for all MinIO IAM sub-system to be initialized.. possible cause (%v) (retrying in %s)", err, retryInterval)
+ time.Sleep(retryInterval)
+ continue
+ }
+ iamLogIf(ctx, fmt.Errorf("IAM sub-system is partially initialized, unable to write the IAM format: %w", err), logger.WarningKind)
+ return
+ }
+
+ break
+ }
+
+ cache := sys.store.lock()
+ setDefaultCannedPolicies(cache.iamPolicyDocsMap)
+ sys.store.unlock()
+
+ // Load IAM data from storage.
+ for {
+ if err := sys.Load(retryCtx, true); err != nil {
+ if configRetriableErrors(err) {
+ retryInterval := time.Duration(r.Float64() * float64(time.Second))
+ logger.Info("Waiting for all MinIO IAM sub-system to be initialized.. possible cause (%v) (retrying in %s)", err, retryInterval)
+ time.Sleep(retryInterval)
+ continue
+ }
+ if err != nil {
+ iamLogIf(ctx, fmt.Errorf("Unable to initialize IAM sub-system, some users may not be available: %w", err), logger.WarningKind)
+ }
+ }
+ break
+ }
+
+ refreshInterval := sys.iamRefreshInterval
+ go sys.periodicRoutines(ctx, refreshInterval)
+
+ bootstrapTraceMsg("finishing IAM loading")
+}
+
+const maxDurationSecondsForLog = 5
+
+func (sys *IAMSys) periodicRoutines(ctx context.Context, baseInterval time.Duration) {
+ // Watch for IAM config changes for iamStorageWatcher.
+ watcher, isWatcher := sys.store.IAMStorageAPI.(iamStorageWatcher)
+ if isWatcher {
+ go func() {
+ ch := watcher.watch(ctx, iamConfigPrefix)
+ for event := range ch {
+ if err := sys.loadWatchedEvent(ctx, event); err != nil {
+ // we simply log errors
+ iamLogIf(ctx, fmt.Errorf("Failure in loading watch event: %v", err), logger.WarningKind)
+ }
+ }
+ }()
+ }
+
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+
+ // Calculate the waitInterval between periodic refreshes so that each server
+ // independently picks a (uniformly distributed) random time in an interval
+ // of size = baseInterval.
+ //
+ // For example:
+ //
+ // - if baseInterval=10s, then 5s <= waitInterval() < 15s
+ //
+ // - if baseInterval=10m, then 5m <= waitInterval() < 15m
+ waitInterval := func() time.Duration {
+ // Calculate a random value such that 0 <= value < baseInterval
+ randAmt := time.Duration(r.Float64() * float64(baseInterval))
+ return baseInterval/2 + randAmt
+ }
+
+ timer := time.NewTimer(waitInterval())
+ defer timer.Stop()
+
+ lastPurgeHour := -1
+ for {
+ select {
+ case <-timer.C:
+ // Load all IAM items (except STS creds) periodically.
+ refreshStart := time.Now()
+ if err := sys.Load(ctx, false); err != nil {
+ iamLogIf(ctx, fmt.Errorf("Failure in periodic refresh for IAM (duration: %s): %v", time.Since(refreshStart), err), logger.WarningKind)
+ } else {
+ took := time.Since(refreshStart).Seconds()
+ if took > maxDurationSecondsForLog {
+ // Log if we took a lot of time to load.
+ logger.Info("IAM refresh took (duration: %.2fs)", took)
+ }
+ }
+
+ // Run purge routines once in each hour.
+ if refreshStart.Hour() != lastPurgeHour {
+ lastPurgeHour = refreshStart.Hour()
+ // Poll and remove accounts for those users who were removed
+ // from LDAP/OpenID.
+ if sys.LDAPConfig.Enabled() {
+ sys.purgeExpiredCredentialsForLDAP(ctx)
+ sys.updateGroupMembershipsForLDAP(ctx)
+ }
+ if sys.OpenIDConfig.ProviderEnabled() {
+ sys.purgeExpiredCredentialsForExternalSSO(ctx)
+ }
+ }
+
+ timer.Reset(waitInterval())
+ case <-ctx.Done():
+ return
+ }
+ }
+}
+
+// Prints IAM role ARNs.
+func (sys *IAMSys) printIAMRoles() {
+ if len(sys.rolesMap) == 0 {
+ return
+ }
+ var arns []string
+ for arn := range sys.rolesMap {
+ arns = append(arns, arn.String())
+ }
+ sort.Strings(arns)
+ msgs := make([]string, 0, len(arns))
+ for _, arn := range arns {
+ msgs = append(msgs, color.Bold(arn))
+ }
+
+ logger.Info(fmt.Sprintf("%s %s", color.Blue("IAM Roles:"), strings.Join(msgs, " ")))
+}
+
+// HasWatcher - returns if the IAM system has a watcher to be notified of
+// changes.
+func (sys *IAMSys) HasWatcher() bool {
+ return sys.store.HasWatcher()
+}
+
+func (sys *IAMSys) loadWatchedEvent(ctx context.Context, event iamWatchEvent) (err error) {
+ usersPrefix := strings.HasPrefix(event.keyPath, iamConfigUsersPrefix)
+ groupsPrefix := strings.HasPrefix(event.keyPath, iamConfigGroupsPrefix)
+ stsPrefix := strings.HasPrefix(event.keyPath, iamConfigSTSPrefix)
+ svcPrefix := strings.HasPrefix(event.keyPath, iamConfigServiceAccountsPrefix)
+ policyPrefix := strings.HasPrefix(event.keyPath, iamConfigPoliciesPrefix)
+ policyDBUsersPrefix := strings.HasPrefix(event.keyPath, iamConfigPolicyDBUsersPrefix)
+ policyDBSTSUsersPrefix := strings.HasPrefix(event.keyPath, iamConfigPolicyDBSTSUsersPrefix)
+ policyDBGroupsPrefix := strings.HasPrefix(event.keyPath, iamConfigPolicyDBGroupsPrefix)
+
+ ctx, cancel := context.WithTimeout(ctx, defaultContextTimeout)
+ defer cancel()
+
+ switch {
+ case usersPrefix:
+ accessKey := path.Dir(strings.TrimPrefix(event.keyPath, iamConfigUsersPrefix))
+ err = sys.store.UserNotificationHandler(ctx, accessKey, regUser)
+ case stsPrefix:
+ accessKey := path.Dir(strings.TrimPrefix(event.keyPath, iamConfigSTSPrefix))
+ err = sys.store.UserNotificationHandler(ctx, accessKey, stsUser)
+ case svcPrefix:
+ accessKey := path.Dir(strings.TrimPrefix(event.keyPath, iamConfigServiceAccountsPrefix))
+ err = sys.store.UserNotificationHandler(ctx, accessKey, svcUser)
+ case groupsPrefix:
+ group := path.Dir(strings.TrimPrefix(event.keyPath, iamConfigGroupsPrefix))
+ err = sys.store.GroupNotificationHandler(ctx, group)
+ case policyPrefix:
+ policyName := path.Dir(strings.TrimPrefix(event.keyPath, iamConfigPoliciesPrefix))
+ err = sys.store.PolicyNotificationHandler(ctx, policyName)
+ case policyDBUsersPrefix:
+ policyMapFile := strings.TrimPrefix(event.keyPath, iamConfigPolicyDBUsersPrefix)
+ user := strings.TrimSuffix(policyMapFile, ".json")
+ err = sys.store.PolicyMappingNotificationHandler(ctx, user, false, regUser)
+ case policyDBSTSUsersPrefix:
+ policyMapFile := strings.TrimPrefix(event.keyPath, iamConfigPolicyDBSTSUsersPrefix)
+ user := strings.TrimSuffix(policyMapFile, ".json")
+ err = sys.store.PolicyMappingNotificationHandler(ctx, user, false, stsUser)
+ case policyDBGroupsPrefix:
+ policyMapFile := strings.TrimPrefix(event.keyPath, iamConfigPolicyDBGroupsPrefix)
+ user := strings.TrimSuffix(policyMapFile, ".json")
+ err = sys.store.PolicyMappingNotificationHandler(ctx, user, true, regUser)
+ }
+ return err
+}
+
+// HasRolePolicy - returns if a role policy is configured for IAM.
+func (sys *IAMSys) HasRolePolicy() bool {
+ return len(sys.rolesMap) > 0
+}
+
+// GetRolePolicy - returns policies associated with a role ARN.
+func (sys *IAMSys) GetRolePolicy(arnStr string) (arn.ARN, string, error) {
+ roleArn, err := arn.Parse(arnStr)
+ if err != nil {
+ return arn.ARN{}, "", fmt.Errorf("RoleARN parse err: %v", err)
+ }
+ rolePolicy, ok := sys.rolesMap[roleArn]
+ if !ok {
+ return arn.ARN{}, "", fmt.Errorf("RoleARN %s is not defined.", arnStr)
+ }
+ return roleArn, rolePolicy, nil
+}
+
+// DeletePolicy - deletes a canned policy from backend. `notifyPeers` is true
+// whenever this is called via the API. It is false when called via a
+// notification from another peer. This is to avoid infinite loops.
+func (sys *IAMSys) DeletePolicy(ctx context.Context, policyName string, notifyPeers bool) error {
+ if !sys.Initialized() {
+ return errServerNotInitialized
+ }
+
+ for _, v := range policy.DefaultPolicies {
+ if v.Name == policyName {
+ if err := checkConfig(ctx, globalObjectAPI, getPolicyDocPath(policyName)); err != nil && err == errConfigNotFound {
+ return fmt.Errorf("inbuilt policy `%s` not allowed to be deleted", policyName)
+ }
+ }
+ }
+
+ err := sys.store.DeletePolicy(ctx, policyName, !notifyPeers)
+ if err != nil {
+ return err
+ }
+
+ if !notifyPeers || sys.HasWatcher() {
+ return nil
+ }
+
+ // Notify all other MinIO peers to delete policy
+ for _, nerr := range globalNotificationSys.DeletePolicy(ctx, policyName) {
+ if nerr.Err != nil {
+ logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
+ iamLogIf(ctx, nerr.Err)
+ }
+ }
+
+ return nil
+}
+
+// InfoPolicy - returns the policy definition with some metadata.
+func (sys *IAMSys) InfoPolicy(policyName string) (*madmin.PolicyInfo, error) {
+ if !sys.Initialized() {
+ return nil, errServerNotInitialized
+ }
+
+ d, err := sys.store.GetPolicyDoc(policyName)
+ if err != nil {
+ return nil, err
+ }
+
+ pdata, err := json.Marshal(d.Policy)
+ if err != nil {
+ return nil, err
+ }
+
+ return &madmin.PolicyInfo{
+ PolicyName: policyName,
+ Policy: pdata,
+ CreateDate: d.CreateDate,
+ UpdateDate: d.UpdateDate,
+ }, nil
+}
+
+// ListPolicies - lists all canned policies.
+func (sys *IAMSys) ListPolicies(ctx context.Context, bucketName string) (map[string]policy.Policy, error) {
+ if !sys.Initialized() {
+ return nil, errServerNotInitialized
+ }
+
+ return sys.store.ListPolicies(ctx, bucketName)
+}
+
+// ListPolicyDocs - lists all canned policy docs.
+func (sys *IAMSys) ListPolicyDocs(ctx context.Context, bucketName string) (map[string]PolicyDoc, error) {
+ if !sys.Initialized() {
+ return nil, errServerNotInitialized
+ }
+
+ return sys.store.ListPolicyDocs(ctx, bucketName)
+}
+
+// SetPolicy - sets a new named policy.
+func (sys *IAMSys) SetPolicy(ctx context.Context, policyName string, p policy.Policy) (time.Time, error) {
+ if !sys.Initialized() {
+ return time.Time{}, errServerNotInitialized
+ }
+
+ updatedAt, err := sys.store.SetPolicy(ctx, policyName, p)
+ if err != nil {
+ return updatedAt, err
+ }
+
+ if !sys.HasWatcher() {
+ // Notify all other MinIO peers to reload policy
+ for _, nerr := range globalNotificationSys.LoadPolicy(ctx, policyName) {
+ if nerr.Err != nil {
+ logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
+ iamLogIf(ctx, nerr.Err)
+ }
+ }
+ }
+ return updatedAt, nil
+}
+
+// RevokeTokens - revokes all STS tokens, or those of specified type, for a user
+// If `tokenRevokeType` is empty, all tokens are revoked.
+func (sys *IAMSys) RevokeTokens(ctx context.Context, accessKey, tokenRevokeType string) error {
+ if !sys.Initialized() {
+ return errServerNotInitialized
+ }
+
+ return sys.store.RevokeTokens(ctx, accessKey, tokenRevokeType)
+}
+
+// DeleteUser - delete user (only for long-term users not STS users).
+func (sys *IAMSys) DeleteUser(ctx context.Context, accessKey string, notifyPeers bool) error {
+ if !sys.Initialized() {
+ return errServerNotInitialized
+ }
+
+ if err := sys.store.DeleteUser(ctx, accessKey, regUser); err != nil {
+ return err
+ }
+
+ // Notify all other MinIO peers to delete user.
+ if notifyPeers && !sys.HasWatcher() {
+ for _, nerr := range globalNotificationSys.DeleteUser(ctx, accessKey) {
+ if nerr.Err != nil {
+ logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
+ iamLogIf(ctx, nerr.Err)
+ }
+ }
+ }
+
+ return nil
+}
+
+// CurrentPolicies - returns comma separated policy string, from
+// an input policy after validating if there are any current
+// policies which exist on MinIO corresponding to the input.
+func (sys *IAMSys) CurrentPolicies(policyName string) string {
+ if !sys.Initialized() {
+ return ""
+ }
+
+ policies, _ := sys.store.MergePolicies(policyName)
+ return policies
+}
+
+func (sys *IAMSys) notifyForUser(ctx context.Context, accessKey string, isTemp bool) {
+ // Notify all other MinIO peers to reload user.
+ if !sys.HasWatcher() {
+ for _, nerr := range globalNotificationSys.LoadUser(ctx, accessKey, isTemp) {
+ if nerr.Err != nil {
+ logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
+ iamLogIf(ctx, nerr.Err)
+ }
+ }
+ }
+}
+
+// SetTempUser - set temporary user credentials, these credentials have an
+// expiry. The permissions for these STS credentials is determined in one of the
+// following ways:
+//
+// - RoleARN - if a role-arn is specified in the request, the STS credential's
+// policy is the role's policy.
+//
+// - inherited from parent - this is the case for AssumeRole API, where the
+// parent user is an actual real user with their own (permanent) credentials and
+// policy association.
+//
+// - inherited from "virtual" parent - this is the case for AssumeRoleWithLDAP
+// where the parent user is the DN of the actual LDAP user. The parent user
+// itself cannot login, but the policy associated with them determines the base
+// policy for the STS credential. The policy mapping can be updated by the
+// administrator.
+//
+// - from `Subject.CommonName` field from the STS request for
+// AssumeRoleWithCertificate. In this case, the policy for the STS credential
+// has the same name as the value of this field.
+//
+// - from special JWT claim from STS request for AssumeRoleWithOIDC API (when
+// not using RoleARN). The claim value can be a string or a list and refers to
+// the names of access policies.
+//
+// For all except the RoleARN case, the implementation is the same - the policy
+// for the STS credential is associated with a parent user. For the
+// AssumeRoleWithCertificate case, the "virtual" parent user is the value of the
+// `Subject.CommonName` field. For the OIDC (without RoleARN) case the "virtual"
+// parent is derived as a concatenation of the `sub` and `iss` fields. The
+// policies applicable to the STS credential are associated with this "virtual"
+// parent.
+//
+// When a policyName is given to this function, the policy association is
+// created and stored in the IAM store. Thus, it should NOT be given for the
+// role-arn case (because the role-to-policy mapping is separately stored
+// elsewhere), the AssumeRole case (because the parent user is real and their
+// policy is associated via policy-set API) and the AssumeRoleWithLDAP case
+// (because the policy association is made via policy-set API).
+func (sys *IAMSys) SetTempUser(ctx context.Context, accessKey string, cred auth.Credentials, policyName string) (time.Time, error) {
+ if !sys.Initialized() {
+ return time.Time{}, errServerNotInitialized
+ }
+
+ if newGlobalAuthZPluginFn() != nil {
+ // If OPA is set, we do not need to set a policy mapping.
+ policyName = ""
+ }
+
+ updatedAt, err := sys.store.SetTempUser(ctx, accessKey, cred, policyName)
+ if err != nil {
+ return time.Time{}, err
+ }
+
+ sys.notifyForUser(ctx, cred.AccessKey, true)
+
+ return updatedAt, nil
+}
+
+// ListBucketUsers - list all users who can access this 'bucket'
+func (sys *IAMSys) ListBucketUsers(ctx context.Context, bucket string) (map[string]madmin.UserInfo, error) {
+ if !sys.Initialized() {
+ return nil, errServerNotInitialized
+ }
+
+ select {
+ case <-sys.configLoaded:
+ return sys.store.GetBucketUsers(bucket)
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ }
+}
+
+// ListUsers - list all users.
+func (sys *IAMSys) ListUsers(ctx context.Context) (map[string]madmin.UserInfo, error) {
+ if !sys.Initialized() {
+ return nil, errServerNotInitialized
+ }
+ select {
+ case <-sys.configLoaded:
+ return sys.store.GetUsers(), nil
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ }
+}
+
+// ListLDAPUsers - list LDAP users which has
+func (sys *IAMSys) ListLDAPUsers(ctx context.Context) (map[string]madmin.UserInfo, error) {
+ if !sys.Initialized() {
+ return nil, errServerNotInitialized
+ }
+
+ if sys.usersSysType != LDAPUsersSysType {
+ return nil, errIAMActionNotAllowed
+ }
+
+ select {
+ case <-sys.configLoaded:
+ stsMap, err := sys.store.GetAllSTSUserMappings(sys.LDAPConfig.IsLDAPUserDN)
+ if err != nil {
+ return nil, err
+ }
+ ldapUsers := make(map[string]madmin.UserInfo, len(stsMap))
+ for user, policy := range stsMap {
+ ldapUsers[user] = madmin.UserInfo{
+ PolicyName: policy,
+ Status: statusEnabled,
+ }
+ }
+ return ldapUsers, nil
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ }
+}
+
+type cleanEntitiesQuery struct {
+ Users map[string]set.StringSet
+ Groups set.StringSet
+ Policies set.StringSet
+}
+
+// createCleanEntitiesQuery - maps users to their groups and normalizes user or group DNs if ldap.
+func (sys *IAMSys) createCleanEntitiesQuery(q madmin.PolicyEntitiesQuery, ldap bool) cleanEntitiesQuery {
+ cleanQ := cleanEntitiesQuery{
+ Users: make(map[string]set.StringSet),
+ Groups: set.CreateStringSet(q.Groups...),
+ Policies: set.CreateStringSet(q.Policy...),
+ }
+
+ if ldap {
+ // Validate and normalize users, then fetch and normalize their groups
+ // Also include unvalidated users for backward compatibility.
+ for _, user := range q.Users {
+ lookupRes, actualGroups, _ := sys.LDAPConfig.GetValidatedDNWithGroups(user)
+ if lookupRes != nil {
+ groupSet := set.CreateStringSet(actualGroups...)
+
+ // duplicates can be overwritten, fetched groups should be identical.
+ cleanQ.Users[lookupRes.NormDN] = groupSet
+ }
+ // Search for non-normalized DN as well for backward compatibility.
+ if _, ok := cleanQ.Users[user]; !ok {
+ cleanQ.Users[user] = nil
+ }
+ }
+
+ // Validate and normalize groups.
+ for _, group := range q.Groups {
+ lookupRes, underDN, _ := sys.LDAPConfig.GetValidatedGroupDN(nil, group)
+ if lookupRes != nil && underDN {
+ cleanQ.Groups.Add(lookupRes.NormDN)
+ }
+ }
+ } else {
+ for _, user := range q.Users {
+ info, err := sys.store.GetUserInfo(user)
+ var groupSet set.StringSet
+ if err == nil {
+ groupSet = set.CreateStringSet(info.MemberOf...)
+ }
+ cleanQ.Users[user] = groupSet
+ }
+ }
+ return cleanQ
+}
+
+// QueryLDAPPolicyEntities - queries policy associations for LDAP users/groups/policies.
+func (sys *IAMSys) QueryLDAPPolicyEntities(ctx context.Context, q madmin.PolicyEntitiesQuery) (*madmin.PolicyEntitiesResult, error) {
+ if !sys.Initialized() {
+ return nil, errServerNotInitialized
+ }
+
+ if !sys.LDAPConfig.Enabled() {
+ return nil, errIAMActionNotAllowed
+ }
+
+ select {
+ case <-sys.configLoaded:
+ cleanQuery := sys.createCleanEntitiesQuery(q, true)
+ pe := sys.store.ListPolicyMappings(cleanQuery, sys.LDAPConfig.IsLDAPUserDN, sys.LDAPConfig.IsLDAPGroupDN, sys.LDAPConfig.DecodeDN)
+ pe.Timestamp = UTCNow()
+ return &pe, nil
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ }
+}
+
+// IsTempUser - returns if given key is a temporary user and parent user.
+func (sys *IAMSys) IsTempUser(name string) (bool, string, error) {
+ if !sys.Initialized() {
+ return false, "", errServerNotInitialized
+ }
+
+ u, found := sys.store.GetUser(name)
+ if !found {
+ return false, "", errNoSuchUser
+ }
+ cred := u.Credentials
+ if cred.IsTemp() {
+ return true, cred.ParentUser, nil
+ }
+
+ return false, "", nil
+}
+
+// IsServiceAccount - returns if given key is a service account
+func (sys *IAMSys) IsServiceAccount(name string) (bool, string, error) {
+ if !sys.Initialized() {
+ return false, "", errServerNotInitialized
+ }
+
+ u, found := sys.store.GetUser(name)
+ if !found {
+ return false, "", errNoSuchUser
+ }
+ cred := u.Credentials
+ if cred.IsServiceAccount() {
+ return true, cred.ParentUser, nil
+ }
+
+ return false, "", nil
+}
+
+// GetUserInfo - get info on a user.
+func (sys *IAMSys) GetUserInfo(ctx context.Context, name string) (u madmin.UserInfo, err error) {
+ if !sys.Initialized() {
+ return u, errServerNotInitialized
+ }
+
+ loadUserCalled := false
+ select {
+ case <-sys.configLoaded:
+ default:
+ sys.store.LoadUser(ctx, name)
+ loadUserCalled = true
+ }
+
+ userInfo, err := sys.store.GetUserInfo(name)
+ if err == errNoSuchUser && !loadUserCalled {
+ sys.store.LoadUser(ctx, name)
+ userInfo, err = sys.store.GetUserInfo(name)
+ }
+ return userInfo, err
+}
+
+// QueryPolicyEntities - queries policy associations for builtin users/groups/policies.
+func (sys *IAMSys) QueryPolicyEntities(ctx context.Context, q madmin.PolicyEntitiesQuery) (*madmin.PolicyEntitiesResult, error) {
+ if !sys.Initialized() {
+ return nil, errServerNotInitialized
+ }
+
+ select {
+ case <-sys.configLoaded:
+ cleanQuery := sys.createCleanEntitiesQuery(q, false)
+ var userPredicate, groupPredicate func(string) bool
+ if sys.LDAPConfig.Enabled() {
+ userPredicate = func(s string) bool {
+ return !sys.LDAPConfig.IsLDAPUserDN(s)
+ }
+ groupPredicate = func(s string) bool {
+ return !sys.LDAPConfig.IsLDAPGroupDN(s)
+ }
+ }
+ pe := sys.store.ListPolicyMappings(cleanQuery, userPredicate, groupPredicate, nil)
+ pe.Timestamp = UTCNow()
+ return &pe, nil
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ }
+}
+
+// SetUserStatus - sets current user status, supports disabled or enabled.
+func (sys *IAMSys) SetUserStatus(ctx context.Context, accessKey string, status madmin.AccountStatus) (updatedAt time.Time, err error) {
+ if !sys.Initialized() {
+ return updatedAt, errServerNotInitialized
+ }
+
+ if sys.usersSysType != MinIOUsersSysType {
+ return updatedAt, errIAMActionNotAllowed
+ }
+
+ updatedAt, err = sys.store.SetUserStatus(ctx, accessKey, status)
+ if err != nil {
+ return updatedAt, err
+ }
+
+ sys.notifyForUser(ctx, accessKey, false)
+ return updatedAt, nil
+}
+
+func (sys *IAMSys) notifyForServiceAccount(ctx context.Context, accessKey string) {
+ // Notify all other Minio peers to reload the service account
+ if !sys.HasWatcher() {
+ for _, nerr := range globalNotificationSys.LoadServiceAccount(ctx, accessKey) {
+ if nerr.Err != nil {
+ logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
+ iamLogIf(ctx, nerr.Err)
+ }
+ }
+ }
+}
+
+type newServiceAccountOpts struct {
+ sessionPolicy *policy.Policy
+ accessKey string
+ secretKey string
+ name, description string
+ expiration *time.Time
+ allowSiteReplicatorAccount bool // allow creating internal service account for site-replication.
+
+ claims map[string]any
+}
+
+// NewServiceAccount - create a new service account
+func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, groups []string, opts newServiceAccountOpts) (auth.Credentials, time.Time, error) {
+ if !sys.Initialized() {
+ return auth.Credentials{}, time.Time{}, errServerNotInitialized
+ }
+
+ if parentUser == "" {
+ return auth.Credentials{}, time.Time{}, errInvalidArgument
+ }
+
+ if len(opts.accessKey) > 0 && len(opts.secretKey) == 0 {
+ return auth.Credentials{}, time.Time{}, auth.ErrNoSecretKeyWithAccessKey
+ }
+ if len(opts.secretKey) > 0 && len(opts.accessKey) == 0 {
+ return auth.Credentials{}, time.Time{}, auth.ErrNoAccessKeyWithSecretKey
+ }
+
+ var policyBuf []byte
+ if opts.sessionPolicy != nil {
+ err := opts.sessionPolicy.Validate()
+ if err != nil {
+ return auth.Credentials{}, time.Time{}, err
+ }
+ policyBuf, err = json.Marshal(opts.sessionPolicy)
+ if err != nil {
+ return auth.Credentials{}, time.Time{}, err
+ }
+ if len(policyBuf) > maxSVCSessionPolicySize {
+ return auth.Credentials{}, time.Time{}, errSessionPolicyTooLarge
+ }
+ }
+
+ // found newly requested service account, to be same as
+ // parentUser, reject such operations.
+ if parentUser == opts.accessKey {
+ return auth.Credentials{}, time.Time{}, errIAMActionNotAllowed
+ }
+ if siteReplicatorSvcAcc == opts.accessKey && !opts.allowSiteReplicatorAccount {
+ return auth.Credentials{}, time.Time{}, errIAMActionNotAllowed
+ }
+ m := make(map[string]any)
+ m[parentClaim] = parentUser
+
+ if len(policyBuf) > 0 {
+ m[policy.SessionPolicyName] = base64.StdEncoding.EncodeToString(policyBuf)
+ m[iamPolicyClaimNameSA()] = embeddedPolicyType
+ } else {
+ m[iamPolicyClaimNameSA()] = inheritedPolicyType
+ }
+
+ // Add all the necessary claims for the service account.
+ for k, v := range opts.claims {
+ _, ok := m[k]
+ if !ok {
+ m[k] = v
+ }
+ }
+
+ var accessKey, secretKey string
+ var err error
+ if len(opts.accessKey) > 0 || len(opts.secretKey) > 0 {
+ accessKey, secretKey = opts.accessKey, opts.secretKey
+ } else {
+ accessKey, secretKey, err = auth.GenerateCredentials()
+ if err != nil {
+ return auth.Credentials{}, time.Time{}, err
+ }
+ }
+ cred, err := auth.CreateNewCredentialsWithMetadata(accessKey, secretKey, m, secretKey)
+ if err != nil {
+ return auth.Credentials{}, time.Time{}, err
+ }
+ cred.ParentUser = parentUser
+ cred.Groups = groups
+ cred.Status = string(auth.AccountOn)
+ cred.Name = opts.name
+ cred.Description = opts.description
+
+ if opts.expiration != nil {
+ expirationInUTC := opts.expiration.UTC()
+ if err := validateSvcExpirationInUTC(expirationInUTC); err != nil {
+ return auth.Credentials{}, time.Time{}, err
+ }
+ cred.Expiration = expirationInUTC
+ }
+
+ updatedAt, err := sys.store.AddServiceAccount(ctx, cred)
+ if err != nil {
+ return auth.Credentials{}, time.Time{}, err
+ }
+
+ sys.notifyForServiceAccount(ctx, cred.AccessKey)
+ return cred, updatedAt, nil
+}
+
+type updateServiceAccountOpts struct {
+ sessionPolicy *policy.Policy
+ secretKey string
+ status string
+ name, description string
+ expiration *time.Time
+}
+
+// UpdateServiceAccount - edit a service account
+func (sys *IAMSys) UpdateServiceAccount(ctx context.Context, accessKey string, opts updateServiceAccountOpts) (updatedAt time.Time, err error) {
+ if !sys.Initialized() {
+ return updatedAt, errServerNotInitialized
+ }
+
+ updatedAt, err = sys.store.UpdateServiceAccount(ctx, accessKey, opts)
+ if err != nil {
+ return updatedAt, err
+ }
+
+ sys.notifyForServiceAccount(ctx, accessKey)
+ return updatedAt, nil
+}
+
+// ListServiceAccounts - lists all service accounts associated to a specific user
+func (sys *IAMSys) ListServiceAccounts(ctx context.Context, accessKey string) ([]auth.Credentials, error) {
+ if !sys.Initialized() {
+ return nil, errServerNotInitialized
+ }
+
+ select {
+ case <-sys.configLoaded:
+ return sys.store.ListServiceAccounts(ctx, accessKey)
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ }
+}
+
+// ListTempAccounts - lists all temporary service accounts associated to a specific user
+func (sys *IAMSys) ListTempAccounts(ctx context.Context, accessKey string) ([]UserIdentity, error) {
+ if !sys.Initialized() {
+ return nil, errServerNotInitialized
+ }
+
+ select {
+ case <-sys.configLoaded:
+ return sys.store.ListTempAccounts(ctx, accessKey)
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ }
+}
+
+// ListSTSAccounts - lists all STS accounts associated to a specific user
+func (sys *IAMSys) ListSTSAccounts(ctx context.Context, accessKey string) ([]auth.Credentials, error) {
+ if !sys.Initialized() {
+ return nil, errServerNotInitialized
+ }
+
+ select {
+ case <-sys.configLoaded:
+ return sys.store.ListSTSAccounts(ctx, accessKey)
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ }
+}
+
+// ListAllAccessKeys - lists all access keys (sts/service accounts)
+func (sys *IAMSys) ListAllAccessKeys(ctx context.Context) ([]auth.Credentials, error) {
+ if !sys.Initialized() {
+ return nil, errServerNotInitialized
+ }
+
+ select {
+ case <-sys.configLoaded:
+ return sys.store.ListAccessKeys(ctx)
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ }
+}
+
+// GetServiceAccount - wrapper method to get information about a service account
+func (sys *IAMSys) GetServiceAccount(ctx context.Context, accessKey string) (auth.Credentials, *policy.Policy, error) {
+ sa, embeddedPolicy, err := sys.getServiceAccount(ctx, accessKey)
+ if err != nil {
+ return auth.Credentials{}, nil, err
+ }
+ // Hide secret & session keys
+ sa.Credentials.SecretKey = ""
+ sa.Credentials.SessionToken = ""
+ return sa.Credentials, embeddedPolicy, nil
+}
+
+func (sys *IAMSys) getServiceAccount(ctx context.Context, accessKey string) (UserIdentity, *policy.Policy, error) {
+ sa, jwtClaims, err := sys.getAccountWithClaims(ctx, accessKey)
+ if err != nil {
+ if err == errNoSuchAccount {
+ return UserIdentity{}, nil, errNoSuchServiceAccount
+ }
+ return UserIdentity{}, nil, err
+ }
+ if !sa.Credentials.IsServiceAccount() {
+ return UserIdentity{}, nil, errNoSuchServiceAccount
+ }
+
+ var embeddedPolicy *policy.Policy
+
+ pt, ptok := jwtClaims.Lookup(iamPolicyClaimNameSA())
+ sp, spok := jwtClaims.Lookup(policy.SessionPolicyName)
+ if ptok && spok && pt == embeddedPolicyType {
+ policyBytes, err := base64.StdEncoding.DecodeString(sp)
+ if err != nil {
+ return UserIdentity{}, nil, err
+ }
+ embeddedPolicy, err = policy.ParseConfig(bytes.NewReader(policyBytes))
+ if err != nil {
+ return UserIdentity{}, nil, err
+ }
+ }
+
+ return sa, embeddedPolicy, nil
+}
+
+// GetTemporaryAccount - wrapper method to get information about a temporary account
+func (sys *IAMSys) GetTemporaryAccount(ctx context.Context, accessKey string) (auth.Credentials, *policy.Policy, error) {
+ if !sys.Initialized() {
+ return auth.Credentials{}, nil, errServerNotInitialized
+ }
+ tmpAcc, embeddedPolicy, err := sys.getTempAccount(ctx, accessKey)
+ if err != nil {
+ if err == errNoSuchTempAccount {
+ sys.store.LoadUser(ctx, accessKey)
+ tmpAcc, embeddedPolicy, err = sys.getTempAccount(ctx, accessKey)
+ }
+ if err != nil {
+ return auth.Credentials{}, nil, err
+ }
+ }
+ // Hide secret & session keys
+ tmpAcc.Credentials.SecretKey = ""
+ tmpAcc.Credentials.SessionToken = ""
+ return tmpAcc.Credentials, embeddedPolicy, nil
+}
+
+func (sys *IAMSys) getTempAccount(ctx context.Context, accessKey string) (UserIdentity, *policy.Policy, error) {
+ tmpAcc, claims, err := sys.getAccountWithClaims(ctx, accessKey)
+ if err != nil {
+ if err == errNoSuchAccount {
+ return UserIdentity{}, nil, errNoSuchTempAccount
+ }
+ return UserIdentity{}, nil, err
+ }
+ if !tmpAcc.Credentials.IsTemp() {
+ return UserIdentity{}, nil, errNoSuchTempAccount
+ }
+
+ var embeddedPolicy *policy.Policy
+
+ sp, spok := claims.Lookup(policy.SessionPolicyName)
+ if spok {
+ policyBytes, err := base64.StdEncoding.DecodeString(sp)
+ if err != nil {
+ return UserIdentity{}, nil, err
+ }
+ embeddedPolicy, err = policy.ParseConfig(bytes.NewReader(policyBytes))
+ if err != nil {
+ return UserIdentity{}, nil, err
+ }
+ }
+
+ return tmpAcc, embeddedPolicy, nil
+}
+
+// getAccountWithClaims - gets information about an account with claims
+func (sys *IAMSys) getAccountWithClaims(ctx context.Context, accessKey string) (UserIdentity, *jwt.MapClaims, error) {
+ if !sys.Initialized() {
+ return UserIdentity{}, nil, errServerNotInitialized
+ }
+
+ acc, ok := sys.store.GetUser(accessKey)
+ if !ok {
+ return UserIdentity{}, nil, errNoSuchAccount
+ }
+
+ jwtClaims, err := extractJWTClaims(acc)
+ if err != nil {
+ return UserIdentity{}, nil, err
+ }
+
+ return acc, jwtClaims, nil
+}
+
+// GetClaimsForSvcAcc - gets the claims associated with the service account.
+func (sys *IAMSys) GetClaimsForSvcAcc(ctx context.Context, accessKey string) (map[string]any, error) {
+ if !sys.Initialized() {
+ return nil, errServerNotInitialized
+ }
+
+ sa, ok := sys.store.GetUser(accessKey)
+ if !ok || !sa.Credentials.IsServiceAccount() {
+ return nil, errNoSuchServiceAccount
+ }
+
+ jwtClaims, err := extractJWTClaims(sa)
+ if err != nil {
+ return nil, err
+ }
+
+ return jwtClaims.Map(), nil
+}
+
+// DeleteServiceAccount - delete a service account
+func (sys *IAMSys) DeleteServiceAccount(ctx context.Context, accessKey string, notifyPeers bool) error {
+ if !sys.Initialized() {
+ return errServerNotInitialized
+ }
+
+ sa, ok := sys.store.GetUser(accessKey)
+ if !ok || !sa.Credentials.IsServiceAccount() {
+ return nil
+ }
+
+ if err := sys.store.DeleteUser(ctx, accessKey, svcUser); err != nil {
+ return err
+ }
+
+ if notifyPeers && !sys.HasWatcher() {
+ for _, nerr := range globalNotificationSys.DeleteServiceAccount(ctx, accessKey) {
+ if nerr.Err != nil {
+ logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
+ iamLogIf(ctx, nerr.Err)
+ }
+ }
+ }
+
+ return nil
+}
+
+// CreateUser - create new user credentials and policy, if user already exists
+// they shall be rewritten with new inputs.
+func (sys *IAMSys) CreateUser(ctx context.Context, accessKey string, ureq madmin.AddOrUpdateUserReq) (updatedAt time.Time, err error) {
+ if !sys.Initialized() {
+ return updatedAt, errServerNotInitialized
+ }
+
+ if !auth.IsAccessKeyValid(accessKey) {
+ return updatedAt, auth.ErrInvalidAccessKeyLength
+ }
+
+ if auth.ContainsReservedChars(accessKey) {
+ return updatedAt, auth.ErrContainsReservedChars
+ }
+
+ if !auth.IsSecretKeyValid(ureq.SecretKey) {
+ return updatedAt, auth.ErrInvalidSecretKeyLength
+ }
+
+ updatedAt, err = sys.store.AddUser(ctx, accessKey, ureq)
+ if err != nil {
+ return updatedAt, err
+ }
+
+ sys.notifyForUser(ctx, accessKey, false)
+ return updatedAt, nil
+}
+
+// SetUserSecretKey - sets user secret key
+func (sys *IAMSys) SetUserSecretKey(ctx context.Context, accessKey string, secretKey string) error {
+ if !sys.Initialized() {
+ return errServerNotInitialized
+ }
+
+ if sys.usersSysType != MinIOUsersSysType {
+ return errIAMActionNotAllowed
+ }
+
+ if !auth.IsAccessKeyValid(accessKey) {
+ return auth.ErrInvalidAccessKeyLength
+ }
+
+ if !auth.IsSecretKeyValid(secretKey) {
+ return auth.ErrInvalidSecretKeyLength
+ }
+
+ return sys.store.UpdateUserSecretKey(ctx, accessKey, secretKey)
+}
+
+// purgeExpiredCredentialsForExternalSSO - validates if local credentials are still valid
+// by checking remote IDP if the relevant users are still active and present.
+func (sys *IAMSys) purgeExpiredCredentialsForExternalSSO(ctx context.Context) {
+ parentUsersMap := sys.store.GetAllParentUsers()
+ var expiredUsers []string
+ for parentUser, puInfo := range parentUsersMap {
+ // There are multiple role ARNs for parent user only when there
+ // are multiple openid provider configurations with the same ID
+ // provider. We lookup the provider associated with some one of
+ // the roleARNs to check if the user still exists. If they don't
+ // we can safely remove credentials for this parent user
+ // associated with any of the provider configurations.
+ //
+ // If there is no roleARN mapped to the user, the user may be
+ // coming from a policy claim based openid provider.
+ roleArns := puInfo.roleArns.ToSlice()
+ var roleArn string
+ if len(roleArns) == 0 {
+ iamLogIf(GlobalContext,
+ fmt.Errorf("parentUser: %s had no roleArns mapped!", parentUser))
+ continue
+ }
+ roleArn = roleArns[0]
+ u, err := sys.OpenIDConfig.LookupUser(roleArn, puInfo.subClaimValue)
+ if err != nil {
+ iamLogIf(GlobalContext, err)
+ continue
+ }
+ // If user is set to "disabled", we will remove them
+ // subsequently.
+ if !u.Enabled {
+ expiredUsers = append(expiredUsers, parentUser)
+ }
+ }
+
+ // We ignore any errors
+ _ = sys.store.DeleteUsers(ctx, expiredUsers)
+}
+
+// purgeExpiredCredentialsForLDAP - validates if local credentials are still
+// valid by checking LDAP server if the relevant users are still present.
+func (sys *IAMSys) purgeExpiredCredentialsForLDAP(ctx context.Context) {
+ parentUsers := sys.store.GetAllParentUsers()
+ var allDistNames []string
+ for parentUser, info := range parentUsers {
+ if !sys.LDAPConfig.IsLDAPUserDN(parentUser) {
+ continue
+ }
+
+ if info.subClaimValue != "" {
+ // we need to ask LDAP about the actual user DN not normalized DN.
+ allDistNames = append(allDistNames, info.subClaimValue)
+ } else {
+ allDistNames = append(allDistNames, parentUser)
+ }
+ }
+
+ expiredUsers, err := sys.LDAPConfig.GetNonEligibleUserDistNames(allDistNames)
+ if err != nil {
+ // Log and return on error - perhaps it'll work the next time.
+ iamLogIf(GlobalContext, err)
+ return
+ }
+
+ // We ignore any errors
+ _ = sys.store.DeleteUsers(ctx, expiredUsers)
+}
+
+// updateGroupMembershipsForLDAP - updates the list of groups associated with the credential.
+func (sys *IAMSys) updateGroupMembershipsForLDAP(ctx context.Context) {
+ // 1. Collect all LDAP users with active creds.
+ allCreds := sys.store.GetSTSAndServiceAccounts()
+ // List of unique LDAP (parent) user DNs that have active creds
+ var parentUserActualDNList []string
+ // Map of LDAP user (internal representation) to list of active credential objects
+ parentUserToCredsMap := make(map[string][]auth.Credentials)
+ // DN to ldap username mapping for each LDAP user
+ actualDNToLDAPUsernameMap := make(map[string]string)
+ // External (actual) LDAP DN to internal normalized representation
+ actualDNToParentUserMap := make(map[string]string)
+ for _, cred := range allCreds {
+ // Expired credentials don't need parent user updates.
+ if cred.IsExpired() {
+ continue
+ }
+
+ if !sys.LDAPConfig.IsLDAPUserDN(cred.ParentUser) {
+ continue
+ }
+
+ // Check if this is the first time we are
+ // encountering this LDAP user.
+ if _, ok := parentUserToCredsMap[cred.ParentUser]; !ok {
+ // Try to find the ldapUsername for this
+ // parentUser by extracting JWT claims
+ var (
+ jwtClaims *jwt.MapClaims
+ err error
+ )
+
+ if cred.SessionToken == "" {
+ continue
+ }
+
+ if cred.IsServiceAccount() {
+ jwtClaims, err = auth.ExtractClaims(cred.SessionToken, cred.SecretKey)
+ if err != nil {
+ jwtClaims, err = auth.ExtractClaims(cred.SessionToken, globalActiveCred.SecretKey)
+ }
+ } else {
+ var secretKey string
+ secretKey, err = getTokenSigningKey()
+ if err != nil {
+ continue
+ }
+ jwtClaims, err = auth.ExtractClaims(cred.SessionToken, secretKey)
+ }
+ if err != nil {
+ // skip this cred - session token seems invalid
+ continue
+ }
+
+ ldapUsername, okUserN := jwtClaims.Lookup(ldapUserN)
+ ldapActualDN, okDN := jwtClaims.Lookup(ldapActualUser)
+ if !okUserN || !okDN {
+ // skip this cred - we dont have the
+ // username info needed
+ continue
+ }
+
+ // Collect each new cred.ParentUser into parentUsers
+ parentUserActualDNList = append(parentUserActualDNList, ldapActualDN)
+
+ // Update the ldapUsernameMap
+ actualDNToLDAPUsernameMap[ldapActualDN] = ldapUsername
+
+ // Update the actualDNToParentUserMap
+ actualDNToParentUserMap[ldapActualDN] = cred.ParentUser
+ }
+ parentUserToCredsMap[cred.ParentUser] = append(parentUserToCredsMap[cred.ParentUser], cred)
+ }
+
+ // 2. Query LDAP server for groups of the LDAP users collected.
+ updatedGroups, err := sys.LDAPConfig.LookupGroupMemberships(parentUserActualDNList, actualDNToLDAPUsernameMap)
+ if err != nil {
+ // Log and return on error - perhaps it'll work the next time.
+ iamLogIf(GlobalContext, err)
+ return
+ }
+
+ // 3. Update creds for those users whose groups are changed
+ for _, parentActualDN := range parentUserActualDNList {
+ currGroupsSet := updatedGroups[parentActualDN]
+ parentUser := actualDNToParentUserMap[parentActualDN]
+ currGroups := currGroupsSet.ToSlice()
+ for _, cred := range parentUserToCredsMap[parentUser] {
+ gSet := set.CreateStringSet(cred.Groups...)
+ if gSet.Equals(currGroupsSet) {
+ // No change to groups memberships for this
+ // credential.
+ continue
+ }
+
+ // Expired credentials don't need group membership updates.
+ if cred.IsExpired() {
+ continue
+ }
+
+ cred.Groups = currGroups
+ if err := sys.store.UpdateUserIdentity(ctx, cred); err != nil {
+ // Log and continue error - perhaps it'll work the next time.
+ iamLogIf(GlobalContext, err)
+ }
+ }
+ }
+}
+
+// NormalizeLDAPAccessKeypairs - normalize the access key pairs (service
+// accounts) for LDAP users. This normalizes the parent user and the group names
+// whenever the parent user parses validly as a DN.
+func (sys *IAMSys) NormalizeLDAPAccessKeypairs(ctx context.Context, accessKeyMap map[string]madmin.SRSvcAccCreate,
+) (skippedAccessKeys []string, err error) {
+ conn, err := sys.LDAPConfig.LDAP.Connect()
+ if err != nil {
+ return skippedAccessKeys, err
+ }
+ defer conn.Close()
+
+ // Bind to the lookup user account
+ if err = sys.LDAPConfig.LDAP.LookupBind(conn); err != nil {
+ return skippedAccessKeys, err
+ }
+
+ var collectedErrors []error
+ updatedKeysMap := make(map[string]madmin.SRSvcAccCreate)
+ for ak, createReq := range accessKeyMap {
+ parent := createReq.Parent
+ groups := createReq.Groups
+
+ _, err := ldap.NormalizeDN(parent)
+ if err != nil {
+ // not a valid DN, ignore.
+ continue
+ }
+
+ hasDiff := false
+
+ // For the parent value, we require that the parent exists in the LDAP
+ // server and is under a configured base DN.
+ validatedParent, isUnderBaseDN, err := sys.LDAPConfig.GetValidatedUserDN(conn, parent)
+ if err != nil {
+ collectedErrors = append(collectedErrors, fmt.Errorf("could not validate parent exists in LDAP directory: %w", err))
+ continue
+ }
+ if validatedParent == nil || !isUnderBaseDN {
+ skippedAccessKeys = append(skippedAccessKeys, ak)
+ continue
+ }
+
+ if validatedParent.NormDN != parent {
+ hasDiff = true
+ }
+
+ var normalizedGroups []string
+ for _, group := range groups {
+ // For a group, we store the normalized DN even if it not under a
+ // configured base DN.
+ validatedGroup, _, err := sys.LDAPConfig.GetValidatedGroupDN(conn, group)
+ if err != nil {
+ collectedErrors = append(collectedErrors, fmt.Errorf("could not validate group exists in LDAP directory: %w", err))
+ continue
+ }
+ if validatedGroup == nil {
+ // DN group was not found in the LDAP directory for access-key
+ continue
+ }
+
+ if validatedGroup.NormDN != group {
+ hasDiff = true
+ }
+ normalizedGroups = append(normalizedGroups, validatedGroup.NormDN)
+ }
+
+ if hasDiff {
+ updatedCreateReq := createReq
+ updatedCreateReq.Parent = validatedParent.NormDN
+ updatedCreateReq.Groups = normalizedGroups
+
+ updatedKeysMap[ak] = updatedCreateReq
+ }
+ }
+
+ // if there are any errors, return a collected error.
+ if len(collectedErrors) > 0 {
+ return skippedAccessKeys, fmt.Errorf("errors validating LDAP DN: %w", errors.Join(collectedErrors...))
+ }
+
+ // Replace the map values with the updated ones
+ maps.Copy(accessKeyMap, updatedKeysMap)
+
+ return skippedAccessKeys, nil
+}
+
+func (sys *IAMSys) getStoredLDAPPolicyMappingKeys(ctx context.Context, isGroup bool) set.StringSet {
+ entityKeysInStorage := set.NewStringSet()
+ cache := sys.store.rlock()
+ defer sys.store.runlock()
+ cachedPolicyMap := cache.iamSTSPolicyMap
+ if isGroup {
+ cachedPolicyMap = cache.iamGroupPolicyMap
+ }
+ cachedPolicyMap.Range(func(k string, v MappedPolicy) bool {
+ entityKeysInStorage.Add(k)
+ return true
+ })
+
+ return entityKeysInStorage
+}
+
+// NormalizeLDAPMappingImport - validates the LDAP policy mappings. Keys in the
+// given map may not correspond to LDAP DNs - these keys are ignored.
+//
+// For validated mappings, it updates the key in the given map to be in
+// normalized form.
+func (sys *IAMSys) NormalizeLDAPMappingImport(ctx context.Context, isGroup bool,
+ policyMap map[string]MappedPolicy,
+) ([]string, error) {
+ conn, err := sys.LDAPConfig.LDAP.Connect()
+ if err != nil {
+ return []string{}, err
+ }
+ defer conn.Close()
+
+ // Bind to the lookup user account
+ if err = sys.LDAPConfig.LDAP.LookupBind(conn); err != nil {
+ return []string{}, err
+ }
+
+ // We map keys that correspond to LDAP DNs and validate that they exist in
+ // the LDAP server.
+ dnValidator := sys.LDAPConfig.GetValidatedUserDN
+ if isGroup {
+ dnValidator = sys.LDAPConfig.GetValidatedGroupDN
+ }
+
+ // map of normalized DN keys to original keys.
+ normalizedDNKeysMap := make(map[string][]string)
+ var collectedErrors []error
+ var skipped []string
+ for k := range policyMap {
+ _, err := ldap.NormalizeDN(k)
+ if err != nil {
+ // not a valid DN, ignore.
+ continue
+ }
+ validatedDN, underBaseDN, err := dnValidator(conn, k)
+ if err != nil {
+ collectedErrors = append(collectedErrors, fmt.Errorf("could not validate `%s` exists in LDAP directory: %w", k, err))
+ continue
+ }
+ if validatedDN == nil || !underBaseDN {
+ skipped = append(skipped, k)
+ continue
+ }
+
+ if validatedDN.NormDN != k {
+ normalizedDNKeysMap[validatedDN.NormDN] = append(normalizedDNKeysMap[validatedDN.NormDN], k)
+ }
+ }
+
+ // if there are any errors, return a collected error.
+ if len(collectedErrors) > 0 {
+ return []string{}, fmt.Errorf("errors validating LDAP DN: %w", errors.Join(collectedErrors...))
+ }
+
+ entityKeysInStorage := sys.getStoredLDAPPolicyMappingKeys(ctx, isGroup)
+
+ for normKey, origKeys := range normalizedDNKeysMap {
+ if len(origKeys) > 1 {
+ // If there are multiple DN keys that normalize to the same value,
+ // check if the policy mappings are equal, if they are we don't need
+ // to return an error.
+ policiesDiffer := false
+ firstMappedPolicies := policyMap[origKeys[0]].policySet()
+ for i := 1; i < len(origKeys); i++ {
+ otherMappedPolicies := policyMap[origKeys[i]].policySet()
+ if !firstMappedPolicies.Equals(otherMappedPolicies) {
+ policiesDiffer = true
+ break
+ }
+ }
+
+ if policiesDiffer {
+ return []string{}, fmt.Errorf("multiple DNs map to the same LDAP DN[%s]: %v; please remove DNs that are not needed",
+ normKey, origKeys)
+ }
+
+ if len(origKeys[1:]) > 0 {
+ // Log that extra DN mappings will not be imported.
+ iamLogEvent(ctx, "import-ldap-normalize: extraneous DN mappings found for LDAP DN[%s]: %v will not be imported", origKeys[0], origKeys[1:])
+ }
+
+ // Policies mapped to the DN's are the same, so we remove the extra
+ // ones from the map.
+ for i := 1; i < len(origKeys); i++ {
+ delete(policyMap, origKeys[i])
+
+ // Remove the mapping from storage by setting the policy to "".
+ if entityKeysInStorage.Contains(origKeys[i]) {
+ // Ignore any deletion error.
+ _, delErr := sys.PolicyDBSet(ctx, origKeys[i], "", stsUser, isGroup)
+ if delErr != nil {
+ logErr := fmt.Errorf("failed to delete extraneous LDAP DN mapping for `%s`: %w", origKeys[i], delErr)
+ iamLogIf(ctx, logErr)
+ }
+ }
+ }
+ }
+
+ // Replacing origKeys[0] with normKey in the policyMap
+
+ // len(origKeys) is always > 0, so here len(origKeys) == 1
+ mappingValue := policyMap[origKeys[0]]
+ delete(policyMap, origKeys[0])
+ policyMap[normKey] = mappingValue
+ iamLogEvent(ctx, "import-ldap-normalize: normalized LDAP DN mapping from `%s` to `%s`", origKeys[0], normKey)
+
+ // Remove the mapping from storage by setting the policy to "".
+ if entityKeysInStorage.Contains(origKeys[0]) {
+ // Ignore any deletion error.
+ _, delErr := sys.PolicyDBSet(ctx, origKeys[0], "", stsUser, isGroup)
+ if delErr != nil {
+ logErr := fmt.Errorf("failed to delete extraneous LDAP DN mapping for `%s`: %w", origKeys[0], delErr)
+ iamLogIf(ctx, logErr)
+ }
+ }
+ }
+ return skipped, nil
+}
+
+// CheckKey validates the incoming accessKey
+func (sys *IAMSys) CheckKey(ctx context.Context, accessKey string) (u UserIdentity, ok bool, err error) {
+ if !sys.Initialized() {
+ return u, false, nil
+ }
+
+ if accessKey == globalActiveCred.AccessKey {
+ return newUserIdentity(globalActiveCred), true, nil
+ }
+
+ loadUserCalled := false
+ select {
+ case <-sys.configLoaded:
+ default:
+ err = sys.store.LoadUser(ctx, accessKey)
+ loadUserCalled = true
+ }
+
+ u, ok = sys.store.GetUser(accessKey)
+ if !ok && !loadUserCalled {
+ err = sys.store.LoadUser(ctx, accessKey)
+ loadUserCalled = true
+
+ u, ok = sys.store.GetUser(accessKey)
+ }
+
+ if !ok && loadUserCalled && err != nil {
+ iamLogOnceIf(ctx, err, accessKey)
+
+ // return 503 to application
+ return u, false, errIAMNotInitialized
+ }
+
+ return u, ok && u.Credentials.IsValid(), nil
+}
+
+// GetUser - get user credentials
+func (sys *IAMSys) GetUser(ctx context.Context, accessKey string) (u UserIdentity, ok bool) {
+ u, ok, _ = sys.CheckKey(ctx, accessKey)
+ return u, ok
+}
+
+// Notify all other MinIO peers to load group.
+func (sys *IAMSys) notifyForGroup(ctx context.Context, group string) {
+ if !sys.HasWatcher() {
+ for _, nerr := range globalNotificationSys.LoadGroup(ctx, group) {
+ if nerr.Err != nil {
+ logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
+ iamLogIf(ctx, nerr.Err)
+ }
+ }
+ }
+}
+
+// AddUsersToGroup - adds users to a group, creating the group if
+// needed. No error if user(s) already are in the group.
+func (sys *IAMSys) AddUsersToGroup(ctx context.Context, group string, members []string) (updatedAt time.Time, err error) {
+ if !sys.Initialized() {
+ return updatedAt, errServerNotInitialized
+ }
+
+ if auth.ContainsReservedChars(group) {
+ return updatedAt, errGroupNameContainsReservedChars
+ }
+
+ updatedAt, err = sys.store.AddUsersToGroup(ctx, group, members)
+ if err != nil {
+ return updatedAt, err
+ }
+
+ sys.notifyForGroup(ctx, group)
+ return updatedAt, nil
+}
+
+// RemoveUsersFromGroup - remove users from group. If no users are
+// given, and the group is empty, deletes the group as well.
+func (sys *IAMSys) RemoveUsersFromGroup(ctx context.Context, group string, members []string) (updatedAt time.Time, err error) {
+ if !sys.Initialized() {
+ return updatedAt, errServerNotInitialized
+ }
+
+ if sys.usersSysType != MinIOUsersSysType {
+ return updatedAt, errIAMActionNotAllowed
+ }
+
+ updatedAt, err = sys.store.RemoveUsersFromGroup(ctx, group, members)
+ if err != nil {
+ return updatedAt, err
+ }
+
+ sys.notifyForGroup(ctx, group)
+ return updatedAt, nil
+}
+
+// SetGroupStatus - enable/disabled a group
+func (sys *IAMSys) SetGroupStatus(ctx context.Context, group string, enabled bool) (updatedAt time.Time, err error) {
+ if !sys.Initialized() {
+ return updatedAt, errServerNotInitialized
+ }
+
+ if sys.usersSysType != MinIOUsersSysType {
+ return updatedAt, errIAMActionNotAllowed
+ }
+
+ updatedAt, err = sys.store.SetGroupStatus(ctx, group, enabled)
+ if err != nil {
+ return updatedAt, err
+ }
+
+ sys.notifyForGroup(ctx, group)
+ return updatedAt, nil
+}
+
+// GetGroupDescription - builds up group description
+func (sys *IAMSys) GetGroupDescription(group string) (gd madmin.GroupDesc, err error) {
+ if !sys.Initialized() {
+ return gd, errServerNotInitialized
+ }
+
+ return sys.store.GetGroupDescription(group)
+}
+
+// ListGroups - lists groups.
+func (sys *IAMSys) ListGroups(ctx context.Context) (r []string, err error) {
+ if !sys.Initialized() {
+ return r, errServerNotInitialized
+ }
+
+ select {
+ case <-sys.configLoaded:
+ return sys.store.ListGroups(ctx)
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ }
+}
+
+// PolicyDBSet - sets a policy for a user or group in the PolicyDB. This does
+// not validate if the user/group exists - that is the responsibility of the
+// caller.
+func (sys *IAMSys) PolicyDBSet(ctx context.Context, name, policy string, userType IAMUserType, isGroup bool) (updatedAt time.Time, err error) {
+ if !sys.Initialized() {
+ return updatedAt, errServerNotInitialized
+ }
+
+ updatedAt, err = sys.store.PolicyDBSet(ctx, name, policy, userType, isGroup)
+ if err != nil {
+ return updatedAt, err
+ }
+
+ // Notify all other MinIO peers to reload policy
+ if !sys.HasWatcher() {
+ for _, nerr := range globalNotificationSys.LoadPolicyMapping(ctx, name, userType, isGroup) {
+ if nerr.Err != nil {
+ logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
+ iamLogIf(ctx, nerr.Err)
+ }
+ }
+ }
+
+ return updatedAt, nil
+}
+
+// PolicyDBUpdateBuiltin - adds or removes policies from a user or a group
+// verified to be an internal IDP user.
+func (sys *IAMSys) PolicyDBUpdateBuiltin(ctx context.Context, isAttach bool,
+ r madmin.PolicyAssociationReq,
+) (updatedAt time.Time, addedOrRemoved, effectivePolicies []string, err error) {
+ if !sys.Initialized() {
+ err = errServerNotInitialized
+ return updatedAt, addedOrRemoved, effectivePolicies, err
+ }
+
+ userOrGroup := r.User
+ var isGroup bool
+ if userOrGroup == "" {
+ isGroup = true
+ userOrGroup = r.Group
+ }
+
+ if isGroup {
+ _, err = sys.GetGroupDescription(userOrGroup)
+ if err != nil {
+ return updatedAt, addedOrRemoved, effectivePolicies, err
+ }
+ } else {
+ var isTemp bool
+ isTemp, _, err = sys.IsTempUser(userOrGroup)
+ if err != nil && err != errNoSuchUser {
+ return updatedAt, addedOrRemoved, effectivePolicies, err
+ }
+ if isTemp {
+ err = errIAMActionNotAllowed
+ return updatedAt, addedOrRemoved, effectivePolicies, err
+ }
+
+ // When the user is root credential you are not allowed to
+ // add policies for root user.
+ if userOrGroup == globalActiveCred.AccessKey {
+ err = errIAMActionNotAllowed
+ return updatedAt, addedOrRemoved, effectivePolicies, err
+ }
+
+ // Validate that user exists.
+ var userExists bool
+ _, userExists = sys.GetUser(ctx, userOrGroup)
+ if !userExists {
+ err = errNoSuchUser
+ return updatedAt, addedOrRemoved, effectivePolicies, err
+ }
+ }
+
+ updatedAt, addedOrRemoved, effectivePolicies, err = sys.store.PolicyDBUpdate(ctx, userOrGroup, isGroup,
+ regUser, r.Policies, isAttach)
+ if err != nil {
+ return updatedAt, addedOrRemoved, effectivePolicies, err
+ }
+
+ // Notify all other MinIO peers to reload policy
+ if !sys.HasWatcher() {
+ for _, nerr := range globalNotificationSys.LoadPolicyMapping(ctx, userOrGroup, regUser, isGroup) {
+ if nerr.Err != nil {
+ logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
+ iamLogIf(ctx, nerr.Err)
+ }
+ }
+ }
+
+ replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
+ Type: madmin.SRIAMItemPolicyMapping,
+ PolicyMapping: &madmin.SRPolicyMapping{
+ UserOrGroup: userOrGroup,
+ UserType: int(regUser),
+ IsGroup: isGroup,
+ Policy: strings.Join(effectivePolicies, ","),
+ },
+ UpdatedAt: updatedAt,
+ }))
+
+ return updatedAt, addedOrRemoved, effectivePolicies, err
+}
+
+// PolicyDBUpdateLDAP - adds or removes policies from a user or a group verified
+// to be in the LDAP directory.
+func (sys *IAMSys) PolicyDBUpdateLDAP(ctx context.Context, isAttach bool,
+ r madmin.PolicyAssociationReq,
+) (updatedAt time.Time, addedOrRemoved, effectivePolicies []string, err error) {
+ if !sys.Initialized() {
+ err = errServerNotInitialized
+ return updatedAt, addedOrRemoved, effectivePolicies, err
+ }
+
+ var dn string
+ var dnResult *ldap.DNSearchResult
+ var isGroup bool
+ if r.User != "" {
+ dnResult, err = sys.LDAPConfig.GetValidatedDNForUsername(r.User)
+ if err != nil {
+ iamLogIf(ctx, err)
+ return updatedAt, addedOrRemoved, effectivePolicies, err
+ }
+ if dnResult == nil {
+ // dn not found - still attempt to detach if provided user is a DN.
+ if !isAttach && sys.LDAPConfig.IsLDAPUserDN(r.User) {
+ dn = sys.LDAPConfig.QuickNormalizeDN(r.User)
+ } else {
+ err = errNoSuchUser
+ return updatedAt, addedOrRemoved, effectivePolicies, err
+ }
+ } else {
+ dn = dnResult.NormDN
+ }
+ isGroup = false
+ } else {
+ var underBaseDN bool
+ if dnResult, underBaseDN, err = sys.LDAPConfig.GetValidatedGroupDN(nil, r.Group); err != nil {
+ iamLogIf(ctx, err)
+ return updatedAt, addedOrRemoved, effectivePolicies, err
+ }
+ if dnResult == nil || !underBaseDN {
+ if !isAttach {
+ dn = sys.LDAPConfig.QuickNormalizeDN(r.Group)
+ } else {
+ err = errNoSuchGroup
+ return updatedAt, addedOrRemoved, effectivePolicies, err
+ }
+ } else {
+ // We use the group DN returned by the LDAP server (this may not
+ // equal the input group name, but we assume it is canonical).
+ dn = dnResult.NormDN
+ }
+ isGroup = true
+ }
+
+ // Backward compatibility in detaching non-normalized DNs.
+ if !isAttach {
+ var oldDN string
+ if isGroup {
+ oldDN = r.Group
+ } else {
+ oldDN = r.User
+ }
+ if oldDN != dn {
+ sys.store.PolicyDBUpdate(ctx, oldDN, isGroup, stsUser, r.Policies, isAttach)
+ }
+ }
+
+ userType := stsUser
+ updatedAt, addedOrRemoved, effectivePolicies, err = sys.store.PolicyDBUpdate(
+ ctx, dn, isGroup, userType, r.Policies, isAttach)
+ if err != nil {
+ return updatedAt, addedOrRemoved, effectivePolicies, err
+ }
+
+ // Notify all other MinIO peers to reload policy
+ if !sys.HasWatcher() {
+ for _, nerr := range globalNotificationSys.LoadPolicyMapping(ctx, dn, userType, isGroup) {
+ if nerr.Err != nil {
+ logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
+ iamLogIf(ctx, nerr.Err)
+ }
+ }
+ }
+
+ replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
+ Type: madmin.SRIAMItemPolicyMapping,
+ PolicyMapping: &madmin.SRPolicyMapping{
+ UserOrGroup: dn,
+ UserType: int(userType),
+ IsGroup: isGroup,
+ Policy: strings.Join(effectivePolicies, ","),
+ },
+ UpdatedAt: updatedAt,
+ }))
+
+ return updatedAt, addedOrRemoved, effectivePolicies, err
+}
+
+// PolicyDBGet - gets policy set on a user or group. If a list of groups is
+// given, policies associated with them are included as well.
+func (sys *IAMSys) PolicyDBGet(name string, groups ...string) ([]string, error) {
+ if !sys.Initialized() {
+ return nil, errServerNotInitialized
+ }
+
+ return sys.store.PolicyDBGet(name, groups...)
+}
+
+const sessionPolicyNameExtracted = policy.SessionPolicyName + "-extracted"
+
+// IsAllowedServiceAccount - checks if the given service account is allowed to perform
+// actions. The permission of the parent user is checked first
+func (sys *IAMSys) IsAllowedServiceAccount(args policy.Args, parentUser string) bool {
+ // Verify if the parent claim matches the parentUser.
+ p, ok := args.Claims[parentClaim]
+ if ok {
+ parentInClaim, ok := p.(string)
+ if !ok {
+ // Reject malformed/malicious requests.
+ return false
+ }
+ // The parent claim in the session token should be equal
+ // to the parent detected in the backend
+ if parentInClaim != parentUser {
+ return false
+ }
+ } else {
+ // This is needed so a malicious user cannot
+ // use a leaked session key of another user
+ // to widen its privileges.
+ return false
+ }
+
+ isOwnerDerived := parentUser == globalActiveCred.AccessKey
+
+ var err error
+ var svcPolicies []string
+ roleArn := args.GetRoleArn()
+
+ switch {
+ case isOwnerDerived:
+ // All actions are allowed by default and no policy evaluation is
+ // required.
+
+ case roleArn != "":
+ arn, err := arn.Parse(roleArn)
+ if err != nil {
+ iamLogIf(GlobalContext, fmt.Errorf("error parsing role ARN %s: %v", roleArn, err))
+ return false
+ }
+ svcPolicies = newMappedPolicy(sys.rolesMap[arn]).toSlice()
+ default:
+ // Check policy for parent user of service account.
+ svcPolicies, err = sys.PolicyDBGet(parentUser, args.Groups...)
+ if err != nil {
+ iamLogIf(GlobalContext, err)
+ return false
+ }
+
+ // Finally, if there is no parent policy, check if a policy claim is
+ // present.
+ if len(svcPolicies) == 0 {
+ policySet, _ := policy.GetPoliciesFromClaims(args.Claims, iamPolicyClaimNameOpenID())
+ svcPolicies = policySet.ToSlice()
+ }
+ }
+
+ // Defensive code: Do not allow any operation if no policy is found.
+ if !isOwnerDerived && len(svcPolicies) == 0 {
+ return false
+ }
+
+ var combinedPolicy policy.Policy
+ // Policies were found, evaluate all of them.
+ if !isOwnerDerived {
+ availablePoliciesStr, c := sys.store.MergePolicies(strings.Join(svcPolicies, ","))
+ if availablePoliciesStr == "" {
+ return false
+ }
+ combinedPolicy = c
+ }
+
+ parentArgs := args
+ parentArgs.AccountName = parentUser
+
+ saPolicyClaim, ok := args.Claims[iamPolicyClaimNameSA()]
+ if !ok {
+ return false
+ }
+
+ saPolicyClaimStr, ok := saPolicyClaim.(string)
+ if !ok {
+ // Sub policy if set, should be a string reject
+ // malformed/malicious requests.
+ return false
+ }
+
+ if saPolicyClaimStr == inheritedPolicyType {
+ return isOwnerDerived || combinedPolicy.IsAllowed(parentArgs)
+ }
+
+ // 3. If an inline session-policy is present, evaluate it.
+ hasSessionPolicy, isAllowedSP := isAllowedBySessionPolicyForServiceAccount(args)
+ if hasSessionPolicy {
+ return isAllowedSP && (isOwnerDerived || combinedPolicy.IsAllowed(parentArgs))
+ }
+
+ // Sub policy not set. Evaluate only the parent policies.
+ return (isOwnerDerived || combinedPolicy.IsAllowed(parentArgs))
+}
+
+// IsAllowedSTS is meant for STS based temporary credentials,
+// which implements claims validation and verification other than
+// applying policies.
+func (sys *IAMSys) IsAllowedSTS(args policy.Args, parentUser string) bool {
+ // 1. Determine mapped policies
+
+ isOwnerDerived := parentUser == globalActiveCred.AccessKey
+ var policies []string
+ roleArn := args.GetRoleArn()
+
+ switch {
+ case isOwnerDerived:
+ // All actions are allowed by default and no policy evaluation is
+ // required.
+
+ case roleArn != "":
+ // If a roleARN is present, the role policy is applied.
+ arn, err := arn.Parse(roleArn)
+ if err != nil {
+ iamLogIf(GlobalContext, fmt.Errorf("error parsing role ARN %s: %v", roleArn, err))
+ return false
+ }
+ policies = newMappedPolicy(sys.rolesMap[arn]).toSlice()
+
+ default:
+ // Otherwise, inherit parent user's policy
+ var err error
+ policies, err = sys.PolicyDBGet(parentUser, args.Groups...)
+ if err != nil {
+ iamLogIf(GlobalContext, fmt.Errorf("error fetching policies on %s: %v", parentUser, err))
+ return false
+ }
+
+ // Finally, if there is no parent policy, check if a policy claim is
+ // present in the session token.
+ if len(policies) == 0 {
+ // If there is no parent policy mapping, we fall back to
+ // using policy claim from JWT.
+ policySet, ok := args.GetPolicies(iamPolicyClaimNameOpenID())
+ if !ok {
+ // When claims are set, it should have a policy claim field.
+ return false
+ }
+ policies = policySet.ToSlice()
+ }
+ }
+
+ // Defensive code: Do not allow any operation if no policy is found in the session token
+ if !isOwnerDerived && len(policies) == 0 {
+ return false
+ }
+
+ // 2. Combine the mapped policies into a single combined policy.
+
+ var combinedPolicy policy.Policy
+ // Policies were found, evaluate all of them.
+ if !isOwnerDerived {
+ availablePoliciesStr, c := sys.store.MergePolicies(strings.Join(policies, ","))
+ if availablePoliciesStr == "" {
+ // all policies presented in the claim should exist
+ iamLogIf(GlobalContext, fmt.Errorf("expected policy (%s) missing from the JWT claim %s, rejecting the request", policies, iamPolicyClaimNameOpenID()))
+
+ return false
+ }
+ combinedPolicy = c
+ }
+
+ // 3. If an inline session-policy is present, evaluate it.
+
+ // Now check if we have a sessionPolicy.
+ hasSessionPolicy, isAllowedSP := isAllowedBySessionPolicy(args)
+ if hasSessionPolicy {
+ return isAllowedSP && (isOwnerDerived || combinedPolicy.IsAllowed(args))
+ }
+
+ // Sub policy not set, this is most common since subPolicy
+ // is optional, use the inherited policies.
+ return isOwnerDerived || combinedPolicy.IsAllowed(args)
+}
+
+func isAllowedBySessionPolicyForServiceAccount(args policy.Args) (hasSessionPolicy bool, isAllowed bool) {
+ hasSessionPolicy = false
+ isAllowed = false
+
+ // Now check if we have a sessionPolicy.
+ spolicy, ok := args.Claims[sessionPolicyNameExtracted]
+ if !ok {
+ return hasSessionPolicy, isAllowed
+ }
+
+ hasSessionPolicy = true
+
+ spolicyStr, ok := spolicy.(string)
+ if !ok {
+ // Sub policy if set, should be a string reject
+ // malformed/malicious requests.
+ return hasSessionPolicy, isAllowed
+ }
+
+ // Check if policy is parseable.
+ subPolicy, err := policy.ParseConfig(bytes.NewReader([]byte(spolicyStr)))
+ if err != nil {
+ // Log any error in input session policy config.
+ iamLogIf(GlobalContext, err)
+ return hasSessionPolicy, isAllowed
+ }
+
+ // SPECIAL CASE: For service accounts, any valid JSON is allowed as a
+ // policy, regardless of whether the number of statements is 0, this
+ // includes `null`, `{}` and `{"Statement": null}`. In fact, MinIO Console
+ // sends `null` when no policy is set and the intended behavior is that the
+ // service account should inherit parent policy. So when policy is empty in
+ // all fields we return hasSessionPolicy=false.
+ if subPolicy.Version == "" && subPolicy.Statements == nil && subPolicy.ID == "" {
+ hasSessionPolicy = false
+ return hasSessionPolicy, isAllowed
+ }
+
+ // As the session policy exists, even if the parent is the root account, it
+ // must be restricted by it. So, we set `.IsOwner` to false here
+ // unconditionally.
+ //
+ // We also set `DenyOnly` arg to false here - this is an IMPORTANT corner
+ // case: DenyOnly is used only for allowing an account to do actions related
+ // to its own account (like create service accounts for itself, among
+ // others). However when a session policy is present, we need to validate
+ // that the action is actually allowed, rather than checking if the action
+ // is only disallowed.
+ sessionPolicyArgs := args
+ sessionPolicyArgs.IsOwner = false
+ sessionPolicyArgs.DenyOnly = false
+
+ // Sub policy is set and valid.
+ return hasSessionPolicy, subPolicy.IsAllowed(sessionPolicyArgs)
+}
+
+func isAllowedBySessionPolicy(args policy.Args) (hasSessionPolicy bool, isAllowed bool) {
+ hasSessionPolicy = false
+ isAllowed = false
+
+ // Now check if we have a sessionPolicy.
+ spolicy, ok := args.Claims[sessionPolicyNameExtracted]
+ if !ok {
+ return hasSessionPolicy, isAllowed
+ }
+
+ hasSessionPolicy = true
+
+ spolicyStr, ok := spolicy.(string)
+ if !ok {
+ // Sub policy if set, should be a string reject
+ // malformed/malicious requests.
+ return hasSessionPolicy, isAllowed
+ }
+
+ // Check if policy is parseable.
+ subPolicy, err := policy.ParseConfig(bytes.NewReader([]byte(spolicyStr)))
+ if err != nil {
+ // Log any error in input session policy config.
+ iamLogIf(GlobalContext, err)
+ return hasSessionPolicy, isAllowed
+ }
+
+ // Policy without Version string value reject it.
+ if subPolicy.Version == "" {
+ return hasSessionPolicy, isAllowed
+ }
+
+ // As the session policy exists, even if the parent is the root account, it
+ // must be restricted by it. So, we set `.IsOwner` to false here
+ // unconditionally.
+ //
+ // We also set `DenyOnly` arg to false here - this is an IMPORTANT corner
+ // case: DenyOnly is used only for allowing an account to do actions related
+ // to its own account (like create service accounts for itself, among
+ // others). However when a session policy is present, we need to validate
+ // that the action is actually allowed, rather than checking if the action
+ // is only disallowed.
+ sessionPolicyArgs := args
+ sessionPolicyArgs.IsOwner = false
+ sessionPolicyArgs.DenyOnly = false
+
+ // Sub policy is set and valid.
+ return hasSessionPolicy, subPolicy.IsAllowed(sessionPolicyArgs)
+}
+
+// GetCombinedPolicy returns a combined policy combining all policies
+func (sys *IAMSys) GetCombinedPolicy(policies ...string) policy.Policy {
+ _, policy := sys.store.MergePolicies(strings.Join(policies, ","))
+ return policy
+}
+
+// doesPolicyAllow - checks if the given policy allows the passed action with given args. This is rarely needed.
+// Notice there is no account name involved, so this is a dangerous function.
+func (sys *IAMSys) doesPolicyAllow(policy string, args policy.Args) bool {
+ // Policies were found, evaluate all of them.
+ return sys.GetCombinedPolicy(policy).IsAllowed(args)
+}
+
+// IsAllowed - checks given policy args is allowed to continue the Rest API.
+func (sys *IAMSys) IsAllowed(args policy.Args) bool {
+ // If opa is configured, use OPA always.
+ if authz := newGlobalAuthZPluginFn(); authz != nil {
+ ok, err := authz.IsAllowed(args)
+ if err != nil {
+ authZLogIf(GlobalContext, err)
+ }
+ return ok
+ }
+
+ // Policies don't apply to the owner.
+ if args.IsOwner {
+ return true
+ }
+
+ // If the credential is temporary, perform STS related checks.
+ ok, parentUser, err := sys.IsTempUser(args.AccountName)
+ if err != nil {
+ return false
+ }
+ if ok {
+ return sys.IsAllowedSTS(args, parentUser)
+ }
+
+ // If the credential is for a service account, perform related check
+ ok, parentUser, err = sys.IsServiceAccount(args.AccountName)
+ if err != nil {
+ return false
+ }
+ if ok {
+ return sys.IsAllowedServiceAccount(args, parentUser)
+ }
+
+ // Continue with the assumption of a regular user
+ policies, err := sys.PolicyDBGet(args.AccountName, args.Groups...)
+ if err != nil {
+ return false
+ }
+
+ if len(policies) == 0 {
+ // No policy found.
+ return false
+ }
+
+ // Policies were found, evaluate all of them.
+ return sys.GetCombinedPolicy(policies...).IsAllowed(args)
+}
+
+// SetUsersSysType - sets the users system type, regular or LDAP.
+func (sys *IAMSys) SetUsersSysType(t UsersSysType) {
+ sys.usersSysType = t
+}
+
+// GetUsersSysType - returns the users system type for this IAM
+func (sys *IAMSys) GetUsersSysType() UsersSysType {
+ return sys.usersSysType
+}
+
+// NewIAMSys - creates new config system object.
+func NewIAMSys() *IAMSys {
+ return &IAMSys{
+ usersSysType: MinIOUsersSysType,
+ configLoaded: make(chan struct{}),
+ }
+}
diff --git a/cmd/ilm-config.go b/cmd/ilm-config.go
new file mode 100644
index 00000000..83e7e305
--- /dev/null
+++ b/cmd/ilm-config.go
@@ -0,0 +1,57 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "sync"
+
+ "github.com/minio/minio/internal/config/ilm"
+)
+
+var globalILMConfig = ilmConfig{
+ cfg: ilm.Config{
+ ExpirationWorkers: 100,
+ TransitionWorkers: 100,
+ },
+}
+
+type ilmConfig struct {
+ mu sync.RWMutex
+ cfg ilm.Config
+}
+
+func (c *ilmConfig) getExpirationWorkers() int {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+
+ return c.cfg.ExpirationWorkers
+}
+
+func (c *ilmConfig) getTransitionWorkers() int {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+
+ return c.cfg.TransitionWorkers
+}
+
+func (c *ilmConfig) update(cfg ilm.Config) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ c.cfg = cfg
+}
diff --git a/cmd/is-dir-empty_linux.go b/cmd/is-dir-empty_linux.go
new file mode 100644
index 00000000..99945d67
--- /dev/null
+++ b/cmd/is-dir-empty_linux.go
@@ -0,0 +1,43 @@
+//go:build linux && !appengine
+// +build linux,!appengine
+
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "syscall"
+)
+
+// Returns true if no error and there is no object or prefix inside this directory
+func isDirEmpty(dirname string, legacy bool) bool {
+ if legacy {
+ // On filesystems such as btrfs, nfs this is not true, so fallback
+ // to performing readdir() instead.
+ entries, err := readDirN(dirname, 1)
+ if err != nil {
+ return false
+ }
+ return len(entries) == 0
+ }
+ var stat syscall.Stat_t
+ if err := syscall.Stat(dirname, &stat); err != nil {
+ return false
+ }
+ return stat.Mode&syscall.S_IFMT == syscall.S_IFDIR && stat.Nlink == 2
+}
diff --git a/cmd/is-dir-empty_other.go b/cmd/is-dir-empty_other.go
new file mode 100644
index 00000000..ab7b2f7e
--- /dev/null
+++ b/cmd/is-dir-empty_other.go
@@ -0,0 +1,30 @@
+//go:build !linux
+// +build !linux
+
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+// isDirEmpty - returns true if there is no error and no object and prefix inside this directory
+func isDirEmpty(dirname string, _ bool) bool {
+ entries, err := readDirN(dirname, 1)
+ if err != nil {
+ return false
+ }
+ return len(entries) == 0
+}
diff --git a/cmd/jwt.go b/cmd/jwt.go
new file mode 100644
index 00000000..c86b8f67
--- /dev/null
+++ b/cmd/jwt.go
@@ -0,0 +1,139 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "errors"
+ "maps"
+ "net/http"
+ "time"
+
+ jwtgo "github.com/golang-jwt/jwt/v4"
+ jwtreq "github.com/golang-jwt/jwt/v4/request"
+ "github.com/minio/minio/internal/auth"
+ xjwt "github.com/minio/minio/internal/jwt"
+ "github.com/minio/pkg/v3/policy"
+)
+
+const (
+ jwtAlgorithm = "Bearer"
+
+ // Default JWT token for web handlers is one day.
+ defaultJWTExpiry = 24 * time.Hour
+
+ // Inter-node JWT token expiry is 100 years approx.
+ defaultInterNodeJWTExpiry = 100 * 365 * 24 * time.Hour
+)
+
+var (
+ errInvalidAccessKeyID = errors.New("The access key ID you provided does not exist in our records")
+ errAccessKeyDisabled = errors.New("The access key you provided is disabled")
+ errAuthentication = errors.New("Authentication failed, check your access credentials")
+ errNoAuthToken = errors.New("JWT token missing")
+ errSkewedAuthTime = errors.New("Skewed authentication date/time")
+ errMalformedAuth = errors.New("Malformed authentication input")
+)
+
+func authenticateNode(accessKey, secretKey string) (string, error) {
+ claims := xjwt.NewStandardClaims()
+ claims.SetExpiry(UTCNow().Add(defaultInterNodeJWTExpiry))
+ claims.SetAccessKey(accessKey)
+
+ jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, claims)
+ return jwt.SignedString([]byte(secretKey))
+}
+
+// Check if the request is authenticated.
+// Returns nil if the request is authenticated. errNoAuthToken if token missing.
+// Returns errAuthentication for all other errors.
+func metricsRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, []string, bool, error) {
+ token, err := jwtreq.AuthorizationHeaderExtractor.ExtractToken(req)
+ if err != nil {
+ if err == jwtreq.ErrNoTokenInRequest {
+ return nil, nil, false, errNoAuthToken
+ }
+ return nil, nil, false, err
+ }
+ claims := xjwt.NewMapClaims()
+ if err := xjwt.ParseWithClaims(token, claims, func(claims *xjwt.MapClaims) ([]byte, error) {
+ if claims.AccessKey != globalActiveCred.AccessKey {
+ u, ok := globalIAMSys.GetUser(req.Context(), claims.AccessKey)
+ if !ok {
+ // Credentials will be invalid but for disabled
+ // return a different error in such a scenario.
+ if u.Credentials.Status == auth.AccountOff {
+ return nil, errAccessKeyDisabled
+ }
+ return nil, errInvalidAccessKeyID
+ }
+ cred := u.Credentials
+ // Expired credentials return error.
+ if cred.IsTemp() && cred.IsExpired() {
+ return nil, errInvalidAccessKeyID
+ }
+ return []byte(cred.SecretKey), nil
+ } // this means claims.AccessKey == rootAccessKey
+ if !globalAPIConfig.permitRootAccess() {
+ // if root access is disabled, fail this request.
+ return nil, errAccessKeyDisabled
+ }
+ return []byte(globalActiveCred.SecretKey), nil
+ }); err != nil {
+ return claims, nil, false, errAuthentication
+ }
+ owner := true
+ var groups []string
+ if globalActiveCred.AccessKey != claims.AccessKey {
+ // Check if the access key is part of users credentials.
+ u, ok := globalIAMSys.GetUser(req.Context(), claims.AccessKey)
+ if !ok {
+ return nil, nil, false, errInvalidAccessKeyID
+ }
+ ucred := u.Credentials
+ // get embedded claims
+ eclaims, s3Err := checkClaimsFromToken(req, ucred)
+ if s3Err != ErrNone {
+ return nil, nil, false, errAuthentication
+ }
+
+ maps.Copy(claims.MapClaims, eclaims)
+
+ // if root access is disabled, disable all its service accounts and temporary credentials.
+ if ucred.ParentUser == globalActiveCred.AccessKey && !globalAPIConfig.permitRootAccess() {
+ return nil, nil, false, errAccessKeyDisabled
+ }
+
+ // Now check if we have a sessionPolicy.
+ if _, ok = eclaims[policy.SessionPolicyName]; ok {
+ owner = false
+ } else {
+ owner = globalActiveCred.AccessKey == ucred.ParentUser
+ }
+
+ groups = ucred.Groups
+ }
+
+ return claims, groups, owner, nil
+}
+
+// newCachedAuthToken returns the cached token.
+func newCachedAuthToken() func() string {
+ return func() string {
+ return globalNodeAuthToken
+ }
+}
diff --git a/cmd/jwt_test.go b/cmd/jwt_test.go
new file mode 100644
index 00000000..a45ce35c
--- /dev/null
+++ b/cmd/jwt_test.go
@@ -0,0 +1,190 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "net/http"
+ "os"
+ "testing"
+
+ jwtgo "github.com/golang-jwt/jwt/v4"
+ xjwt "github.com/minio/minio/internal/jwt"
+)
+
+func getTokenString(accessKey, secretKey string) (string, error) {
+ claims := xjwt.NewMapClaims()
+ claims.SetExpiry(UTCNow().Add(defaultJWTExpiry))
+ claims.SetAccessKey(accessKey)
+ token := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, claims)
+ return token.SignedString([]byte(secretKey))
+}
+
+// Tests web request authenticator.
+func TestWebRequestAuthenticate(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ obj, fsDir, err := prepareFS(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(fsDir)
+ if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil {
+ t.Fatal(err)
+ }
+
+ creds := globalActiveCred
+ token, err := getTokenString(creds.AccessKey, creds.SecretKey)
+ if err != nil {
+ t.Fatalf("unable get token %s", err)
+ }
+ testCases := []struct {
+ req *http.Request
+ expectedErr error
+ }{
+ // Set valid authorization header.
+ {
+ req: &http.Request{
+ Header: http.Header{
+ "Authorization": []string{token},
+ },
+ },
+ expectedErr: nil,
+ },
+ // No authorization header.
+ {
+ req: &http.Request{
+ Header: http.Header{},
+ },
+ expectedErr: errNoAuthToken,
+ },
+ // Invalid authorization token.
+ {
+ req: &http.Request{
+ Header: http.Header{
+ "Authorization": []string{"invalid-token"},
+ },
+ },
+ expectedErr: errAuthentication,
+ },
+ }
+
+ for i, testCase := range testCases {
+ _, _, _, gotErr := metricsRequestAuthenticate(testCase.req)
+ if testCase.expectedErr != gotErr {
+ t.Errorf("Test %d, expected err %s, got %s", i+1, testCase.expectedErr, gotErr)
+ }
+ }
+}
+
+func BenchmarkParseJWTStandardClaims(b *testing.B) {
+ ctx, cancel := context.WithCancel(b.Context())
+ defer cancel()
+
+ obj, fsDir, err := prepareFS(ctx)
+ if err != nil {
+ b.Fatal(err)
+ }
+ defer os.RemoveAll(fsDir)
+ if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil {
+ b.Fatal(err)
+ }
+
+ creds := globalActiveCred
+ token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ b.ResetTimer()
+ b.ReportAllocs()
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ err = xjwt.ParseWithStandardClaims(token, xjwt.NewStandardClaims(), []byte(creds.SecretKey))
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+}
+
+func BenchmarkParseJWTMapClaims(b *testing.B) {
+ ctx, cancel := context.WithCancel(b.Context())
+ defer cancel()
+
+ obj, fsDir, err := prepareFS(ctx)
+ if err != nil {
+ b.Fatal(err)
+ }
+ defer os.RemoveAll(fsDir)
+ if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil {
+ b.Fatal(err)
+ }
+
+ creds := globalActiveCred
+ token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ b.ResetTimer()
+ b.ReportAllocs()
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ err = xjwt.ParseWithClaims(token, xjwt.NewMapClaims(), func(*xjwt.MapClaims) ([]byte, error) {
+ return []byte(creds.SecretKey), nil
+ })
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+}
+
+func BenchmarkAuthenticateNode(b *testing.B) {
+ ctx, cancel := context.WithCancel(b.Context())
+ defer cancel()
+
+ obj, fsDir, err := prepareFS(ctx)
+ if err != nil {
+ b.Fatal(err)
+ }
+ defer os.RemoveAll(fsDir)
+ if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil {
+ b.Fatal(err)
+ }
+
+ creds := globalActiveCred
+ b.Run("uncached", func(b *testing.B) {
+ fn := authenticateNode
+ b.ResetTimer()
+ b.ReportAllocs()
+ for b.Loop() {
+ fn(creds.AccessKey, creds.SecretKey)
+ }
+ })
+ b.Run("cached", func(b *testing.B) {
+ fn := newCachedAuthToken()
+ b.ResetTimer()
+ b.ReportAllocs()
+ for b.Loop() {
+ fn()
+ }
+ })
+}
diff --git a/cmd/kms-handlers.go b/cmd/kms-handlers.go
new file mode 100644
index 00000000..ce5017c1
--- /dev/null
+++ b/cmd/kms-handlers.go
@@ -0,0 +1,328 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "crypto/subtle"
+ "encoding/json"
+ "net/http"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/auth"
+ "github.com/minio/minio/internal/kms"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/policy"
+)
+
+// KMSStatusHandler - GET /minio/kms/v1/status
+func (a kmsAPIHandlers) KMSStatusHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "KMSStatus")
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSStatusAction)
+ if objectAPI == nil {
+ return
+ }
+
+ if GlobalKMS == nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
+ return
+ }
+
+ stat, err := GlobalKMS.Status(ctx)
+ if err != nil {
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
+ return
+ }
+ resp, err := json.Marshal(stat)
+ if err != nil {
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
+ return
+ }
+ writeSuccessResponseJSON(w, resp)
+}
+
+// KMSMetricsHandler - GET /minio/kms/v1/metrics
+func (a kmsAPIHandlers) KMSMetricsHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "KMSMetrics")
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSMetricsAction)
+ if objectAPI == nil {
+ return
+ }
+
+ if GlobalKMS == nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
+ return
+ }
+
+ metrics, err := GlobalKMS.Metrics(ctx)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ if res, err := json.Marshal(metrics); err != nil {
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
+ } else {
+ writeSuccessResponseJSON(w, res)
+ }
+}
+
+// KMSAPIsHandler - GET /minio/kms/v1/apis
+func (a kmsAPIHandlers) KMSAPIsHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "KMSAPIs")
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSAPIAction)
+ if objectAPI == nil {
+ return
+ }
+
+ if GlobalKMS == nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
+ return
+ }
+
+ apis, err := GlobalKMS.APIs(ctx)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ if res, err := json.Marshal(apis); err != nil {
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
+ } else {
+ writeSuccessResponseJSON(w, res)
+ }
+}
+
+type versionResponse struct {
+ Version string `json:"version"`
+}
+
+// KMSVersionHandler - GET /minio/kms/v1/version
+func (a kmsAPIHandlers) KMSVersionHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "KMSVersion")
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSVersionAction)
+ if objectAPI == nil {
+ return
+ }
+
+ if GlobalKMS == nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
+ return
+ }
+
+ version, err := GlobalKMS.Version(ctx)
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ res := &versionResponse{Version: version}
+ v, err := json.Marshal(res)
+ if err != nil {
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
+ return
+ }
+ writeSuccessResponseJSON(w, v)
+}
+
+// KMSCreateKeyHandler - POST /minio/kms/v1/key/create?key-id=
+func (a kmsAPIHandlers) KMSCreateKeyHandler(w http.ResponseWriter, r *http.Request) {
+ // If env variable MINIO_KMS_SECRET_KEY is populated, prevent creation of new keys
+ ctx := newContext(r, w, "KMSCreateKey")
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSCreateKeyAction)
+ if objectAPI == nil {
+ return
+ }
+
+ if GlobalKMS == nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
+ return
+ }
+
+ keyID := r.Form.Get("key-id")
+
+ // Ensure policy allows the user to create this key name
+ cred, owner, s3Err := validateAdminSignature(ctx, r, "")
+ if s3Err != ErrNone {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
+ return
+ }
+ if !checkKMSActionAllowed(r, owner, cred, policy.KMSCreateKeyAction, keyID) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+
+ if err := GlobalKMS.CreateKey(ctx, &kms.CreateKeyRequest{Name: keyID}); err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+ writeSuccessResponseHeadersOnly(w)
+}
+
+// KMSListKeysHandler - GET /minio/kms/v1/key/list?pattern=
+func (a kmsAPIHandlers) KMSListKeysHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "KMSListKeys")
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ // This only checks if the action (kms:ListKeys) is allowed, it does not check
+ // each key name against the policy's Resources. We check that below, once
+ // we have the list of key names from the KMS.
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSListKeysAction)
+ if objectAPI == nil {
+ return
+ }
+
+ if GlobalKMS == nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
+ return
+ }
+ allKeys, _, err := GlobalKMS.ListKeys(ctx, &kms.ListRequest{
+ Prefix: r.Form.Get("pattern"),
+ })
+ if err != nil {
+ writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
+ return
+ }
+
+ // Get the cred and owner for checking authz below.
+ cred, owner, s3Err := validateAdminSignature(ctx, r, "")
+ if s3Err != ErrNone {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
+ return
+ }
+
+ // Now we have all the key names, for each of them, check whether the policy grants permission for
+ // the user to list it. Filter in place to leave only allowed keys.
+ n := 0
+ for _, k := range allKeys {
+ if checkKMSActionAllowed(r, owner, cred, policy.KMSListKeysAction, k.Name) {
+ allKeys[n] = k
+ n++
+ }
+ }
+ allKeys = allKeys[:n]
+
+ if res, err := json.Marshal(allKeys); err != nil {
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
+ } else {
+ writeSuccessResponseJSON(w, res)
+ }
+}
+
+// KMSKeyStatusHandler - GET /minio/kms/v1/key/status?key-id=
+func (a kmsAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "KMSKeyStatus")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+ objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSKeyStatusAction)
+ if objectAPI == nil {
+ return
+ }
+
+ if GlobalKMS == nil {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
+ return
+ }
+
+ keyID := r.Form.Get("key-id")
+ if keyID == "" {
+ keyID = GlobalKMS.DefaultKey
+ }
+ response := madmin.KMSKeyStatus{
+ KeyID: keyID,
+ }
+
+ // Ensure policy allows the user to get this key's status
+ cred, owner, s3Err := validateAdminSignature(ctx, r, "")
+ if s3Err != ErrNone {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
+ return
+ }
+ if !checkKMSActionAllowed(r, owner, cred, policy.KMSKeyStatusAction, keyID) {
+ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+
+ kmsContext := kms.Context{"MinIO admin API": "KMSKeyStatusHandler"} // Context for a test key operation
+ // 1. Generate a new key using the KMS.
+ key, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{Name: keyID, AssociatedData: kmsContext})
+ if err != nil {
+ response.EncryptionErr = err.Error()
+ resp, err := json.Marshal(response)
+ if err != nil {
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
+ return
+ }
+ writeSuccessResponseJSON(w, resp)
+ return
+ }
+
+ // 2. Verify that we can indeed decrypt the (encrypted) key
+ decryptedKey, err := GlobalKMS.Decrypt(ctx, &kms.DecryptRequest{
+ Name: key.KeyID,
+ Ciphertext: key.Ciphertext,
+ AssociatedData: kmsContext,
+ })
+ if err != nil {
+ response.DecryptionErr = err.Error()
+ resp, err := json.Marshal(response)
+ if err != nil {
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
+ return
+ }
+ writeSuccessResponseJSON(w, resp)
+ return
+ }
+
+ // 3. Compare generated key with decrypted key
+ if subtle.ConstantTimeCompare(key.Plaintext, decryptedKey) != 1 {
+ response.DecryptionErr = "The generated and the decrypted data key do not match"
+ resp, err := json.Marshal(response)
+ if err != nil {
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
+ return
+ }
+ writeSuccessResponseJSON(w, resp)
+ return
+ }
+
+ resp, err := json.Marshal(response)
+ if err != nil {
+ writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
+ return
+ }
+ writeSuccessResponseJSON(w, resp)
+}
+
+// checkKMSActionAllowed checks for authorization for a specific action on a resource.
+func checkKMSActionAllowed(r *http.Request, owner bool, cred auth.Credentials, action policy.KMSAction, resource string) bool {
+ return globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.Action(action),
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ Claims: cred.Claims,
+ BucketName: resource, // overloading BucketName as that's what the policy engine uses to assemble a Resource.
+ })
+}
diff --git a/cmd/kms-handlers_test.go b/cmd/kms-handlers_test.go
new file mode 100644
index 00000000..4eccab4c
--- /dev/null
+++ b/cmd/kms-handlers_test.go
@@ -0,0 +1,847 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/kms"
+ "github.com/minio/pkg/v3/policy"
+)
+
+const (
+ // KMS API paths
+ // For example: /minio/kms/v1/key/list?pattern=*
+ kmsURL = kmsPathPrefix + kmsAPIVersionPrefix
+ kmsStatusPath = kmsURL + "/status"
+ kmsMetricsPath = kmsURL + "/metrics"
+ kmsAPIsPath = kmsURL + "/apis"
+ kmsVersionPath = kmsURL + "/version"
+ kmsKeyCreatePath = kmsURL + "/key/create"
+ kmsKeyListPath = kmsURL + "/key/list"
+ kmsKeyStatusPath = kmsURL + "/key/status"
+
+ // Admin API paths
+ // For example: /minio/admin/v3/kms/status
+ adminURL = adminPathPrefix + adminAPIVersionPrefix
+ kmsAdminStatusPath = adminURL + "/kms/status"
+ kmsAdminKeyStatusPath = adminURL + "/kms/key/status"
+ kmsAdminKeyCreate = adminURL + "/kms/key/create"
+)
+
+const (
+ userAccessKey = "miniofakeuseraccesskey"
+ userSecretKey = "miniofakeusersecret"
+)
+
+type kmsTestCase struct {
+ name string
+ method string
+ path string
+ query map[string]string
+
+ // User credentials and policy for request
+ policy string
+ asRoot bool
+
+ // Wanted in response.
+ wantStatusCode int
+ wantKeyNames []string
+ wantResp []string
+}
+
+func TestKMSHandlersCreateKey(t *testing.T) {
+ adminTestBed, tearDown := setupKMSTest(t, true)
+ defer tearDown()
+
+ tests := []kmsTestCase{
+ // Create key test
+ {
+ name: "create key as user with no policy want forbidden",
+ method: http.MethodPost,
+ path: kmsKeyCreatePath,
+ query: map[string]string{"key-id": "new-test-key"},
+ asRoot: false,
+
+ wantStatusCode: http.StatusForbidden,
+ wantResp: []string{"AccessDenied"},
+ },
+ {
+ name: "create key as user with no resources specified want success",
+ method: http.MethodPost,
+ path: kmsKeyCreatePath,
+ query: map[string]string{"key-id": "new-test-key"},
+ asRoot: false,
+
+ policy: `{"Effect": "Allow",
+ "Action": ["kms:CreateKey"] }`,
+
+ wantStatusCode: http.StatusOK,
+ },
+ {
+ name: "create key as user set policy to allow want success",
+ method: http.MethodPost,
+ path: kmsKeyCreatePath,
+ query: map[string]string{"key-id": "second-new-test-key"},
+ asRoot: false,
+
+ policy: `{"Effect": "Allow",
+ "Action": ["kms:CreateKey"],
+ "Resource": ["arn:minio:kms:::second-new-test-*"] }`,
+
+ wantStatusCode: http.StatusOK,
+ },
+ {
+ name: "create key as user set policy to non matching resource want forbidden",
+ method: http.MethodPost,
+ path: kmsKeyCreatePath,
+ query: map[string]string{"key-id": "third-new-test-key"},
+ asRoot: false,
+
+ policy: `{"Effect": "Allow",
+ "Action": ["kms:CreateKey"],
+ "Resource": ["arn:minio:kms:::non-matching-key-name"] }`,
+
+ wantStatusCode: http.StatusForbidden,
+ wantResp: []string{"AccessDenied"},
+ },
+ }
+ for testNum, test := range tests {
+ t.Run(fmt.Sprintf("%d %s", testNum+1, test.name), func(t *testing.T) {
+ execKMSTest(t, test, adminTestBed)
+ })
+ }
+}
+
+func TestKMSHandlersKeyStatus(t *testing.T) {
+ adminTestBed, tearDown := setupKMSTest(t, true)
+ defer tearDown()
+
+ tests := []kmsTestCase{
+ {
+ name: "create a first key root user",
+ method: http.MethodPost,
+ path: kmsKeyCreatePath,
+ query: map[string]string{"key-id": "abc-test-key"},
+ asRoot: true,
+
+ wantStatusCode: http.StatusOK,
+ },
+ {
+ name: "key status as root want success",
+ method: http.MethodGet,
+ path: kmsKeyStatusPath,
+ query: map[string]string{"key-id": "abc-test-key"},
+ asRoot: true,
+
+ wantStatusCode: http.StatusOK,
+ wantResp: []string{"abc-test-key"},
+ },
+ {
+ name: "key status as user no policy want forbidden",
+ method: http.MethodGet,
+ path: kmsKeyStatusPath,
+ query: map[string]string{"key-id": "abc-test-key"},
+ asRoot: false,
+
+ wantStatusCode: http.StatusForbidden,
+ wantResp: []string{"AccessDenied"},
+ },
+ {
+ name: "key status as user legacy no resources specified want success",
+ method: http.MethodGet,
+ path: kmsKeyStatusPath,
+ query: map[string]string{"key-id": "abc-test-key"},
+ asRoot: false,
+
+ policy: `{"Effect": "Allow",
+ "Action": ["kms:KeyStatus"] }`,
+
+ wantStatusCode: http.StatusOK,
+ wantResp: []string{"abc-test-key"},
+ },
+ {
+ name: "key status as user set policy to allow only one key",
+ method: http.MethodGet,
+ path: kmsKeyStatusPath,
+ query: map[string]string{"key-id": "abc-test-key"},
+ asRoot: false,
+
+ policy: `{"Effect": "Allow",
+ "Action": ["kms:KeyStatus"],
+ "Resource": ["arn:minio:kms:::abc-test-*"] }`,
+
+ wantStatusCode: http.StatusOK,
+ wantResp: []string{"abc-test-key"},
+ },
+ {
+ name: "key status as user set policy to allow non-matching key",
+ method: http.MethodGet,
+ path: kmsKeyStatusPath,
+ query: map[string]string{"key-id": "abc-test-key"},
+ asRoot: false,
+
+ policy: `{"Effect": "Allow",
+ "Action": ["kms:KeyStatus"],
+ "Resource": ["arn:minio:kms:::xyz-test-key"] }`,
+
+ wantStatusCode: http.StatusForbidden,
+ wantResp: []string{"AccessDenied"},
+ },
+ }
+ for testNum, test := range tests {
+ t.Run(fmt.Sprintf("%d %s", testNum+1, test.name), func(t *testing.T) {
+ execKMSTest(t, test, adminTestBed)
+ })
+ }
+}
+
+func TestKMSHandlersAPIs(t *testing.T) {
+ adminTestBed, tearDown := setupKMSTest(t, true)
+ defer tearDown()
+
+ tests := []kmsTestCase{
+ // Version test
+ {
+ name: "version as root want success",
+ method: http.MethodGet,
+ path: kmsVersionPath,
+ asRoot: true,
+
+ wantStatusCode: http.StatusOK,
+ wantResp: []string{"version"},
+ },
+ {
+ name: "version as user with no policy want forbidden",
+ method: http.MethodGet,
+ path: kmsVersionPath,
+ asRoot: false,
+
+ wantStatusCode: http.StatusForbidden,
+ wantResp: []string{"AccessDenied"},
+ },
+ {
+ name: "version as user with policy ignores resource want success",
+ method: http.MethodGet,
+ path: kmsVersionPath,
+ asRoot: false,
+
+ policy: `{"Effect": "Allow",
+ "Action": ["kms:Version"],
+ "Resource": ["arn:minio:kms:::does-not-matter-it-is-ignored"] }`,
+
+ wantStatusCode: http.StatusOK,
+ wantResp: []string{"version"},
+ },
+
+ // APIs test
+ {
+ name: "apis as root want success",
+ method: http.MethodGet,
+ path: kmsAPIsPath,
+ asRoot: true,
+
+ wantStatusCode: http.StatusOK,
+ wantResp: []string{"stub/path"},
+ },
+ {
+ name: "apis as user with no policy want forbidden",
+ method: http.MethodGet,
+ path: kmsAPIsPath,
+ asRoot: false,
+
+ wantStatusCode: http.StatusForbidden,
+ wantResp: []string{"AccessDenied"},
+ },
+ {
+ name: "apis as user with policy ignores resource want success",
+ method: http.MethodGet,
+ path: kmsAPIsPath,
+ asRoot: false,
+
+ policy: `{"Effect": "Allow",
+ "Action": ["kms:API"],
+ "Resource": ["arn:minio:kms:::does-not-matter-it-is-ignored"] }`,
+
+ wantStatusCode: http.StatusOK,
+ wantResp: []string{"stub/path"},
+ },
+
+ // Metrics test
+ {
+ name: "metrics as root want success",
+ method: http.MethodGet,
+ path: kmsMetricsPath,
+ asRoot: true,
+
+ wantStatusCode: http.StatusOK,
+ wantResp: []string{"kms"},
+ },
+ {
+ name: "metrics as user with no policy want forbidden",
+ method: http.MethodGet,
+ path: kmsMetricsPath,
+ asRoot: false,
+
+ wantStatusCode: http.StatusForbidden,
+ wantResp: []string{"AccessDenied"},
+ },
+ {
+ name: "metrics as user with policy ignores resource want success",
+ method: http.MethodGet,
+ path: kmsMetricsPath,
+ asRoot: false,
+
+ policy: `{"Effect": "Allow",
+ "Action": ["kms:Metrics"],
+ "Resource": ["arn:minio:kms:::does-not-matter-it-is-ignored"] }`,
+
+ wantStatusCode: http.StatusOK,
+ wantResp: []string{"kms"},
+ },
+
+ // Status tests
+ {
+ name: "status as root want success",
+ method: http.MethodGet,
+ path: kmsStatusPath,
+ asRoot: true,
+
+ wantStatusCode: http.StatusOK,
+ wantResp: []string{"MinIO builtin"},
+ },
+ {
+ name: "status as user with no policy want forbidden",
+ method: http.MethodGet,
+ path: kmsStatusPath,
+ asRoot: false,
+
+ wantStatusCode: http.StatusForbidden,
+ wantResp: []string{"AccessDenied"},
+ },
+ {
+ name: "status as user with policy ignores resource want success",
+ method: http.MethodGet,
+ path: kmsStatusPath,
+ asRoot: false,
+
+ policy: `{"Effect": "Allow",
+ "Action": ["kms:Status"],
+ "Resource": ["arn:minio:kms:::does-not-matter-it-is-ignored"]}`,
+
+ wantStatusCode: http.StatusOK,
+ wantResp: []string{"MinIO builtin"},
+ },
+ }
+ for testNum, test := range tests {
+ t.Run(fmt.Sprintf("%d %s", testNum+1, test.name), func(t *testing.T) {
+ execKMSTest(t, test, adminTestBed)
+ })
+ }
+}
+
+func TestKMSHandlersListKeys(t *testing.T) {
+ adminTestBed, tearDown := setupKMSTest(t, true)
+ defer tearDown()
+
+ tests := []kmsTestCase{
+ {
+ name: "create a first key root user",
+ method: http.MethodPost,
+ path: kmsKeyCreatePath,
+ query: map[string]string{"key-id": "abc-test-key"},
+ asRoot: true,
+
+ wantStatusCode: http.StatusOK,
+ },
+ {
+ name: "create a second key root user",
+ method: http.MethodPost,
+ path: kmsKeyCreatePath,
+ query: map[string]string{"key-id": "xyz-test-key"},
+ asRoot: true,
+
+ wantStatusCode: http.StatusOK,
+ },
+
+ // List keys tests
+ {
+ name: "list keys as root want all to be returned",
+ method: http.MethodGet,
+ path: kmsKeyListPath,
+ query: map[string]string{"pattern": "*"},
+ asRoot: true,
+
+ wantStatusCode: http.StatusOK,
+ wantKeyNames: []string{"default-test-key", "abc-test-key", "xyz-test-key"},
+ },
+ {
+ name: "list keys as user with no policy want forbidden",
+ method: http.MethodGet,
+ path: kmsKeyListPath,
+ query: map[string]string{"pattern": "*"},
+ asRoot: false,
+
+ wantStatusCode: http.StatusForbidden,
+ wantResp: []string{"AccessDenied"},
+ },
+ {
+ name: "list keys as user with no resources specified want success",
+ method: http.MethodGet,
+ path: kmsKeyListPath,
+ query: map[string]string{"pattern": "*"},
+ asRoot: false,
+
+ policy: `{"Effect": "Allow",
+ "Action": ["kms:ListKeys"]
+ }`,
+
+ wantStatusCode: http.StatusOK,
+ wantKeyNames: []string{"default-test-key", "abc-test-key", "xyz-test-key"},
+ },
+ {
+ name: "list keys as user set policy resource to allow only one key",
+ method: http.MethodGet,
+ path: kmsKeyListPath,
+ query: map[string]string{"pattern": "*"},
+ asRoot: false,
+
+ policy: `{"Effect": "Allow",
+ "Action": ["kms:ListKeys"],
+ "Resource": ["arn:minio:kms:::abc*"]}`,
+
+ wantStatusCode: http.StatusOK,
+ wantKeyNames: []string{"abc-test-key"},
+ },
+ {
+ name: "list keys as user set policy to allow only one key, use pattern that includes correct key",
+ method: http.MethodGet,
+ path: kmsKeyListPath,
+ query: map[string]string{"pattern": "abc*"},
+
+ policy: `{"Effect": "Allow",
+ "Action": ["kms:ListKeys"],
+ "Resource": ["arn:minio:kms:::abc*"]}`,
+
+ wantStatusCode: http.StatusOK,
+ wantKeyNames: []string{"abc-test-key"},
+ },
+ {
+ name: "list keys as user set policy to allow only one key, use pattern that excludes correct key",
+ method: http.MethodGet,
+ path: kmsKeyListPath,
+ query: map[string]string{"pattern": "xyz*"},
+ asRoot: false,
+
+ policy: `{"Effect": "Allow",
+ "Action": ["kms:ListKeys"],
+ "Resource": ["arn:minio:kms:::abc*"]}`,
+
+ wantStatusCode: http.StatusOK,
+ wantKeyNames: []string{},
+ },
+ {
+ name: "list keys as user set policy that has no matching key resources",
+ method: http.MethodGet,
+ path: kmsKeyListPath,
+ query: map[string]string{"pattern": "*"},
+ asRoot: false,
+
+ policy: `{"Effect": "Allow",
+ "Action": ["kms:ListKeys"],
+ "Resource": ["arn:minio:kms:::nonematch*"]}`,
+
+ wantStatusCode: http.StatusOK,
+ wantKeyNames: []string{},
+ },
+ {
+ name: "list keys as user set policy that allows listing but denies specific keys",
+ method: http.MethodGet,
+ path: kmsKeyListPath,
+ query: map[string]string{"pattern": "*"},
+ asRoot: false,
+
+ // It looks like this should allow listing any key that isn't "default-test-key", however
+ // the policy engine matches all Deny statements first, without regard to Resources (for KMS).
+ // This is for backwards compatibility where historically KMS statements ignored Resources.
+ policy: `{
+ "Effect": "Allow",
+ "Action": ["kms:ListKeys"]
+ },{
+ "Effect": "Deny",
+ "Action": ["kms:ListKeys"],
+ "Resource": ["arn:minio:kms:::default-test-key"]
+ }`,
+
+ wantStatusCode: http.StatusForbidden,
+ wantResp: []string{"AccessDenied"},
+ },
+ }
+
+ for testNum, test := range tests {
+ t.Run(fmt.Sprintf("%d %s", testNum+1, test.name), func(t *testing.T) {
+ execKMSTest(t, test, adminTestBed)
+ })
+ }
+}
+
+func TestKMSHandlerAdminAPI(t *testing.T) {
+ adminTestBed, tearDown := setupKMSTest(t, true)
+ defer tearDown()
+
+ tests := []kmsTestCase{
+ // Create key tests
+ {
+ name: "create a key root user",
+ method: http.MethodPost,
+ path: kmsAdminKeyCreate,
+ query: map[string]string{"key-id": "abc-test-key"},
+ asRoot: true,
+
+ wantStatusCode: http.StatusOK,
+ },
+ {
+ name: "create key as user with no policy want forbidden",
+ method: http.MethodPost,
+ path: kmsAdminKeyCreate,
+ query: map[string]string{"key-id": "new-test-key"},
+ asRoot: false,
+
+ wantStatusCode: http.StatusForbidden,
+ wantResp: []string{"AccessDenied"},
+ },
+ {
+ name: "create key as user with no resources specified want success",
+ method: http.MethodPost,
+ path: kmsAdminKeyCreate,
+ query: map[string]string{"key-id": "new-test-key"},
+ asRoot: false,
+
+ policy: `{"Effect": "Allow",
+ "Action": ["admin:KMSCreateKey"] }`,
+
+ wantStatusCode: http.StatusOK,
+ },
+ {
+ name: "create key as user set policy to non matching resource want success",
+ method: http.MethodPost,
+ path: kmsAdminKeyCreate,
+ query: map[string]string{"key-id": "third-new-test-key"},
+ asRoot: false,
+
+ // Admin actions ignore Resources
+ policy: `{"Effect": "Allow",
+ "Action": ["admin:KMSCreateKey"],
+ "Resource": ["arn:minio:kms:::this-is-disregarded"] }`,
+
+ wantStatusCode: http.StatusOK,
+ },
+
+ // Status tests
+ {
+ name: "status as root want success",
+ method: http.MethodPost,
+ path: kmsAdminStatusPath,
+ asRoot: true,
+
+ wantStatusCode: http.StatusOK,
+ wantResp: []string{"MinIO builtin"},
+ },
+ {
+ name: "status as user with no policy want forbidden",
+ method: http.MethodPost,
+ path: kmsAdminStatusPath,
+ asRoot: false,
+
+ wantStatusCode: http.StatusForbidden,
+ wantResp: []string{"AccessDenied"},
+ },
+ {
+ name: "status as user with policy ignores resource want success",
+ method: http.MethodPost,
+ path: kmsAdminStatusPath,
+ asRoot: false,
+
+ policy: `{"Effect": "Allow",
+ "Action": ["admin:KMSKeyStatus"],
+ "Resource": ["arn:minio:kms:::does-not-matter-it-is-ignored"] }`,
+
+ wantStatusCode: http.StatusOK,
+ wantResp: []string{"MinIO builtin"},
+ },
+
+ // Key status tests
+ {
+ name: "key status as root want success",
+ method: http.MethodGet,
+ path: kmsAdminKeyStatusPath,
+ asRoot: true,
+
+ wantStatusCode: http.StatusOK,
+ wantResp: []string{"key-id"},
+ },
+ {
+ name: "key status as user with no policy want forbidden",
+ method: http.MethodGet,
+ path: kmsAdminKeyStatusPath,
+ asRoot: false,
+
+ wantStatusCode: http.StatusForbidden,
+ wantResp: []string{"AccessDenied"},
+ },
+ {
+ name: "key status as user with policy ignores resource want success",
+ method: http.MethodGet,
+ path: kmsAdminKeyStatusPath,
+ asRoot: false,
+
+ policy: `{"Effect": "Allow",
+ "Action": ["admin:KMSKeyStatus"],
+ "Resource": ["arn:minio:kms:::does-not-matter-it-is-ignored"] }`,
+
+ wantStatusCode: http.StatusOK,
+ wantResp: []string{"key-id"},
+ },
+ }
+
+ for testNum, test := range tests {
+ t.Run(fmt.Sprintf("%d %s", testNum+1, test.name), func(t *testing.T) {
+ execKMSTest(t, test, adminTestBed)
+ })
+ }
+}
+
+// execKMSTest runs a single test case for KMS handlers
+func execKMSTest(t *testing.T, test kmsTestCase, adminTestBed *adminErasureTestBed) {
+ var accessKey, secretKey string
+ if test.asRoot {
+ accessKey, secretKey = globalActiveCred.AccessKey, globalActiveCred.SecretKey
+ } else {
+ setupKMSUser(t, userAccessKey, userSecretKey, test.policy)
+ accessKey = userAccessKey
+ secretKey = userSecretKey
+ }
+
+ req := buildKMSRequest(t, test.method, test.path, accessKey, secretKey, test.query)
+ rec := httptest.NewRecorder()
+ adminTestBed.router.ServeHTTP(rec, req)
+
+ t.Logf("HTTP req: %s, resp code: %d, resp body: %s", req.URL.String(), rec.Code, rec.Body.String())
+
+ // Check status code
+ if rec.Code != test.wantStatusCode {
+ t.Errorf("want status code %d, got %d", test.wantStatusCode, rec.Code)
+ }
+
+ // Check returned key list is correct
+ if test.wantKeyNames != nil {
+ keys := []madmin.KMSKeyInfo{}
+ err := json.Unmarshal(rec.Body.Bytes(), &keys)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(keys) != len(test.wantKeyNames) {
+ t.Fatalf("want %d keys, got %d", len(test.wantKeyNames), len(keys))
+ }
+
+ for i, want := range keys {
+ if want.CreatedBy != kms.StubCreatedBy {
+ t.Fatalf("want key created by %s, got %s", kms.StubCreatedBy, want.CreatedBy)
+ }
+ if want.CreatedAt != kms.StubCreatedAt {
+ t.Fatalf("want key created at %s, got %s", kms.StubCreatedAt, want.CreatedAt)
+ }
+ if test.wantKeyNames[i] != want.Name {
+ t.Fatalf("want key name %s, got %s", test.wantKeyNames[i], want.Name)
+ }
+ }
+ }
+
+ // Check generic text in the response
+ if test.wantResp != nil {
+ for _, want := range test.wantResp {
+ if !strings.Contains(rec.Body.String(), want) {
+ t.Fatalf("want response to contain %s, got %s", want, rec.Body.String())
+ }
+ }
+ }
+}
+
+// TestKMSHandlerNotConfiguredOrInvalidCreds tests KMS handlers for situations where KMS is not configured
+// or invalid credentials are provided.
+func TestKMSHandlerNotConfiguredOrInvalidCreds(t *testing.T) {
+ adminTestBed, tearDown := setupKMSTest(t, false)
+ defer tearDown()
+
+ tests := []struct {
+ name string
+ method string
+ path string
+ query map[string]string
+ }{
+ {
+ name: "GET status",
+ method: http.MethodGet,
+ path: kmsStatusPath,
+ },
+ {
+ name: "GET metrics",
+ method: http.MethodGet,
+ path: kmsMetricsPath,
+ },
+ {
+ name: "GET apis",
+ method: http.MethodGet,
+ path: kmsAPIsPath,
+ },
+ {
+ name: "GET version",
+ method: http.MethodGet,
+ path: kmsVersionPath,
+ },
+ {
+ name: "POST key create",
+ method: http.MethodPost,
+ path: kmsKeyCreatePath,
+ query: map[string]string{"key-id": "master-key-id"},
+ },
+ {
+ name: "GET key list",
+ method: http.MethodGet,
+ path: kmsKeyListPath,
+ query: map[string]string{"pattern": "*"},
+ },
+ {
+ name: "GET key status",
+ method: http.MethodGet,
+ path: kmsKeyStatusPath,
+ query: map[string]string{"key-id": "master-key-id"},
+ },
+ }
+
+ // Test when the GlobalKMS is not configured
+ for _, test := range tests {
+ t.Run(test.name+" not configured", func(t *testing.T) {
+ req := buildKMSRequest(t, test.method, test.path, "", "", test.query)
+ rec := httptest.NewRecorder()
+ adminTestBed.router.ServeHTTP(rec, req)
+ if rec.Code != http.StatusNotImplemented {
+ t.Errorf("want status code %d, got %d", http.StatusNotImplemented, rec.Code)
+ }
+ })
+ }
+
+ // Test when the GlobalKMS is configured but the credentials are invalid
+ GlobalKMS = kms.NewStub("default-test-key")
+ for _, test := range tests {
+ t.Run(test.name+" invalid credentials", func(t *testing.T) {
+ req := buildKMSRequest(t, test.method, test.path, userAccessKey, userSecretKey, test.query)
+ rec := httptest.NewRecorder()
+ adminTestBed.router.ServeHTTP(rec, req)
+ if rec.Code != http.StatusForbidden {
+ t.Errorf("want status code %d, got %d", http.StatusForbidden, rec.Code)
+ }
+ })
+ }
+}
+
+func setupKMSTest(t *testing.T, enableKMS bool) (*adminErasureTestBed, func()) {
+ adminTestBed, err := prepareAdminErasureTestBed(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+ registerKMSRouter(adminTestBed.router)
+
+ if enableKMS {
+ GlobalKMS = kms.NewStub("default-test-key")
+ }
+
+ tearDown := func() {
+ adminTestBed.TearDown()
+ GlobalKMS = nil
+ }
+ return adminTestBed, tearDown
+}
+
+func buildKMSRequest(t *testing.T, method, path, accessKey, secretKey string, query map[string]string) *http.Request {
+ if len(query) > 0 {
+ queryVal := url.Values{}
+ for k, v := range query {
+ queryVal.Add(k, v)
+ }
+ path = path + "?" + queryVal.Encode()
+ }
+
+ if accessKey == "" && secretKey == "" {
+ accessKey = globalActiveCred.AccessKey
+ secretKey = globalActiveCred.SecretKey
+ }
+
+ req, err := newTestSignedRequestV4(method, path, 0, nil, accessKey, secretKey, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return req
+}
+
+// setupKMSUser is a test helper that creates a new user with the provided access key and secret key
+// and applies the given policy to the user.
+func setupKMSUser(t *testing.T, accessKey, secretKey, p string) {
+ ctx := t.Context()
+ createUserParams := madmin.AddOrUpdateUserReq{
+ SecretKey: secretKey,
+ Status: madmin.AccountEnabled,
+ }
+ _, err := globalIAMSys.CreateUser(ctx, accessKey, createUserParams)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ testKMSPolicyName := "testKMSPolicy"
+ if p != "" {
+ p = `{"Version":"2012-10-17","Statement":[` + p + `]}`
+ policyData, err := policy.ParseConfig(strings.NewReader(p))
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, err = globalIAMSys.SetPolicy(ctx, testKMSPolicyName, *policyData)
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, err = globalIAMSys.PolicyDBSet(ctx, accessKey, testKMSPolicyName, regUser, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ } else {
+ err = globalIAMSys.DeletePolicy(ctx, testKMSPolicyName, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, err = globalIAMSys.PolicyDBSet(ctx, accessKey, "", regUser, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+}
diff --git a/cmd/kms-router.go b/cmd/kms-router.go
new file mode 100644
index 00000000..2428f4c1
--- /dev/null
+++ b/cmd/kms-router.go
@@ -0,0 +1,67 @@
+// Copyright (c) 2015-2022 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "net/http"
+
+ "github.com/klauspost/compress/gzhttp"
+ "github.com/klauspost/compress/gzip"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/mux"
+)
+
+const (
+ kmsPathPrefix = minioReservedBucketPath + "/kms"
+ kmsAPIVersion = "v1"
+ kmsAPIVersionPrefix = SlashSeparator + kmsAPIVersion
+)
+
+type kmsAPIHandlers struct{}
+
+// registerKMSRouter - Registers KMS APIs
+func registerKMSRouter(router *mux.Router) {
+ kmsAPI := kmsAPIHandlers{}
+ kmsRouter := router.PathPrefix(kmsPathPrefix).Subrouter()
+
+ KMSVersions := []string{
+ kmsAPIVersionPrefix,
+ }
+
+ gz, err := gzhttp.NewWrapper(gzhttp.MinSize(1000), gzhttp.CompressionLevel(gzip.BestSpeed))
+ if err != nil {
+ // Static params, so this is very unlikely.
+ logger.Fatal(err, "Unable to initialize server")
+ }
+
+ for _, version := range KMSVersions {
+ // KMS Status APIs
+ kmsRouter.Methods(http.MethodGet).Path(version + "/status").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSStatusHandler)))
+ kmsRouter.Methods(http.MethodGet).Path(version + "/metrics").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSMetricsHandler)))
+ kmsRouter.Methods(http.MethodGet).Path(version + "/apis").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSAPIsHandler)))
+ kmsRouter.Methods(http.MethodGet).Path(version + "/version").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSVersionHandler)))
+ // KMS Key APIs
+ kmsRouter.Methods(http.MethodPost).Path(version+"/key/create").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSCreateKeyHandler))).Queries("key-id", "{key-id:.*}")
+ kmsRouter.Methods(http.MethodGet).Path(version+"/key/list").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSListKeysHandler))).Queries("pattern", "{pattern:.*}")
+ kmsRouter.Methods(http.MethodGet).Path(version + "/key/status").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSKeyStatusHandler)))
+ }
+
+ // If none of the routes match add default error handler routes
+ kmsRouter.NotFoundHandler = httpTraceAll(errorResponseHandler)
+ kmsRouter.MethodNotAllowedHandler = httpTraceAll(methodNotAllowedHandler("KMS"))
+}
diff --git a/cmd/last-minute.go b/cmd/last-minute.go
new file mode 100644
index 00000000..aff5965e
--- /dev/null
+++ b/cmd/last-minute.go
@@ -0,0 +1,208 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+//go:generate msgp -file=$GOFILE -unexported
+
+package cmd
+
+import (
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+)
+
+const (
+ sizeLessThan1KiB = iota
+ sizeLessThan1MiB
+ sizeLessThan10MiB
+ sizeLessThan100MiB
+ sizeLessThan1GiB
+ sizeGreaterThan1GiB
+ // Add new entries here
+
+ sizeLastElemMarker
+)
+
+// sizeToTag converts a size to a tag.
+func sizeToTag(size int64) int {
+ switch {
+ case size < 1024:
+ return sizeLessThan1KiB
+ case size < 1024*1024:
+ return sizeLessThan1MiB
+ case size < 10*1024*1024:
+ return sizeLessThan10MiB
+ case size < 100*1024*1024:
+ return sizeLessThan100MiB
+ case size < 1024*1024*1024:
+ return sizeLessThan1GiB
+ default:
+ return sizeGreaterThan1GiB
+ }
+}
+
+func sizeTagToString(tag int) string {
+ switch tag {
+ case sizeLessThan1KiB:
+ return "LESS_THAN_1_KiB"
+ case sizeLessThan1MiB:
+ return "LESS_THAN_1_MiB"
+ case sizeLessThan10MiB:
+ return "LESS_THAN_10_MiB"
+ case sizeLessThan100MiB:
+ return "LESS_THAN_100_MiB"
+ case sizeLessThan1GiB:
+ return "LESS_THAN_1_GiB"
+ case sizeGreaterThan1GiB:
+ return "GREATER_THAN_1_GiB"
+ default:
+ return "unknown"
+ }
+}
+
+// AccElem holds information for calculating an average value
+type AccElem struct {
+ Total int64
+ Size int64
+ N int64
+}
+
+// Add a duration to a single element.
+func (a *AccElem) add(dur time.Duration) {
+ if dur < 0 {
+ dur = 0
+ }
+ a.Total += int64(dur)
+ a.N++
+}
+
+// Merge b into a.
+func (a *AccElem) merge(b AccElem) {
+ a.N += b.N
+ a.Total += b.Total
+ a.Size += b.Size
+}
+
+// Avg returns average time spent.
+func (a AccElem) avg() time.Duration {
+ if a.N >= 1 && a.Total > 0 {
+ return time.Duration(a.Total / a.N)
+ }
+ return 0
+}
+
+// asTimedAction returns the element as a madmin.TimedAction.
+func (a AccElem) asTimedAction() madmin.TimedAction {
+ return madmin.TimedAction{AccTime: uint64(a.Total), Count: uint64(a.N), Bytes: uint64(a.Size)}
+}
+
+// lastMinuteLatency keeps track of last minute latency.
+type lastMinuteLatency struct {
+ Totals [60]AccElem
+ LastSec int64
+}
+
+// Merge data of two lastMinuteLatency structure
+func (l lastMinuteLatency) merge(o lastMinuteLatency) (merged lastMinuteLatency) {
+ if l.LastSec > o.LastSec {
+ o.forwardTo(l.LastSec)
+ merged.LastSec = l.LastSec
+ } else {
+ l.forwardTo(o.LastSec)
+ merged.LastSec = o.LastSec
+ }
+
+ for i := range merged.Totals {
+ merged.Totals[i] = AccElem{
+ Total: l.Totals[i].Total + o.Totals[i].Total,
+ N: l.Totals[i].N + o.Totals[i].N,
+ Size: l.Totals[i].Size + o.Totals[i].Size,
+ }
+ }
+ return merged
+}
+
+// Add a new duration data
+func (l *lastMinuteLatency) add(t time.Duration) {
+ sec := time.Now().Unix()
+ l.forwardTo(sec)
+ winIdx := sec % 60
+ l.Totals[winIdx].add(t)
+ l.LastSec = sec
+}
+
+// Add a new duration data
+func (l *lastMinuteLatency) addAll(sec int64, a AccElem) {
+ l.forwardTo(sec)
+ winIdx := sec % 60
+ l.Totals[winIdx].merge(a)
+ l.LastSec = sec
+}
+
+// Merge all recorded latencies of last minute into one
+func (l *lastMinuteLatency) getTotal() AccElem {
+ var res AccElem
+ sec := time.Now().Unix()
+ l.forwardTo(sec)
+ for _, elem := range l.Totals[:] {
+ res.merge(elem)
+ }
+ return res
+}
+
+// forwardTo time t, clearing any entries in between.
+func (l *lastMinuteLatency) forwardTo(t int64) {
+ if l.LastSec >= t {
+ return
+ }
+ if t-l.LastSec >= 60 {
+ l.Totals = [60]AccElem{}
+ return
+ }
+ for l.LastSec != t {
+ // Clear next element.
+ idx := (l.LastSec + 1) % 60
+ l.Totals[idx] = AccElem{}
+ l.LastSec++
+ }
+}
+
+// LastMinuteHistogram keeps track of last minute sizes added.
+type LastMinuteHistogram [sizeLastElemMarker]lastMinuteLatency
+
+// Merge safely merges two LastMinuteHistogram structures into one
+func (l LastMinuteHistogram) Merge(o LastMinuteHistogram) (merged LastMinuteHistogram) {
+ for i := range l {
+ merged[i] = l[i].merge(o[i])
+ }
+ return merged
+}
+
+// Add latency t from object with the specified size.
+func (l *LastMinuteHistogram) Add(size int64, t time.Duration) {
+ l[sizeToTag(size)].add(t)
+}
+
+// GetAvgData will return the average for each bucket from the last time minute.
+// The number of objects is also included.
+func (l *LastMinuteHistogram) GetAvgData() [sizeLastElemMarker]AccElem {
+ var res [sizeLastElemMarker]AccElem
+ for i, elem := range l[:] {
+ res[i] = elem.getTotal()
+ }
+ return res
+}
diff --git a/cmd/last-minute_gen.go b/cmd/last-minute_gen.go
new file mode 100644
index 00000000..4e0d6661
--- /dev/null
+++ b/cmd/last-minute_gen.go
@@ -0,0 +1,728 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "github.com/tinylib/msgp/msgp"
+)
+
+// DecodeMsg implements msgp.Decodable
+func (z *AccElem) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Total":
+ z.Total, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "Total")
+ return
+ }
+ case "Size":
+ z.Size, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ case "N":
+ z.N, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "N")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z AccElem) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 3
+ // write "Total"
+ err = en.Append(0x83, 0xa5, 0x54, 0x6f, 0x74, 0x61, 0x6c)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.Total)
+ if err != nil {
+ err = msgp.WrapError(err, "Total")
+ return
+ }
+ // write "Size"
+ err = en.Append(0xa4, 0x53, 0x69, 0x7a, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.Size)
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ // write "N"
+ err = en.Append(0xa1, 0x4e)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.N)
+ if err != nil {
+ err = msgp.WrapError(err, "N")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z AccElem) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 3
+ // string "Total"
+ o = append(o, 0x83, 0xa5, 0x54, 0x6f, 0x74, 0x61, 0x6c)
+ o = msgp.AppendInt64(o, z.Total)
+ // string "Size"
+ o = append(o, 0xa4, 0x53, 0x69, 0x7a, 0x65)
+ o = msgp.AppendInt64(o, z.Size)
+ // string "N"
+ o = append(o, 0xa1, 0x4e)
+ o = msgp.AppendInt64(o, z.N)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *AccElem) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Total":
+ z.Total, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Total")
+ return
+ }
+ case "Size":
+ z.Size, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ case "N":
+ z.N, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "N")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z AccElem) Msgsize() (s int) {
+ s = 1 + 6 + msgp.Int64Size + 5 + msgp.Int64Size + 2 + msgp.Int64Size
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *LastMinuteHistogram) DecodeMsg(dc *msgp.Reader) (err error) {
+ var zb0001 uint32
+ zb0001, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0001 != uint32(sizeLastElemMarker) {
+ err = msgp.ArrayError{Wanted: uint32(sizeLastElemMarker), Got: zb0001}
+ return
+ }
+ for za0001 := range z {
+ var field []byte
+ _ = field
+ var zb0002 uint32
+ zb0002, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, za0001)
+ return
+ }
+ for zb0002 > 0 {
+ zb0002--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err, za0001)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Totals":
+ var zb0003 uint32
+ zb0003, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, za0001, "Totals")
+ return
+ }
+ if zb0003 != uint32(60) {
+ err = msgp.ArrayError{Wanted: uint32(60), Got: zb0003}
+ return
+ }
+ for za0002 := range z[za0001].Totals {
+ var zb0004 uint32
+ zb0004, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, za0001, "Totals", za0002)
+ return
+ }
+ for zb0004 > 0 {
+ zb0004--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err, za0001, "Totals", za0002)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Total":
+ z[za0001].Totals[za0002].Total, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, za0001, "Totals", za0002, "Total")
+ return
+ }
+ case "Size":
+ z[za0001].Totals[za0002].Size, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, za0001, "Totals", za0002, "Size")
+ return
+ }
+ case "N":
+ z[za0001].Totals[za0002].N, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, za0001, "Totals", za0002, "N")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err, za0001, "Totals", za0002)
+ return
+ }
+ }
+ }
+ }
+ case "LastSec":
+ z[za0001].LastSec, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, za0001, "LastSec")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err, za0001)
+ return
+ }
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *LastMinuteHistogram) EncodeMsg(en *msgp.Writer) (err error) {
+ err = en.WriteArrayHeader(uint32(sizeLastElemMarker))
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for za0001 := range z {
+ // map header, size 2
+ // write "Totals"
+ err = en.Append(0x82, 0xa6, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(60))
+ if err != nil {
+ err = msgp.WrapError(err, za0001, "Totals")
+ return
+ }
+ for za0002 := range z[za0001].Totals {
+ // map header, size 3
+ // write "Total"
+ err = en.Append(0x83, 0xa5, 0x54, 0x6f, 0x74, 0x61, 0x6c)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z[za0001].Totals[za0002].Total)
+ if err != nil {
+ err = msgp.WrapError(err, za0001, "Totals", za0002, "Total")
+ return
+ }
+ // write "Size"
+ err = en.Append(0xa4, 0x53, 0x69, 0x7a, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z[za0001].Totals[za0002].Size)
+ if err != nil {
+ err = msgp.WrapError(err, za0001, "Totals", za0002, "Size")
+ return
+ }
+ // write "N"
+ err = en.Append(0xa1, 0x4e)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z[za0001].Totals[za0002].N)
+ if err != nil {
+ err = msgp.WrapError(err, za0001, "Totals", za0002, "N")
+ return
+ }
+ }
+ // write "LastSec"
+ err = en.Append(0xa7, 0x4c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x63)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z[za0001].LastSec)
+ if err != nil {
+ err = msgp.WrapError(err, za0001, "LastSec")
+ return
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *LastMinuteHistogram) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ o = msgp.AppendArrayHeader(o, uint32(sizeLastElemMarker))
+ for za0001 := range z {
+ // map header, size 2
+ // string "Totals"
+ o = append(o, 0x82, 0xa6, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(60))
+ for za0002 := range z[za0001].Totals {
+ // map header, size 3
+ // string "Total"
+ o = append(o, 0x83, 0xa5, 0x54, 0x6f, 0x74, 0x61, 0x6c)
+ o = msgp.AppendInt64(o, z[za0001].Totals[za0002].Total)
+ // string "Size"
+ o = append(o, 0xa4, 0x53, 0x69, 0x7a, 0x65)
+ o = msgp.AppendInt64(o, z[za0001].Totals[za0002].Size)
+ // string "N"
+ o = append(o, 0xa1, 0x4e)
+ o = msgp.AppendInt64(o, z[za0001].Totals[za0002].N)
+ }
+ // string "LastSec"
+ o = append(o, 0xa7, 0x4c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x63)
+ o = msgp.AppendInt64(o, z[za0001].LastSec)
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *LastMinuteHistogram) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if zb0001 != uint32(sizeLastElemMarker) {
+ err = msgp.ArrayError{Wanted: uint32(sizeLastElemMarker), Got: zb0001}
+ return
+ }
+ for za0001 := range z {
+ var field []byte
+ _ = field
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, za0001)
+ return
+ }
+ for zb0002 > 0 {
+ zb0002--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err, za0001)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Totals":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, za0001, "Totals")
+ return
+ }
+ if zb0003 != uint32(60) {
+ err = msgp.ArrayError{Wanted: uint32(60), Got: zb0003}
+ return
+ }
+ for za0002 := range z[za0001].Totals {
+ var zb0004 uint32
+ zb0004, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, za0001, "Totals", za0002)
+ return
+ }
+ for zb0004 > 0 {
+ zb0004--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err, za0001, "Totals", za0002)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Total":
+ z[za0001].Totals[za0002].Total, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, za0001, "Totals", za0002, "Total")
+ return
+ }
+ case "Size":
+ z[za0001].Totals[za0002].Size, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, za0001, "Totals", za0002, "Size")
+ return
+ }
+ case "N":
+ z[za0001].Totals[za0002].N, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, za0001, "Totals", za0002, "N")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err, za0001, "Totals", za0002)
+ return
+ }
+ }
+ }
+ }
+ case "LastSec":
+ z[za0001].LastSec, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, za0001, "LastSec")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err, za0001)
+ return
+ }
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *LastMinuteHistogram) Msgsize() (s int) {
+ s = msgp.ArrayHeaderSize + (sizeLastElemMarker * (16 + (60 * (14 + msgp.Int64Size + msgp.Int64Size + msgp.Int64Size)) + msgp.Int64Size))
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *lastMinuteLatency) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Totals":
+ var zb0002 uint32
+ zb0002, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Totals")
+ return
+ }
+ if zb0002 != uint32(60) {
+ err = msgp.ArrayError{Wanted: uint32(60), Got: zb0002}
+ return
+ }
+ for za0001 := range z.Totals {
+ var zb0003 uint32
+ zb0003, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err, "Totals", za0001)
+ return
+ }
+ for zb0003 > 0 {
+ zb0003--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err, "Totals", za0001)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Total":
+ z.Totals[za0001].Total, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "Totals", za0001, "Total")
+ return
+ }
+ case "Size":
+ z.Totals[za0001].Size, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "Totals", za0001, "Size")
+ return
+ }
+ case "N":
+ z.Totals[za0001].N, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "Totals", za0001, "N")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err, "Totals", za0001)
+ return
+ }
+ }
+ }
+ }
+ case "LastSec":
+ z.LastSec, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "LastSec")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *lastMinuteLatency) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 2
+ // write "Totals"
+ err = en.Append(0x82, 0xa6, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteArrayHeader(uint32(60))
+ if err != nil {
+ err = msgp.WrapError(err, "Totals")
+ return
+ }
+ for za0001 := range z.Totals {
+ // map header, size 3
+ // write "Total"
+ err = en.Append(0x83, 0xa5, 0x54, 0x6f, 0x74, 0x61, 0x6c)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.Totals[za0001].Total)
+ if err != nil {
+ err = msgp.WrapError(err, "Totals", za0001, "Total")
+ return
+ }
+ // write "Size"
+ err = en.Append(0xa4, 0x53, 0x69, 0x7a, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.Totals[za0001].Size)
+ if err != nil {
+ err = msgp.WrapError(err, "Totals", za0001, "Size")
+ return
+ }
+ // write "N"
+ err = en.Append(0xa1, 0x4e)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.Totals[za0001].N)
+ if err != nil {
+ err = msgp.WrapError(err, "Totals", za0001, "N")
+ return
+ }
+ }
+ // write "LastSec"
+ err = en.Append(0xa7, 0x4c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x63)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.LastSec)
+ if err != nil {
+ err = msgp.WrapError(err, "LastSec")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *lastMinuteLatency) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 2
+ // string "Totals"
+ o = append(o, 0x82, 0xa6, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(60))
+ for za0001 := range z.Totals {
+ // map header, size 3
+ // string "Total"
+ o = append(o, 0x83, 0xa5, 0x54, 0x6f, 0x74, 0x61, 0x6c)
+ o = msgp.AppendInt64(o, z.Totals[za0001].Total)
+ // string "Size"
+ o = append(o, 0xa4, 0x53, 0x69, 0x7a, 0x65)
+ o = msgp.AppendInt64(o, z.Totals[za0001].Size)
+ // string "N"
+ o = append(o, 0xa1, 0x4e)
+ o = msgp.AppendInt64(o, z.Totals[za0001].N)
+ }
+ // string "LastSec"
+ o = append(o, 0xa7, 0x4c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x63)
+ o = msgp.AppendInt64(o, z.LastSec)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *lastMinuteLatency) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Totals":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Totals")
+ return
+ }
+ if zb0002 != uint32(60) {
+ err = msgp.ArrayError{Wanted: uint32(60), Got: zb0002}
+ return
+ }
+ for za0001 := range z.Totals {
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Totals", za0001)
+ return
+ }
+ for zb0003 > 0 {
+ zb0003--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Totals", za0001)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Total":
+ z.Totals[za0001].Total, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Totals", za0001, "Total")
+ return
+ }
+ case "Size":
+ z.Totals[za0001].Size, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Totals", za0001, "Size")
+ return
+ }
+ case "N":
+ z.Totals[za0001].N, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Totals", za0001, "N")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Totals", za0001)
+ return
+ }
+ }
+ }
+ }
+ case "LastSec":
+ z.LastSec, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "LastSec")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *lastMinuteLatency) Msgsize() (s int) {
+ s = 1 + 7 + msgp.ArrayHeaderSize + (60 * (14 + msgp.Int64Size + msgp.Int64Size + msgp.Int64Size)) + 8 + msgp.Int64Size
+ return
+}
diff --git a/cmd/last-minute_gen_test.go b/cmd/last-minute_gen_test.go
new file mode 100644
index 00000000..de684551
--- /dev/null
+++ b/cmd/last-minute_gen_test.go
@@ -0,0 +1,349 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+func TestMarshalUnmarshalAccElem(t *testing.T) {
+ v := AccElem{}
+ 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 BenchmarkMarshalMsgAccElem(b *testing.B) {
+ v := AccElem{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgAccElem(b *testing.B) {
+ v := AccElem{}
+ 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 BenchmarkUnmarshalAccElem(b *testing.B) {
+ v := AccElem{}
+ 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 TestEncodeDecodeAccElem(t *testing.T) {
+ v := AccElem{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeAccElem Msgsize() is inaccurate")
+ }
+
+ vn := AccElem{}
+ 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 BenchmarkEncodeAccElem(b *testing.B) {
+ v := AccElem{}
+ 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 BenchmarkDecodeAccElem(b *testing.B) {
+ v := AccElem{}
+ 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 TestMarshalUnmarshalLastMinuteHistogram(t *testing.T) {
+ v := LastMinuteHistogram{}
+ 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 BenchmarkMarshalMsgLastMinuteHistogram(b *testing.B) {
+ v := LastMinuteHistogram{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgLastMinuteHistogram(b *testing.B) {
+ v := LastMinuteHistogram{}
+ 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 BenchmarkUnmarshalLastMinuteHistogram(b *testing.B) {
+ v := LastMinuteHistogram{}
+ 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 TestEncodeDecodeLastMinuteHistogram(t *testing.T) {
+ v := LastMinuteHistogram{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeLastMinuteHistogram Msgsize() is inaccurate")
+ }
+
+ vn := LastMinuteHistogram{}
+ 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 BenchmarkEncodeLastMinuteHistogram(b *testing.B) {
+ v := LastMinuteHistogram{}
+ 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 BenchmarkDecodeLastMinuteHistogram(b *testing.B) {
+ v := LastMinuteHistogram{}
+ 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 TestMarshalUnmarshallastMinuteLatency(t *testing.T) {
+ v := lastMinuteLatency{}
+ 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 BenchmarkMarshalMsglastMinuteLatency(b *testing.B) {
+ v := lastMinuteLatency{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsglastMinuteLatency(b *testing.B) {
+ v := lastMinuteLatency{}
+ 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 BenchmarkUnmarshallastMinuteLatency(b *testing.B) {
+ v := lastMinuteLatency{}
+ 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 TestEncodeDecodelastMinuteLatency(t *testing.T) {
+ v := lastMinuteLatency{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodelastMinuteLatency Msgsize() is inaccurate")
+ }
+
+ vn := lastMinuteLatency{}
+ 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 BenchmarkEncodelastMinuteLatency(b *testing.B) {
+ v := lastMinuteLatency{}
+ 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 BenchmarkDecodelastMinuteLatency(b *testing.B) {
+ v := lastMinuteLatency{}
+ 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)
+ }
+ }
+}
diff --git a/cmd/lceventsrc_string.go b/cmd/lceventsrc_string.go
new file mode 100644
index 00000000..dd121abb
--- /dev/null
+++ b/cmd/lceventsrc_string.go
@@ -0,0 +1,33 @@
+// Code generated by "stringer -type lcEventSrc -trimprefix lcEventSrc_ bucket-lifecycle-audit.go"; DO NOT EDIT.
+
+package cmd
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[lcEventSrc_None-0]
+ _ = x[lcEventSrc_Heal-1]
+ _ = x[lcEventSrc_Scanner-2]
+ _ = x[lcEventSrc_Decom-3]
+ _ = x[lcEventSrc_Rebal-4]
+ _ = x[lcEventSrc_s3HeadObject-5]
+ _ = x[lcEventSrc_s3GetObject-6]
+ _ = x[lcEventSrc_s3ListObjects-7]
+ _ = x[lcEventSrc_s3PutObject-8]
+ _ = x[lcEventSrc_s3CopyObject-9]
+ _ = x[lcEventSrc_s3CompleteMultipartUpload-10]
+}
+
+const _lcEventSrc_name = "NoneHealScannerDecomRebals3HeadObjects3GetObjects3ListObjectss3PutObjects3CopyObjects3CompleteMultipartUpload"
+
+var _lcEventSrc_index = [...]uint8{0, 4, 8, 15, 20, 25, 37, 48, 61, 72, 84, 109}
+
+func (i lcEventSrc) String() string {
+ if i >= lcEventSrc(len(_lcEventSrc_index)-1) {
+ return "lcEventSrc(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _lcEventSrc_name[_lcEventSrc_index[i]:_lcEventSrc_index[i+1]]
+}
diff --git a/cmd/leak-detect_test.go b/cmd/leak-detect_test.go
new file mode 100644
index 00000000..95b98c4e
--- /dev/null
+++ b/cmd/leak-detect_test.go
@@ -0,0 +1,163 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "runtime/debug"
+ "sort"
+ "strings"
+ "time"
+)
+
+const (
+ // deadline (in seconds) up to which the go routine leak detection has to be retried.
+ leakDetectDeadline = 5
+ // pause time (in milliseconds) between each snapshot at the end of the go routine leak detection.
+ leakDetectPauseTimeMs = 50
+)
+
+// LeakDetect - type with methods for go routine leak detection.
+type LeakDetect struct {
+ relevantRoutines map[string]bool
+}
+
+// NewLeakDetect - Initialize a LeakDetector with the snapshot of relevant Go routines.
+func NewLeakDetect() LeakDetect {
+ snapshot := LeakDetect{
+ relevantRoutines: make(map[string]bool),
+ }
+ for _, g := range pickRelevantGoroutines() {
+ snapshot.relevantRoutines[g] = true
+ }
+ return snapshot
+}
+
+// CompareCurrentSnapshot - Compares the initial relevant stack trace with the current one (during the time of invocation).
+func (initialSnapShot LeakDetect) CompareCurrentSnapshot() []string {
+ var stackDiff []string
+ for _, g := range pickRelevantGoroutines() {
+ // Identify the Go routines those were not present in the initial snapshot.
+ // In other words a stack diff.
+ if !initialSnapShot.relevantRoutines[g] {
+ stackDiff = append(stackDiff, g)
+ }
+ }
+ return stackDiff
+}
+
+// DetectLeak - Creates a snapshot of runtime stack and compares it with the initial stack snapshot.
+func (initialSnapShot LeakDetect) DetectLeak(t TestErrHandler) {
+ if t.Failed() {
+ return
+ }
+ // Loop, waiting for goroutines to shut down.
+ // Wait up to 5 seconds, but finish as quickly as possible.
+ deadline := UTCNow().Add(leakDetectDeadline * time.Second)
+ for {
+ // get sack snapshot of relevant go routines.
+ leaked := initialSnapShot.CompareCurrentSnapshot()
+ // current stack snapshot matches the initial one, no leaks, return.
+ if len(leaked) == 0 {
+ return
+ }
+ // wait a test again will deadline.
+ if UTCNow().Before(deadline) {
+ time.Sleep(leakDetectPauseTimeMs * time.Millisecond)
+ continue
+ }
+ // after the deadline time report all the difference in the latest snapshot compared with the initial one.
+ for _, g := range leaked {
+ t.Errorf("Leaked goroutine: %v", g)
+ }
+ return
+ }
+}
+
+// DetectTestLeak - snapshots the currently running goroutines and returns a
+// function to be run at the end of tests to see whether any
+// goroutines leaked.
+// Usage: `defer DetectTestLeak(t)()` in beginning line of benchmarks or unit tests.
+func DetectTestLeak(t TestErrHandler) func() {
+ initialStackSnapShot := NewLeakDetect()
+ return func() {
+ initialStackSnapShot.DetectLeak(t)
+ }
+}
+
+// list of functions to be ignored from the stack trace.
+// Leak detection is done when tests are run, should ignore the tests related functions,
+// and other runtime functions while identifying leaks.
+var ignoredStackFns = []string{
+ "",
+ // Below are the stacks ignored by the upstream leaktest code.
+ "testing.Main(",
+ "testing.tRunner(",
+ "testing.tRunner(",
+ "runtime.goexit",
+ "created by runtime.gc",
+ // ignore the snapshot function.
+ // since the snapshot is taken here the entry will have the current function too.
+ "pickRelevantGoroutines",
+ "runtime.MHeap_Scavenger",
+ "signal.signal_recv",
+ "sigterm.handler",
+ "runtime_mcall",
+ "goroutine in C code",
+}
+
+// Identify whether the stack trace entry is part of ignoredStackFn .
+func isIgnoredStackFn(stack string) (ok bool) {
+ ok = true
+ for _, stackFn := range ignoredStackFns {
+ if !strings.Contains(stack, stackFn) {
+ ok = false
+ continue
+ }
+ break
+ }
+ return ok
+}
+
+// pickRelevantGoroutines returns all goroutines we care about for the purpose
+// of leak checking. It excludes testing or runtime ones.
+func pickRelevantGoroutines() (gs []string) {
+ // get runtime stack buffer.
+ buf := debug.Stack()
+ // runtime stack of go routines will be listed with 2 blank spaces between each of them, so split on "\n\n" .
+ for g := range strings.SplitSeq(string(buf), "\n\n") {
+ // Again split on a new line, the first line of the second half contains the info about the go routine.
+ sl := strings.SplitN(g, "\n", 2)
+ if len(sl) != 2 {
+ continue
+ }
+ stack := strings.TrimSpace(sl[1])
+ // ignore the testing go routine.
+ // since the tests will be invoking the leaktest it would contain the test go routine.
+ if strings.HasPrefix(stack, "testing.RunTests") {
+ continue
+ }
+ // Ignore the following go routines.
+ // testing and run time go routines should be ignored, only the application generated go routines should be taken into account.
+ if isIgnoredStackFn(stack) {
+ continue
+ }
+ gs = append(gs, g)
+ }
+ sort.Strings(gs)
+ return gs
+}
diff --git a/cmd/listen-notification-handlers.go b/cmd/listen-notification-handlers.go
new file mode 100644
index 00000000..9f3210da
--- /dev/null
+++ b/cmd/listen-notification-handlers.go
@@ -0,0 +1,221 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+ "strconv"
+ "time"
+
+ "github.com/minio/minio/internal/event"
+ "github.com/minio/minio/internal/grid"
+ xhttp "github.com/minio/minio/internal/http"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/minio/internal/pubsub"
+ "github.com/minio/mux"
+ "github.com/minio/pkg/v3/policy"
+)
+
+func (api objectAPIHandlers) ListenNotificationHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "ListenNotification")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ // Validate if bucket exists.
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucketName := vars["bucket"]
+
+ if bucketName == "" {
+ if s3Error := checkRequestAuthType(ctx, r, policy.ListenNotificationAction, bucketName, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+ } else {
+ if s3Error := checkRequestAuthType(ctx, r, policy.ListenBucketNotificationAction, bucketName, ""); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+ }
+
+ values := r.Form
+
+ var prefix string
+ if len(values[peerRESTListenPrefix]) > 1 {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrFilterNamePrefix), r.URL)
+ return
+ }
+
+ if len(values[peerRESTListenPrefix]) == 1 {
+ if err := event.ValidateFilterRuleValue(values[peerRESTListenPrefix][0]); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ prefix = values[peerRESTListenPrefix][0]
+ }
+
+ var suffix string
+ if len(values[peerRESTListenSuffix]) > 1 {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrFilterNameSuffix), r.URL)
+ return
+ }
+
+ if len(values[peerRESTListenSuffix]) == 1 {
+ if err := event.ValidateFilterRuleValue(values[peerRESTListenSuffix][0]); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ suffix = values[peerRESTListenSuffix][0]
+ }
+
+ pattern := event.NewPattern(prefix, suffix)
+
+ var eventNames []event.Name
+ var mask pubsub.Mask
+ for _, s := range values[peerRESTListenEvents] {
+ eventName, err := event.ParseName(s)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ mask.MergeMaskable(eventName)
+ eventNames = append(eventNames, eventName)
+ }
+
+ if bucketName != "" {
+ if _, err := objAPI.GetBucketInfo(ctx, bucketName, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ }
+
+ rulesMap := event.NewRulesMap(eventNames, pattern, event.TargetID{ID: mustGetUUID()})
+
+ setEventStreamHeaders(w)
+
+ // Listen Publisher and peer-listen-client uses nonblocking send and hence does not wait for slow receivers.
+ // Use buffered channel to take care of burst sends or slow w.Write()
+ mergeCh := make(chan []byte, globalAPIConfig.getRequestsPoolCapacity()*len(globalEndpoints.Hostnames()))
+ localCh := make(chan event.Event, globalAPIConfig.getRequestsPoolCapacity())
+
+ // Convert local messages to JSON and send to mergeCh
+ go func() {
+ buf := bytes.NewBuffer(grid.GetByteBuffer()[:0])
+ enc := json.NewEncoder(buf)
+ tmpEvt := struct{ Records []event.Event }{[]event.Event{{}}}
+ for {
+ select {
+ case ev := <-localCh:
+ buf.Reset()
+ tmpEvt.Records[0] = ev
+ if err := enc.Encode(tmpEvt); err != nil {
+ bugLogIf(ctx, err, "event: Encode failed")
+ continue
+ }
+ mergeCh <- append(grid.GetByteBuffer()[:0], buf.Bytes()...)
+ case <-ctx.Done():
+ grid.PutByteBuffer(buf.Bytes())
+ return
+ }
+ }
+ }()
+ peers, _ := newPeerRestClients(globalEndpoints)
+ err := globalHTTPListen.Subscribe(mask, localCh, ctx.Done(), func(ev event.Event) bool {
+ if ev.S3.Bucket.Name != "" && bucketName != "" {
+ if ev.S3.Bucket.Name != bucketName {
+ return false
+ }
+ }
+ return rulesMap.MatchSimple(ev.EventName, ev.S3.Object.Key)
+ })
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ if bucketName != "" {
+ values.Set(peerRESTListenBucket, bucketName)
+ }
+ for _, peer := range peers {
+ if peer == nil {
+ continue
+ }
+ peer.Listen(ctx, mergeCh, values)
+ }
+
+ var (
+ emptyEventTicker <-chan time.Time
+ keepAliveTicker <-chan time.Time
+ )
+
+ if p := values.Get("ping"); p != "" {
+ pingInterval, err := strconv.Atoi(p)
+ if err != nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidQueryParams), r.URL)
+ return
+ }
+ if pingInterval < 1 {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidQueryParams), r.URL)
+ return
+ }
+ t := time.NewTicker(time.Duration(pingInterval) * time.Second)
+ defer t.Stop()
+ emptyEventTicker = t.C
+ } else {
+ // Deprecated Apr 2023
+ t := time.NewTicker(500 * time.Millisecond)
+ defer t.Stop()
+ keepAliveTicker = t.C
+ }
+
+ enc := json.NewEncoder(w)
+ for {
+ select {
+ case ev := <-mergeCh:
+ _, err := w.Write(ev)
+ if err != nil {
+ return
+ }
+ if len(mergeCh) == 0 {
+ // Flush if nothing is queued
+ xhttp.Flush(w)
+ }
+ grid.PutByteBuffer(ev)
+ case <-emptyEventTicker:
+ if err := enc.Encode(struct{ Records []event.Event }{}); err != nil {
+ return
+ }
+ xhttp.Flush(w)
+ case <-keepAliveTicker:
+ if _, err := w.Write([]byte(" ")); err != nil {
+ return
+ }
+ xhttp.Flush(w)
+ case <-ctx.Done():
+ return
+ }
+ }
+}
diff --git a/cmd/local-locker.go b/cmd/local-locker.go
new file mode 100644
index 00000000..3fd630b9
--- /dev/null
+++ b/cmd/local-locker.go
@@ -0,0 +1,452 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+//go:generate msgp -file=$GOFILE -unexported
+
+import (
+ "context"
+ "fmt"
+ "strconv"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/minio/minio/internal/dsync"
+)
+
+// Reject new lock requests immediately when this many are queued
+// for the local lock mutex.
+// We do not block unlocking or maintenance, but they add to the count.
+// The limit is set to allow for bursty behavior,
+// but prevent requests to overload the server completely.
+// Rejected clients are expected to retry.
+const lockMutexWaitLimit = 1000
+
+// lockRequesterInfo stores various info from the client for each lock that is requested.
+type lockRequesterInfo struct {
+ Name string // name of the resource lock was requested for
+ Writer bool // Bool whether write or read lock.
+ UID string // UID to uniquely identify request of client.
+ Timestamp int64 // Timestamp set at the time of initialization.
+ TimeLastRefresh int64 // Timestamp for last lock refresh.
+ Source string // Contains line, function and filename requesting the lock.
+ Group bool // indicates if it was a group lock.
+ Owner string // Owner represents the UUID of the owner who originally requested the lock.
+ Quorum int // Quorum represents the quorum required for this lock to be active.
+ idx int `msg:"-"` // index of the lock in the lockMap.
+}
+
+// isWriteLock returns whether the lock is a write or read lock.
+func isWriteLock(lri []lockRequesterInfo) bool {
+ return len(lri) == 1 && lri[0].Writer
+}
+
+// localLocker implements Dsync.NetLocker
+//
+//msgp:ignore localLocker
+type localLocker struct {
+ mutex sync.Mutex
+ waitMutex atomic.Int32
+ lockMap map[string][]lockRequesterInfo
+ lockUID map[string]string // UUID -> resource map.
+
+ // the following are updated on every cleanup defined in lockValidityDuration
+ readers atomic.Int32
+ writers atomic.Int32
+ lastCleanup atomic.Pointer[time.Time]
+ locksOverloaded atomic.Int64
+}
+
+// getMutex will lock the mutex.
+// Call the returned function to unlock.
+func (l *localLocker) getMutex() func() {
+ l.waitMutex.Add(1)
+ l.mutex.Lock()
+ l.waitMutex.Add(-1)
+ return l.mutex.Unlock
+}
+
+func (l *localLocker) String() string {
+ return globalEndpoints.Localhost()
+}
+
+func (l *localLocker) canTakeLock(resources ...string) bool {
+ for _, resource := range resources {
+ _, lockTaken := l.lockMap[resource]
+ if lockTaken {
+ return false
+ }
+ }
+ return true
+}
+
+func (l *localLocker) Lock(ctx context.Context, args dsync.LockArgs) (reply bool, err error) {
+ if len(args.Resources) > maxDeleteList {
+ return false, fmt.Errorf("internal error: localLocker.Lock called with more than %d resources", maxDeleteList)
+ }
+
+ // If we have too many waiting, reject this at once.
+ if l.waitMutex.Load() > lockMutexWaitLimit {
+ l.locksOverloaded.Add(1)
+ return false, nil
+ }
+ // Wait for mutex
+ defer l.getMutex()()
+ if ctx.Err() != nil {
+ return false, ctx.Err()
+ }
+ if !l.canTakeLock(args.Resources...) {
+ // Not all locks can be taken on resources,
+ // reject it completely.
+ return false, nil
+ }
+
+ // No locks held on the all resources, so claim write
+ // lock on all resources at once.
+ now := UTCNow()
+ for i, resource := range args.Resources {
+ l.lockMap[resource] = []lockRequesterInfo{
+ {
+ Name: resource,
+ Writer: true,
+ Source: args.Source,
+ Owner: args.Owner,
+ UID: args.UID,
+ Timestamp: now.UnixNano(),
+ TimeLastRefresh: now.UnixNano(),
+ Group: len(args.Resources) > 1,
+ Quorum: *args.Quorum,
+ idx: i,
+ },
+ }
+ l.lockUID[formatUUID(args.UID, i)] = resource
+ }
+ return true, nil
+}
+
+func formatUUID(s string, idx int) string {
+ return concat(s, strconv.Itoa(idx))
+}
+
+func (l *localLocker) Unlock(_ context.Context, args dsync.LockArgs) (reply bool, err error) {
+ if len(args.Resources) > maxDeleteList {
+ return false, fmt.Errorf("internal error: localLocker.Unlock called with more than %d resources", maxDeleteList)
+ }
+
+ defer l.getMutex()()
+
+ for _, resource := range args.Resources {
+ lri, ok := l.lockMap[resource]
+ if ok && !isWriteLock(lri) {
+ // Unless it is a write lock reject it.
+ err = fmt.Errorf("unlock attempted on a read locked entity: %s", resource)
+ continue
+ }
+ if ok {
+ reply = l.removeEntry(resource, args, &lri) || reply
+ }
+ }
+ return reply, err
+}
+
+// removeEntry based on the uid of the lock message, removes a single entry from the
+// lockRequesterInfo array or the whole array from the map (in case of a write lock
+// or last read lock)
+// UID and optionally owner must match for entries to be deleted.
+func (l *localLocker) removeEntry(name string, args dsync.LockArgs, lri *[]lockRequesterInfo) bool {
+ // Find correct entry to remove based on uid.
+ for index, entry := range *lri {
+ if entry.UID == args.UID && (args.Owner == "" || entry.Owner == args.Owner) {
+ if len(*lri) == 1 {
+ // Remove the write lock.
+ delete(l.lockMap, name)
+ } else {
+ // Remove the appropriate read lock.
+ *lri = append((*lri)[:index], (*lri)[index+1:]...)
+ l.lockMap[name] = *lri
+ }
+ delete(l.lockUID, formatUUID(args.UID, entry.idx))
+ return true
+ }
+ }
+
+ // None found return false, perhaps entry removed in previous run.
+ return false
+}
+
+func (l *localLocker) RLock(ctx context.Context, args dsync.LockArgs) (reply bool, err error) {
+ if len(args.Resources) != 1 {
+ return false, fmt.Errorf("internal error: localLocker.RLock called with more than one resource")
+ }
+ // If we have too many waiting, reject this at once.
+ if l.waitMutex.Load() > lockMutexWaitLimit {
+ l.locksOverloaded.Add(1)
+ return false, nil
+ }
+
+ // Wait for mutex
+ defer l.getMutex()()
+ if ctx.Err() != nil {
+ return false, ctx.Err()
+ }
+ resource := args.Resources[0]
+ now := UTCNow()
+ lrInfo := lockRequesterInfo{
+ Name: resource,
+ Writer: false,
+ Source: args.Source,
+ Owner: args.Owner,
+ UID: args.UID,
+ Timestamp: now.UnixNano(),
+ TimeLastRefresh: now.UnixNano(),
+ Quorum: *args.Quorum,
+ }
+ lri, ok := l.lockMap[resource]
+ if ok {
+ if reply = !isWriteLock(lri); reply {
+ // Unless there is a write lock
+ l.lockMap[resource] = append(l.lockMap[resource], lrInfo)
+ l.lockUID[formatUUID(args.UID, 0)] = resource
+ }
+ } else {
+ // No locks held on the given name, so claim (first) read lock
+ l.lockMap[resource] = []lockRequesterInfo{lrInfo}
+ l.lockUID[formatUUID(args.UID, 0)] = resource
+ reply = true
+ }
+ return reply, nil
+}
+
+func (l *localLocker) RUnlock(_ context.Context, args dsync.LockArgs) (reply bool, err error) {
+ if len(args.Resources) > 1 {
+ return false, fmt.Errorf("internal error: localLocker.RUnlock called with more than one resource")
+ }
+
+ defer l.getMutex()()
+ var lri []lockRequesterInfo
+
+ resource := args.Resources[0]
+ if lri, reply = l.lockMap[resource]; !reply {
+ // No lock is held on the given name
+ return true, nil
+ }
+ if isWriteLock(lri) {
+ // A write-lock is held, cannot release a read lock
+ return false, fmt.Errorf("RUnlock attempted on a write locked entity: %s", resource)
+ }
+ l.removeEntry(resource, args, &lri)
+ return reply, nil
+}
+
+type lockStats struct {
+ Total int
+ Writes int
+ Reads int
+ LockQueue int
+ LocksAbandoned int
+ LastCleanup *time.Time
+}
+
+func (l *localLocker) stats() lockStats {
+ return lockStats{
+ Total: len(l.lockMap),
+ Reads: int(l.readers.Load()),
+ Writes: int(l.writers.Load()),
+ LockQueue: int(l.waitMutex.Load()),
+ LastCleanup: l.lastCleanup.Load(),
+ }
+}
+
+type localLockMap map[string][]lockRequesterInfo
+
+func (l *localLocker) DupLockMap() localLockMap {
+ defer l.getMutex()()
+
+ lockCopy := make(map[string][]lockRequesterInfo, len(l.lockMap))
+ for k, v := range l.lockMap {
+ if len(v) == 0 {
+ delete(l.lockMap, k)
+ continue
+ }
+ lockCopy[k] = append(make([]lockRequesterInfo, 0, len(v)), v...)
+ }
+ return lockCopy
+}
+
+func (l *localLocker) Close() error {
+ return nil
+}
+
+// IsOnline - local locker is always online.
+func (l *localLocker) IsOnline() bool {
+ return true
+}
+
+// IsLocal - local locker returns true.
+func (l *localLocker) IsLocal() bool {
+ return true
+}
+
+func (l *localLocker) ForceUnlock(ctx context.Context, args dsync.LockArgs) (reply bool, err error) {
+ if ctx.Err() != nil {
+ return false, ctx.Err()
+ }
+
+ defer l.getMutex()()
+ if ctx.Err() != nil {
+ return false, ctx.Err()
+ }
+ if len(args.UID) == 0 {
+ for _, resource := range args.Resources {
+ lris, ok := l.lockMap[resource]
+ if !ok {
+ continue
+ }
+ // Collect uids, so we don't mutate while we delete
+ uids := make([]string, 0, len(lris))
+ for _, lri := range lris {
+ uids = append(uids, lri.UID)
+ }
+
+ // Delete collected uids:
+ for _, uid := range uids {
+ lris, ok := l.lockMap[resource]
+ if !ok {
+ // Just to be safe, delete uuids.
+ for idx := range maxDeleteList {
+ mapID := formatUUID(uid, idx)
+ if _, ok := l.lockUID[mapID]; !ok {
+ break
+ }
+ delete(l.lockUID, mapID)
+ }
+ continue
+ }
+ l.removeEntry(resource, dsync.LockArgs{UID: uid}, &lris)
+ }
+ }
+ return true, nil
+ }
+
+ idx := 0
+ for {
+ mapID := formatUUID(args.UID, idx)
+ resource, ok := l.lockUID[mapID]
+ if !ok {
+ return idx > 0, nil
+ }
+ lris, ok := l.lockMap[resource]
+ if !ok {
+ // Unexpected inconsistency, delete.
+ delete(l.lockUID, mapID)
+ idx++
+ continue
+ }
+ reply = true
+ l.removeEntry(resource, dsync.LockArgs{UID: args.UID}, &lris)
+ idx++
+ }
+}
+
+func (l *localLocker) Refresh(ctx context.Context, args dsync.LockArgs) (refreshed bool, err error) {
+ if ctx.Err() != nil {
+ return false, ctx.Err()
+ }
+
+ defer l.getMutex()()
+ if ctx.Err() != nil {
+ return false, ctx.Err()
+ }
+
+ // Check whether uid is still active.
+ resource, ok := l.lockUID[formatUUID(args.UID, 0)]
+ if !ok {
+ return false, nil
+ }
+ idx := 0
+ for {
+ lris, ok := l.lockMap[resource]
+ if !ok {
+ // Inconsistent. Delete UID.
+ delete(l.lockUID, formatUUID(args.UID, idx))
+ return idx > 0, nil
+ }
+ now := UTCNow()
+ for i := range lris {
+ if lris[i].UID == args.UID {
+ lris[i].TimeLastRefresh = now.UnixNano()
+ }
+ }
+ idx++
+ resource, ok = l.lockUID[formatUUID(args.UID, idx)]
+ if !ok {
+ // No more resources for UID, but we did update at least one.
+ return true, nil
+ }
+ }
+}
+
+// Similar to removeEntry but only removes an entry only if the lock entry exists in map.
+// Caller must hold 'l.mutex' lock.
+func (l *localLocker) expireOldLocks(interval time.Duration) {
+ defer l.getMutex()()
+
+ var readers, writers int32
+ for k, lris := range l.lockMap {
+ modified := false
+ for i := 0; i < len(lris); {
+ lri := &lris[i]
+ if time.Since(time.Unix(0, lri.TimeLastRefresh)) > interval {
+ delete(l.lockUID, formatUUID(lri.UID, lri.idx))
+ if len(lris) == 1 {
+ // Remove the write lock.
+ delete(l.lockMap, lri.Name)
+ modified = false
+ break
+ }
+ modified = true
+ // Remove the appropriate lock.
+ lris = append(lris[:i], lris[i+1:]...)
+ // Check same i
+ } else {
+ if lri.Writer {
+ writers++
+ } else {
+ readers++
+ }
+ // Move to next
+ i++
+ }
+ }
+ if modified {
+ l.lockMap[k] = lris
+ }
+ }
+ t := time.Now()
+ l.lastCleanup.Store(&t)
+ l.readers.Store(readers)
+ l.writers.Store(writers)
+}
+
+func newLocker() *localLocker {
+ return &localLocker{
+ lockMap: make(map[string][]lockRequesterInfo, 1000),
+ lockUID: make(map[string]string, 1000),
+ }
+}
diff --git a/cmd/local-locker_gen.go b/cmd/local-locker_gen.go
new file mode 100644
index 00000000..41bb9254
--- /dev/null
+++ b/cmd/local-locker_gen.go
@@ -0,0 +1,736 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "time"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+// DecodeMsg implements msgp.Decodable
+func (z *localLockMap) DecodeMsg(dc *msgp.Reader) (err error) {
+ var zb0004 uint32
+ zb0004, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if (*z) == nil {
+ (*z) = make(localLockMap, zb0004)
+ } else if len((*z)) > 0 {
+ clear((*z))
+ }
+ var field []byte
+ _ = field
+ for zb0004 > 0 {
+ zb0004--
+ var zb0001 string
+ zb0001, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ var zb0002 []lockRequesterInfo
+ var zb0005 uint32
+ zb0005, err = dc.ReadArrayHeader()
+ if err != nil {
+ err = msgp.WrapError(err, zb0001)
+ return
+ }
+ if cap(zb0002) >= int(zb0005) {
+ zb0002 = (zb0002)[:zb0005]
+ } else {
+ zb0002 = make([]lockRequesterInfo, zb0005)
+ }
+ for zb0003 := range zb0002 {
+ err = zb0002[zb0003].DecodeMsg(dc)
+ if err != nil {
+ err = msgp.WrapError(err, zb0001, zb0003)
+ return
+ }
+ }
+ (*z)[zb0001] = zb0002
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z localLockMap) EncodeMsg(en *msgp.Writer) (err error) {
+ err = en.WriteMapHeader(uint32(len(z)))
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0006, zb0007 := range z {
+ err = en.WriteString(zb0006)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ err = en.WriteArrayHeader(uint32(len(zb0007)))
+ if err != nil {
+ err = msgp.WrapError(err, zb0006)
+ return
+ }
+ for zb0008 := range zb0007 {
+ err = zb0007[zb0008].EncodeMsg(en)
+ if err != nil {
+ err = msgp.WrapError(err, zb0006, zb0008)
+ return
+ }
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z localLockMap) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ o = msgp.AppendMapHeader(o, uint32(len(z)))
+ for zb0006, zb0007 := range z {
+ o = msgp.AppendString(o, zb0006)
+ o = msgp.AppendArrayHeader(o, uint32(len(zb0007)))
+ for zb0008 := range zb0007 {
+ o, err = zb0007[zb0008].MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, zb0006, zb0008)
+ return
+ }
+ }
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *localLockMap) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var zb0004 uint32
+ zb0004, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ if (*z) == nil {
+ (*z) = make(localLockMap, zb0004)
+ } else if len((*z)) > 0 {
+ clear((*z))
+ }
+ var field []byte
+ _ = field
+ for zb0004 > 0 {
+ var zb0002 []lockRequesterInfo
+ zb0004--
+ var zb0001 string
+ zb0001, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ var zb0005 uint32
+ zb0005, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, zb0001)
+ return
+ }
+ if cap(zb0002) >= int(zb0005) {
+ zb0002 = (zb0002)[:zb0005]
+ } else {
+ zb0002 = make([]lockRequesterInfo, zb0005)
+ }
+ for zb0003 := range zb0002 {
+ bts, err = zb0002[zb0003].UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, zb0001, zb0003)
+ return
+ }
+ }
+ (*z)[zb0001] = zb0002
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z localLockMap) Msgsize() (s int) {
+ s = msgp.MapHeaderSize
+ if z != nil {
+ for zb0006, zb0007 := range z {
+ _ = zb0007
+ s += msgp.StringPrefixSize + len(zb0006) + msgp.ArrayHeaderSize
+ for zb0008 := range zb0007 {
+ s += zb0007[zb0008].Msgsize()
+ }
+ }
+ }
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *lockRequesterInfo) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Name":
+ z.Name, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Name")
+ return
+ }
+ case "Writer":
+ z.Writer, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "Writer")
+ return
+ }
+ case "UID":
+ z.UID, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "UID")
+ return
+ }
+ case "Timestamp":
+ z.Timestamp, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "Timestamp")
+ return
+ }
+ case "TimeLastRefresh":
+ z.TimeLastRefresh, err = dc.ReadInt64()
+ if err != nil {
+ err = msgp.WrapError(err, "TimeLastRefresh")
+ return
+ }
+ case "Source":
+ z.Source, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Source")
+ return
+ }
+ case "Group":
+ z.Group, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "Group")
+ return
+ }
+ case "Owner":
+ z.Owner, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Owner")
+ return
+ }
+ case "Quorum":
+ z.Quorum, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "Quorum")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *lockRequesterInfo) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 9
+ // write "Name"
+ err = en.Append(0x89, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Name)
+ if err != nil {
+ err = msgp.WrapError(err, "Name")
+ return
+ }
+ // write "Writer"
+ err = en.Append(0xa6, 0x57, 0x72, 0x69, 0x74, 0x65, 0x72)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.Writer)
+ if err != nil {
+ err = msgp.WrapError(err, "Writer")
+ return
+ }
+ // write "UID"
+ err = en.Append(0xa3, 0x55, 0x49, 0x44)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.UID)
+ if err != nil {
+ err = msgp.WrapError(err, "UID")
+ return
+ }
+ // write "Timestamp"
+ err = en.Append(0xa9, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.Timestamp)
+ if err != nil {
+ err = msgp.WrapError(err, "Timestamp")
+ return
+ }
+ // write "TimeLastRefresh"
+ err = en.Append(0xaf, 0x54, 0x69, 0x6d, 0x65, 0x4c, 0x61, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt64(z.TimeLastRefresh)
+ if err != nil {
+ err = msgp.WrapError(err, "TimeLastRefresh")
+ return
+ }
+ // write "Source"
+ err = en.Append(0xa6, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Source)
+ if err != nil {
+ err = msgp.WrapError(err, "Source")
+ return
+ }
+ // write "Group"
+ err = en.Append(0xa5, 0x47, 0x72, 0x6f, 0x75, 0x70)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.Group)
+ if err != nil {
+ err = msgp.WrapError(err, "Group")
+ return
+ }
+ // write "Owner"
+ err = en.Append(0xa5, 0x4f, 0x77, 0x6e, 0x65, 0x72)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Owner)
+ if err != nil {
+ err = msgp.WrapError(err, "Owner")
+ return
+ }
+ // write "Quorum"
+ err = en.Append(0xa6, 0x51, 0x75, 0x6f, 0x72, 0x75, 0x6d)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.Quorum)
+ if err != nil {
+ err = msgp.WrapError(err, "Quorum")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *lockRequesterInfo) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 9
+ // string "Name"
+ o = append(o, 0x89, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
+ o = msgp.AppendString(o, z.Name)
+ // string "Writer"
+ o = append(o, 0xa6, 0x57, 0x72, 0x69, 0x74, 0x65, 0x72)
+ o = msgp.AppendBool(o, z.Writer)
+ // string "UID"
+ o = append(o, 0xa3, 0x55, 0x49, 0x44)
+ o = msgp.AppendString(o, z.UID)
+ // string "Timestamp"
+ o = append(o, 0xa9, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70)
+ o = msgp.AppendInt64(o, z.Timestamp)
+ // string "TimeLastRefresh"
+ o = append(o, 0xaf, 0x54, 0x69, 0x6d, 0x65, 0x4c, 0x61, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68)
+ o = msgp.AppendInt64(o, z.TimeLastRefresh)
+ // string "Source"
+ o = append(o, 0xa6, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65)
+ o = msgp.AppendString(o, z.Source)
+ // string "Group"
+ o = append(o, 0xa5, 0x47, 0x72, 0x6f, 0x75, 0x70)
+ o = msgp.AppendBool(o, z.Group)
+ // string "Owner"
+ o = append(o, 0xa5, 0x4f, 0x77, 0x6e, 0x65, 0x72)
+ o = msgp.AppendString(o, z.Owner)
+ // string "Quorum"
+ o = append(o, 0xa6, 0x51, 0x75, 0x6f, 0x72, 0x75, 0x6d)
+ o = msgp.AppendInt(o, z.Quorum)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *lockRequesterInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Name":
+ z.Name, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Name")
+ return
+ }
+ case "Writer":
+ z.Writer, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Writer")
+ return
+ }
+ case "UID":
+ z.UID, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "UID")
+ return
+ }
+ case "Timestamp":
+ z.Timestamp, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Timestamp")
+ return
+ }
+ case "TimeLastRefresh":
+ z.TimeLastRefresh, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "TimeLastRefresh")
+ return
+ }
+ case "Source":
+ z.Source, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Source")
+ return
+ }
+ case "Group":
+ z.Group, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Group")
+ return
+ }
+ case "Owner":
+ z.Owner, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Owner")
+ return
+ }
+ case "Quorum":
+ z.Quorum, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Quorum")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *lockRequesterInfo) Msgsize() (s int) {
+ s = 1 + 5 + msgp.StringPrefixSize + len(z.Name) + 7 + msgp.BoolSize + 4 + msgp.StringPrefixSize + len(z.UID) + 10 + msgp.Int64Size + 16 + msgp.Int64Size + 7 + msgp.StringPrefixSize + len(z.Source) + 6 + msgp.BoolSize + 6 + msgp.StringPrefixSize + len(z.Owner) + 7 + msgp.IntSize
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *lockStats) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Total":
+ z.Total, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "Total")
+ return
+ }
+ case "Writes":
+ z.Writes, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "Writes")
+ return
+ }
+ case "Reads":
+ z.Reads, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "Reads")
+ return
+ }
+ case "LockQueue":
+ z.LockQueue, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "LockQueue")
+ return
+ }
+ case "LocksAbandoned":
+ z.LocksAbandoned, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "LocksAbandoned")
+ return
+ }
+ case "LastCleanup":
+ if dc.IsNil() {
+ err = dc.ReadNil()
+ if err != nil {
+ err = msgp.WrapError(err, "LastCleanup")
+ return
+ }
+ z.LastCleanup = nil
+ } else {
+ if z.LastCleanup == nil {
+ z.LastCleanup = new(time.Time)
+ }
+ *z.LastCleanup, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "LastCleanup")
+ return
+ }
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *lockStats) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 6
+ // write "Total"
+ err = en.Append(0x86, 0xa5, 0x54, 0x6f, 0x74, 0x61, 0x6c)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.Total)
+ if err != nil {
+ err = msgp.WrapError(err, "Total")
+ return
+ }
+ // write "Writes"
+ err = en.Append(0xa6, 0x57, 0x72, 0x69, 0x74, 0x65, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.Writes)
+ if err != nil {
+ err = msgp.WrapError(err, "Writes")
+ return
+ }
+ // write "Reads"
+ err = en.Append(0xa5, 0x52, 0x65, 0x61, 0x64, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.Reads)
+ if err != nil {
+ err = msgp.WrapError(err, "Reads")
+ return
+ }
+ // write "LockQueue"
+ err = en.Append(0xa9, 0x4c, 0x6f, 0x63, 0x6b, 0x51, 0x75, 0x65, 0x75, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.LockQueue)
+ if err != nil {
+ err = msgp.WrapError(err, "LockQueue")
+ return
+ }
+ // write "LocksAbandoned"
+ err = en.Append(0xae, 0x4c, 0x6f, 0x63, 0x6b, 0x73, 0x41, 0x62, 0x61, 0x6e, 0x64, 0x6f, 0x6e, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.LocksAbandoned)
+ if err != nil {
+ err = msgp.WrapError(err, "LocksAbandoned")
+ return
+ }
+ // write "LastCleanup"
+ err = en.Append(0xab, 0x4c, 0x61, 0x73, 0x74, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x75, 0x70)
+ if err != nil {
+ return
+ }
+ if z.LastCleanup == nil {
+ err = en.WriteNil()
+ if err != nil {
+ return
+ }
+ } else {
+ err = en.WriteTime(*z.LastCleanup)
+ if err != nil {
+ err = msgp.WrapError(err, "LastCleanup")
+ return
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *lockStats) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 6
+ // string "Total"
+ o = append(o, 0x86, 0xa5, 0x54, 0x6f, 0x74, 0x61, 0x6c)
+ o = msgp.AppendInt(o, z.Total)
+ // string "Writes"
+ o = append(o, 0xa6, 0x57, 0x72, 0x69, 0x74, 0x65, 0x73)
+ o = msgp.AppendInt(o, z.Writes)
+ // string "Reads"
+ o = append(o, 0xa5, 0x52, 0x65, 0x61, 0x64, 0x73)
+ o = msgp.AppendInt(o, z.Reads)
+ // string "LockQueue"
+ o = append(o, 0xa9, 0x4c, 0x6f, 0x63, 0x6b, 0x51, 0x75, 0x65, 0x75, 0x65)
+ o = msgp.AppendInt(o, z.LockQueue)
+ // string "LocksAbandoned"
+ o = append(o, 0xae, 0x4c, 0x6f, 0x63, 0x6b, 0x73, 0x41, 0x62, 0x61, 0x6e, 0x64, 0x6f, 0x6e, 0x65, 0x64)
+ o = msgp.AppendInt(o, z.LocksAbandoned)
+ // string "LastCleanup"
+ o = append(o, 0xab, 0x4c, 0x61, 0x73, 0x74, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x75, 0x70)
+ if z.LastCleanup == nil {
+ o = msgp.AppendNil(o)
+ } else {
+ o = msgp.AppendTime(o, *z.LastCleanup)
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *lockStats) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Total":
+ z.Total, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Total")
+ return
+ }
+ case "Writes":
+ z.Writes, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Writes")
+ return
+ }
+ case "Reads":
+ z.Reads, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Reads")
+ return
+ }
+ case "LockQueue":
+ z.LockQueue, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "LockQueue")
+ return
+ }
+ case "LocksAbandoned":
+ z.LocksAbandoned, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "LocksAbandoned")
+ return
+ }
+ case "LastCleanup":
+ if msgp.IsNil(bts) {
+ bts, err = msgp.ReadNilBytes(bts)
+ if err != nil {
+ return
+ }
+ z.LastCleanup = nil
+ } else {
+ if z.LastCleanup == nil {
+ z.LastCleanup = new(time.Time)
+ }
+ *z.LastCleanup, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "LastCleanup")
+ return
+ }
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *lockStats) Msgsize() (s int) {
+ s = 1 + 6 + msgp.IntSize + 7 + msgp.IntSize + 6 + msgp.IntSize + 10 + msgp.IntSize + 15 + msgp.IntSize + 12
+ if z.LastCleanup == nil {
+ s += msgp.NilSize
+ } else {
+ s += msgp.TimeSize
+ }
+ return
+}
diff --git a/cmd/local-locker_gen_test.go b/cmd/local-locker_gen_test.go
new file mode 100644
index 00000000..fe0fee2d
--- /dev/null
+++ b/cmd/local-locker_gen_test.go
@@ -0,0 +1,349 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+func TestMarshalUnmarshallocalLockMap(t *testing.T) {
+ v := localLockMap{}
+ 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 BenchmarkMarshalMsglocalLockMap(b *testing.B) {
+ v := localLockMap{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsglocalLockMap(b *testing.B) {
+ v := localLockMap{}
+ 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 BenchmarkUnmarshallocalLockMap(b *testing.B) {
+ v := localLockMap{}
+ 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 TestEncodeDecodelocalLockMap(t *testing.T) {
+ v := localLockMap{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodelocalLockMap Msgsize() is inaccurate")
+ }
+
+ vn := localLockMap{}
+ 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 BenchmarkEncodelocalLockMap(b *testing.B) {
+ v := localLockMap{}
+ 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 BenchmarkDecodelocalLockMap(b *testing.B) {
+ v := localLockMap{}
+ 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 TestMarshalUnmarshallockRequesterInfo(t *testing.T) {
+ v := lockRequesterInfo{}
+ 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 BenchmarkMarshalMsglockRequesterInfo(b *testing.B) {
+ v := lockRequesterInfo{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsglockRequesterInfo(b *testing.B) {
+ v := lockRequesterInfo{}
+ 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 BenchmarkUnmarshallockRequesterInfo(b *testing.B) {
+ v := lockRequesterInfo{}
+ 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 TestEncodeDecodelockRequesterInfo(t *testing.T) {
+ v := lockRequesterInfo{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodelockRequesterInfo Msgsize() is inaccurate")
+ }
+
+ vn := lockRequesterInfo{}
+ 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 BenchmarkEncodelockRequesterInfo(b *testing.B) {
+ v := lockRequesterInfo{}
+ 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 BenchmarkDecodelockRequesterInfo(b *testing.B) {
+ v := lockRequesterInfo{}
+ 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 TestMarshalUnmarshallockStats(t *testing.T) {
+ v := lockStats{}
+ 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 BenchmarkMarshalMsglockStats(b *testing.B) {
+ v := lockStats{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsglockStats(b *testing.B) {
+ v := lockStats{}
+ 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 BenchmarkUnmarshallockStats(b *testing.B) {
+ v := lockStats{}
+ 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 TestEncodeDecodelockStats(t *testing.T) {
+ v := lockStats{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodelockStats Msgsize() is inaccurate")
+ }
+
+ vn := lockStats{}
+ 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 BenchmarkEncodelockStats(b *testing.B) {
+ v := lockStats{}
+ 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 BenchmarkDecodelockStats(b *testing.B) {
+ v := lockStats{}
+ 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)
+ }
+ }
+}
diff --git a/cmd/local-locker_test.go b/cmd/local-locker_test.go
new file mode 100644
index 00000000..674cf07b
--- /dev/null
+++ b/cmd/local-locker_test.go
@@ -0,0 +1,442 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "encoding/hex"
+ "fmt"
+ "math/rand"
+ "testing"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/minio/minio/internal/dsync"
+)
+
+func TestLocalLockerExpire(t *testing.T) {
+ wResources := make([]string, 1000)
+ rResources := make([]string, 1000)
+ quorum := 0
+ l := newLocker()
+ ctx := t.Context()
+ for i := range wResources {
+ arg := dsync.LockArgs{
+ UID: mustGetUUID(),
+ Resources: []string{mustGetUUID()},
+ Source: t.Name(),
+ Owner: "owner",
+ Quorum: &quorum,
+ }
+ ok, err := l.Lock(ctx, arg)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !ok {
+ t.Fatal("did not get write lock")
+ }
+ wResources[i] = arg.Resources[0]
+ }
+ for i := range rResources {
+ name := mustGetUUID()
+ arg := dsync.LockArgs{
+ UID: mustGetUUID(),
+ Resources: []string{name},
+ Source: t.Name(),
+ Owner: "owner",
+ Quorum: &quorum,
+ }
+ ok, err := l.RLock(ctx, arg)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !ok {
+ t.Fatal("did not get read lock")
+ }
+ // RLock twice
+ ok, err = l.RLock(ctx, arg)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !ok {
+ t.Fatal("did not get write lock")
+ }
+
+ rResources[i] = arg.Resources[0]
+ }
+ if len(l.lockMap) != len(rResources)+len(wResources) {
+ t.Fatalf("lockmap len, got %d, want %d + %d", len(l.lockMap), len(rResources), len(wResources))
+ }
+ if len(l.lockUID) != len(rResources)+len(wResources) {
+ t.Fatalf("lockUID len, got %d, want %d + %d", len(l.lockUID), len(rResources), len(wResources))
+ }
+ // Expire an hour from now, should keep all
+ l.expireOldLocks(time.Hour)
+ if len(l.lockMap) != len(rResources)+len(wResources) {
+ t.Fatalf("lockmap len, got %d, want %d + %d", len(l.lockMap), len(rResources), len(wResources))
+ }
+ if len(l.lockUID) != len(rResources)+len(wResources) {
+ t.Fatalf("lockUID len, got %d, want %d + %d", len(l.lockUID), len(rResources), len(wResources))
+ }
+
+ // Expire a minute ago.
+ l.expireOldLocks(-time.Minute)
+ if len(l.lockMap) != 0 {
+ t.Fatalf("after cleanup should be empty, got %d", len(l.lockMap))
+ }
+ if len(l.lockUID) != 0 {
+ t.Fatalf("lockUID len, got %d, want %d", len(l.lockUID), 0)
+ }
+}
+
+func TestLocalLockerUnlock(t *testing.T) {
+ const n = 1000
+ const m = 5
+ wResources := make([][m]string, n)
+ rResources := make([]string, n)
+ wUIDs := make([]string, n)
+ rUIDs := make([]string, 0, n*2)
+ l := newLocker()
+ ctx := t.Context()
+ quorum := 0
+ for i := range wResources {
+ names := [m]string{}
+ for j := range names {
+ names[j] = mustGetUUID()
+ }
+ uid := mustGetUUID()
+ arg := dsync.LockArgs{
+ UID: uid,
+ Resources: names[:],
+ Source: t.Name(),
+ Owner: "owner",
+ Quorum: &quorum,
+ }
+ ok, err := l.Lock(ctx, arg)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !ok {
+ t.Fatal("did not get write lock")
+ }
+ wResources[i] = names
+ wUIDs[i] = uid
+ }
+ for i := range rResources {
+ name := mustGetUUID()
+ uid := mustGetUUID()
+ arg := dsync.LockArgs{
+ UID: uid,
+ Resources: []string{name},
+ Source: t.Name(),
+ Owner: "owner",
+ Quorum: &quorum,
+ }
+ ok, err := l.RLock(ctx, arg)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !ok {
+ t.Fatal("did not get write lock")
+ }
+ rUIDs = append(rUIDs, uid)
+
+ // RLock twice, different uid
+ uid = mustGetUUID()
+ arg.UID = uid
+ ok, err = l.RLock(ctx, arg)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !ok {
+ t.Fatal("did not get write lock")
+ }
+ rResources[i] = name
+ rUIDs = append(rUIDs, uid)
+ }
+ // Each Lock has m entries
+ if len(l.lockMap) != len(rResources)+len(wResources)*m {
+ t.Fatalf("lockmap len, got %d, want %d + %d", len(l.lockMap), len(rResources), len(wResources)*m)
+ }
+ // A UID is added for every resource
+ if len(l.lockUID) != len(rResources)*2+len(wResources)*m {
+ t.Fatalf("lockUID len, got %d, want %d + %d", len(l.lockUID), len(rResources)*2, len(wResources)*m)
+ }
+ // RUnlock once...
+ for i, name := range rResources {
+ arg := dsync.LockArgs{
+ UID: rUIDs[i*2],
+ Resources: []string{name},
+ Source: t.Name(),
+ Owner: "owner",
+ Quorum: &quorum,
+ }
+ ok, err := l.RUnlock(ctx, arg)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !ok {
+ t.Fatal("did not get write lock")
+ }
+ }
+
+ // Each Lock has m entries
+ if len(l.lockMap) != len(rResources)+len(wResources)*m {
+ t.Fatalf("lockmap len, got %d, want %d + %d", len(l.lockMap), len(rResources), len(wResources)*m)
+ }
+ // A UID is added for every resource.
+ // We removed len(rResources) read sources.
+ if len(l.lockUID) != len(rResources)+len(wResources)*m {
+ t.Fatalf("lockUID len, got %d, want %d + %d", len(l.lockUID), len(rResources), len(wResources)*m)
+ }
+
+ // RUnlock again, different uids
+ for i, name := range rResources {
+ arg := dsync.LockArgs{
+ UID: rUIDs[i*2+1],
+ Resources: []string{name},
+ Source: "minio",
+ Owner: "owner",
+ Quorum: &quorum,
+ }
+ ok, err := l.RUnlock(ctx, arg)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !ok {
+ t.Fatal("did not get write lock")
+ }
+ }
+
+ // Each Lock has m entries
+ if len(l.lockMap) != 0+len(wResources)*m {
+ t.Fatalf("lockmap len, got %d, want %d + %d", len(l.lockMap), 0, len(wResources)*m)
+ }
+ // A UID is added for every resource.
+ // We removed Add Rlocked entries
+ if len(l.lockUID) != len(wResources)*m {
+ t.Fatalf("lockUID len, got %d, want %d + %d", len(l.lockUID), 0, len(wResources)*m)
+ }
+
+ // Remove write locked
+ for i, names := range wResources {
+ arg := dsync.LockArgs{
+ UID: wUIDs[i],
+ Resources: names[:],
+ Source: "minio",
+ Owner: "owner",
+ Quorum: &quorum,
+ }
+ ok, err := l.Unlock(ctx, arg)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !ok {
+ t.Fatal("did not get write lock")
+ }
+ }
+
+ // All should be gone now...
+ // Each Lock has m entries
+ if len(l.lockMap) != 0 {
+ t.Fatalf("lockmap len, got %d, want %d + %d", len(l.lockMap), 0, 0)
+ }
+ if len(l.lockUID) != 0 {
+ t.Fatalf("lockUID len, got %d, want %d + %d", len(l.lockUID), 0, 0)
+ }
+}
+
+func Test_localLocker_expireOldLocksExpire(t *testing.T) {
+ rng := rand.New(rand.NewSource(0))
+ quorum := 0
+ // Numbers of unique locks
+ for _, locks := range []int{100, 1000, 1e6} {
+ if testing.Short() && locks > 100 {
+ continue
+ }
+ t.Run(fmt.Sprintf("%d-locks", locks), func(t *testing.T) {
+ // Number of readers per lock...
+ for _, readers := range []int{1, 10, 100} {
+ if locks > 1000 && readers > 1 {
+ continue
+ }
+ if testing.Short() && readers > 10 {
+ continue
+ }
+ t.Run(fmt.Sprintf("%d-read", readers), func(t *testing.T) {
+ l := newLocker()
+ for range locks {
+ var tmp [16]byte
+ rng.Read(tmp[:])
+ res := []string{hex.EncodeToString(tmp[:])}
+
+ for range readers {
+ rng.Read(tmp[:])
+ ok, err := l.RLock(t.Context(), dsync.LockArgs{
+ UID: uuid.NewString(),
+ Resources: res,
+ Source: hex.EncodeToString(tmp[:8]),
+ Owner: hex.EncodeToString(tmp[8:]),
+ Quorum: &quorum,
+ })
+ if !ok || err != nil {
+ t.Fatal("failed:", err, ok)
+ }
+ }
+ }
+ start := time.Now()
+ l.expireOldLocks(time.Hour)
+ t.Logf("Scan Took: %v. Left: %d/%d", time.Since(start).Round(time.Millisecond), len(l.lockUID), len(l.lockMap))
+ if len(l.lockMap) != locks {
+ t.Fatalf("objects deleted, want %d != got %d", locks, len(l.lockMap))
+ }
+ if len(l.lockUID) != locks*readers {
+ t.Fatalf("objects deleted, want %d != got %d", locks*readers, len(l.lockUID))
+ }
+
+ // Expire 50%
+ expired := time.Now().Add(-time.Hour * 2)
+ for _, v := range l.lockMap {
+ for i := range v {
+ if rng.Intn(2) == 0 {
+ v[i].TimeLastRefresh = expired.UnixNano()
+ }
+ }
+ }
+ start = time.Now()
+ l.expireOldLocks(time.Hour)
+ t.Logf("Expire 50%% took: %v. Left: %d/%d", time.Since(start).Round(time.Millisecond), len(l.lockUID), len(l.lockMap))
+
+ if len(l.lockUID) == locks*readers {
+ t.Fatalf("objects uids all remain, unlikely")
+ }
+ if len(l.lockMap) == 0 {
+ t.Fatalf("objects all deleted, 0 remains")
+ }
+ if len(l.lockUID) == 0 {
+ t.Fatalf("objects uids all deleted, 0 remains")
+ }
+
+ start = time.Now()
+ l.expireOldLocks(-time.Minute)
+ t.Logf("Expire rest took: %v. Left: %d/%d", time.Since(start).Round(time.Millisecond), len(l.lockUID), len(l.lockMap))
+
+ if len(l.lockMap) != 0 {
+ t.Fatalf("objects not deleted, want %d != got %d", 0, len(l.lockMap))
+ }
+ if len(l.lockUID) != 0 {
+ t.Fatalf("objects not deleted, want %d != got %d", 0, len(l.lockUID))
+ }
+ })
+ }
+ })
+ }
+}
+
+func Test_localLocker_RUnlock(t *testing.T) {
+ rng := rand.New(rand.NewSource(0))
+ quorum := 0
+ // Numbers of unique locks
+ for _, locks := range []int{1, 100, 1000, 1e6} {
+ if testing.Short() && locks > 100 {
+ continue
+ }
+ t.Run(fmt.Sprintf("%d-locks", locks), func(t *testing.T) {
+ // Number of readers per lock...
+ for _, readers := range []int{1, 10, 100} {
+ if locks > 1000 && readers > 1 {
+ continue
+ }
+ if testing.Short() && readers > 10 {
+ continue
+ }
+ t.Run(fmt.Sprintf("%d-read", readers), func(t *testing.T) {
+ l := newLocker()
+ for range locks {
+ var tmp [16]byte
+ rng.Read(tmp[:])
+ res := []string{hex.EncodeToString(tmp[:])}
+
+ for range readers {
+ rng.Read(tmp[:])
+ ok, err := l.RLock(t.Context(), dsync.LockArgs{
+ UID: uuid.NewString(),
+ Resources: res,
+ Source: hex.EncodeToString(tmp[:8]),
+ Owner: hex.EncodeToString(tmp[8:]),
+ Quorum: &quorum,
+ })
+ if !ok || err != nil {
+ t.Fatal("failed:", err, ok)
+ }
+ }
+ }
+
+ // Expire 50%
+ toUnLock := make([]dsync.LockArgs, 0, locks*readers)
+ for k, v := range l.lockMap {
+ for _, lock := range v {
+ if rng.Intn(2) == 0 {
+ toUnLock = append(toUnLock, dsync.LockArgs{Resources: []string{k}, UID: lock.UID})
+ }
+ }
+ }
+ start := time.Now()
+ for _, lock := range toUnLock {
+ ok, err := l.ForceUnlock(t.Context(), lock)
+ if err != nil || !ok {
+ t.Fatal(err)
+ }
+ }
+ t.Logf("Expire 50%% took: %v. Left: %d/%d", time.Since(start).Round(time.Millisecond), len(l.lockUID), len(l.lockMap))
+
+ if len(l.lockUID) == locks*readers {
+ t.Fatalf("objects uids all remain, unlikely")
+ }
+ if len(l.lockMap) == 0 && locks > 10 {
+ t.Fatalf("objects all deleted, 0 remains")
+ }
+ if len(l.lockUID) != locks*readers-len(toUnLock) {
+ t.Fatalf("want %d objects uids all deleted, %d remains", len(l.lockUID), locks*readers-len(toUnLock))
+ }
+
+ toUnLock = toUnLock[:0]
+ for k, v := range l.lockMap {
+ for _, lock := range v {
+ toUnLock = append(toUnLock, dsync.LockArgs{Resources: []string{k}, UID: lock.UID, Owner: lock.Owner})
+ }
+ }
+ start = time.Now()
+ for _, lock := range toUnLock {
+ ok, err := l.RUnlock(t.Context(), lock)
+ if err != nil || !ok {
+ t.Fatal(err)
+ }
+ }
+ t.Logf("Expire rest took: %v. Left: %d/%d", time.Since(start).Round(time.Millisecond), len(l.lockUID), len(l.lockMap))
+
+ if len(l.lockMap) != 0 {
+ t.Fatalf("objects not deleted, want %d != got %d", 0, len(l.lockMap))
+ }
+ if len(l.lockUID) != 0 {
+ t.Fatalf("objects not deleted, want %d != got %d", 0, len(l.lockUID))
+ }
+ })
+ }
+ })
+ }
+}
diff --git a/cmd/lock-rest-client.go b/cmd/lock-rest-client.go
new file mode 100644
index 00000000..ff8e2c35
--- /dev/null
+++ b/cmd/lock-rest-client.go
@@ -0,0 +1,111 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "errors"
+
+ "github.com/minio/minio/internal/dsync"
+ "github.com/minio/minio/internal/grid"
+)
+
+// lockRESTClient is authenticable lock REST client
+type lockRESTClient struct {
+ connection *grid.Connection
+}
+
+// IsOnline - returns whether REST client failed to connect or not.
+func (c *lockRESTClient) IsOnline() bool {
+ return c.connection.State() == grid.StateConnected
+}
+
+// Not a local locker
+func (c *lockRESTClient) IsLocal() bool {
+ return false
+}
+
+// Close - marks the client as closed.
+func (c *lockRESTClient) Close() error {
+ return nil
+}
+
+// String - returns the remote host of the connection.
+func (c *lockRESTClient) String() string {
+ return c.connection.Remote
+}
+
+func (c *lockRESTClient) call(ctx context.Context, h *grid.SingleHandler[*dsync.LockArgs, *dsync.LockResp], args *dsync.LockArgs) (ok bool, err error) {
+ r, err := h.Call(ctx, c.connection, args)
+ if err != nil {
+ return false, err
+ }
+ defer h.PutResponse(r)
+ ok = r.Code == dsync.RespOK
+ switch r.Code {
+ case dsync.RespLockConflict, dsync.RespLockNotFound, dsync.RespOK:
+ // no error
+ case dsync.RespLockNotInitialized:
+ err = errLockNotInitialized
+ default:
+ err = errors.New(r.Err)
+ }
+ return ok, err
+}
+
+// RLock calls read lock REST API.
+func (c *lockRESTClient) RLock(ctx context.Context, args dsync.LockArgs) (reply bool, err error) {
+ return c.call(ctx, lockRPCRLock, &args)
+}
+
+// Lock calls lock REST API.
+func (c *lockRESTClient) Lock(ctx context.Context, args dsync.LockArgs) (reply bool, err error) {
+ return c.call(ctx, lockRPCLock, &args)
+}
+
+// RUnlock calls read unlock REST API.
+func (c *lockRESTClient) RUnlock(ctx context.Context, args dsync.LockArgs) (reply bool, err error) {
+ return c.call(ctx, lockRPCRUnlock, &args)
+}
+
+// Refresh calls Refresh REST API.
+func (c *lockRESTClient) Refresh(ctx context.Context, args dsync.LockArgs) (reply bool, err error) {
+ return c.call(ctx, lockRPCRefresh, &args)
+}
+
+// Unlock calls write unlock RPC.
+func (c *lockRESTClient) Unlock(ctx context.Context, args dsync.LockArgs) (reply bool, err error) {
+ return c.call(ctx, lockRPCUnlock, &args)
+}
+
+// ForceUnlock calls force unlock handler to forcibly unlock an active lock.
+func (c *lockRESTClient) ForceUnlock(ctx context.Context, args dsync.LockArgs) (reply bool, err error) {
+ return c.call(ctx, lockRPCForceUnlock, &args)
+}
+
+func newLockAPI(endpoint Endpoint) dsync.NetLocker {
+ if endpoint.IsLocal {
+ return globalLockServer
+ }
+ return newlockRESTClient(endpoint)
+}
+
+// Returns a lock rest client.
+func newlockRESTClient(ep Endpoint) *lockRESTClient {
+ return &lockRESTClient{globalLockGrid.Load().Connection(ep.GridHost())}
+}
diff --git a/cmd/lock-rest-client_test.go b/cmd/lock-rest-client_test.go
new file mode 100644
index 00000000..cdfadc6a
--- /dev/null
+++ b/cmd/lock-rest-client_test.go
@@ -0,0 +1,72 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "testing"
+
+ "github.com/minio/minio/internal/dsync"
+)
+
+// Tests lock rpc client.
+func TestLockRESTlient(t *testing.T) {
+ // These should not be connectable.
+ endpoint, err := NewEndpoint("http://localhost:9876")
+ if err != nil {
+ t.Fatalf("unexpected error %v", err)
+ }
+ endpointLocal, err := NewEndpoint("http://localhost:9012")
+ if err != nil {
+ t.Fatalf("unexpected error %v", err)
+ }
+ endpointLocal.IsLocal = true
+
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+ err = initGlobalLockGrid(ctx, []PoolEndpoints{{Endpoints: Endpoints{endpoint, endpointLocal}}})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ lkClient := newlockRESTClient(endpoint)
+ if lkClient.IsOnline() {
+ t.Fatalf("unexpected result. connection was online")
+ }
+
+ // Attempt all calls.
+ _, err = lkClient.RLock(t.Context(), dsync.LockArgs{})
+ if err == nil {
+ t.Fatal("Expected for Rlock to fail")
+ }
+
+ _, err = lkClient.Lock(t.Context(), dsync.LockArgs{})
+ if err == nil {
+ t.Fatal("Expected for Lock to fail")
+ }
+
+ _, err = lkClient.RUnlock(t.Context(), dsync.LockArgs{})
+ if err == nil {
+ t.Fatal("Expected for RUnlock to fail")
+ }
+
+ _, err = lkClient.Unlock(t.Context(), dsync.LockArgs{})
+ if err == nil {
+ t.Fatal("Expected for Unlock to fail")
+ }
+}
diff --git a/cmd/lock-rest-server-common.go b/cmd/lock-rest-server-common.go
new file mode 100644
index 00000000..ddaa6bb3
--- /dev/null
+++ b/cmd/lock-rest-server-common.go
@@ -0,0 +1,28 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "errors"
+)
+
+var (
+ errLockConflict = errors.New("lock conflict")
+ errLockNotInitialized = errors.New("lock not initialized")
+ errLockNotFound = errors.New("lock not found")
+)
diff --git a/cmd/lock-rest-server-common_test.go b/cmd/lock-rest-server-common_test.go
new file mode 100644
index 00000000..21b7fdcd
--- /dev/null
+++ b/cmd/lock-rest-server-common_test.go
@@ -0,0 +1,117 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "os"
+ "reflect"
+ "sync"
+ "testing"
+
+ "github.com/minio/minio/internal/dsync"
+)
+
+// Helper function to create a lock server for testing
+func createLockTestServer(ctx context.Context, t *testing.T) (string, *lockRESTServer, string) {
+ obj, fsDir, err := prepareFS(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil {
+ t.Fatalf("unable initialize config file, %s", err)
+ }
+
+ locker := &lockRESTServer{
+ ll: &localLocker{
+ mutex: sync.Mutex{},
+ lockMap: make(map[string][]lockRequesterInfo),
+ },
+ }
+ creds := globalActiveCred
+ token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return fsDir, locker, token
+}
+
+// Test function to remove lock entries from map based on name & uid combination
+func TestLockRpcServerRemoveEntry(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ testPath, locker, _ := createLockTestServer(ctx, t)
+ defer os.RemoveAll(testPath)
+
+ lockRequesterInfo1 := lockRequesterInfo{
+ Owner: "owner",
+ Writer: true,
+ UID: "0123-4567",
+ Timestamp: UTCNow().UnixNano(),
+ TimeLastRefresh: UTCNow().UnixNano(),
+ }
+ lockRequesterInfo2 := lockRequesterInfo{
+ Owner: "owner",
+ Writer: true,
+ UID: "89ab-cdef",
+ Timestamp: UTCNow().UnixNano(),
+ TimeLastRefresh: UTCNow().UnixNano(),
+ }
+
+ locker.ll.lockMap["name"] = []lockRequesterInfo{
+ lockRequesterInfo1,
+ lockRequesterInfo2,
+ }
+
+ lri := locker.ll.lockMap["name"]
+
+ // test unknown uid
+ if locker.ll.removeEntry("name", dsync.LockArgs{
+ Owner: "owner",
+ UID: "unknown-uid",
+ }, &lri) {
+ t.Errorf("Expected %#v, got %#v", false, true)
+ }
+
+ if !locker.ll.removeEntry("name", dsync.LockArgs{
+ Owner: "owner",
+ UID: "0123-4567",
+ }, &lri) {
+ t.Errorf("Expected %#v, got %#v", true, false)
+ } else {
+ gotLri := locker.ll.lockMap["name"]
+ expectedLri := []lockRequesterInfo{lockRequesterInfo2}
+ if !reflect.DeepEqual(expectedLri, gotLri) {
+ t.Errorf("Expected %#v, got %#v", expectedLri, gotLri)
+ }
+ }
+
+ if !locker.ll.removeEntry("name", dsync.LockArgs{
+ Owner: "owner",
+ UID: "89ab-cdef",
+ }, &lri) {
+ t.Errorf("Expected %#v, got %#v", true, false)
+ } else {
+ gotLri := locker.ll.lockMap["name"]
+ expectedLri := []lockRequesterInfo(nil)
+ if !reflect.DeepEqual(expectedLri, gotLri) {
+ t.Errorf("Expected %#v, got %#v", expectedLri, gotLri)
+ }
+ }
+}
diff --git a/cmd/lock-rest-server.go b/cmd/lock-rest-server.go
new file mode 100644
index 00000000..79e3dfdb
--- /dev/null
+++ b/cmd/lock-rest-server.go
@@ -0,0 +1,190 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "time"
+
+ "github.com/minio/minio/internal/dsync"
+ "github.com/minio/minio/internal/grid"
+ "github.com/minio/minio/internal/logger"
+)
+
+// To abstract a node over network.
+type lockRESTServer struct {
+ ll *localLocker
+}
+
+// RefreshHandler - refresh the current lock
+func (l *lockRESTServer) RefreshHandler(args *dsync.LockArgs) (*dsync.LockResp, *grid.RemoteErr) {
+ // Add a timeout similar to what we expect upstream.
+ ctx, cancel := context.WithTimeout(context.Background(), dsync.DefaultTimeouts.RefreshCall)
+ defer cancel()
+
+ resp := lockRPCRefresh.NewResponse()
+ refreshed, err := l.ll.Refresh(ctx, *args)
+ if err != nil {
+ return l.makeResp(resp, err)
+ }
+ if !refreshed {
+ return l.makeResp(resp, errLockNotFound)
+ }
+ return l.makeResp(resp, err)
+}
+
+// LockHandler - Acquires a lock.
+func (l *lockRESTServer) LockHandler(args *dsync.LockArgs) (*dsync.LockResp, *grid.RemoteErr) {
+ // Add a timeout similar to what we expect upstream.
+ ctx, cancel := context.WithTimeout(context.Background(), dsync.DefaultTimeouts.Acquire)
+ defer cancel()
+ resp := lockRPCLock.NewResponse()
+ success, err := l.ll.Lock(ctx, *args)
+ if err == nil && !success {
+ return l.makeResp(resp, errLockConflict)
+ }
+ return l.makeResp(resp, err)
+}
+
+// UnlockHandler - releases the acquired lock.
+func (l *lockRESTServer) UnlockHandler(args *dsync.LockArgs) (*dsync.LockResp, *grid.RemoteErr) {
+ resp := lockRPCUnlock.NewResponse()
+ _, err := l.ll.Unlock(context.Background(), *args)
+ // Ignore the Unlock() "reply" return value because if err == nil, "reply" is always true
+ // Consequently, if err != nil, reply is always false
+ return l.makeResp(resp, err)
+}
+
+// RLockHandler - Acquires an RLock.
+func (l *lockRESTServer) RLockHandler(args *dsync.LockArgs) (*dsync.LockResp, *grid.RemoteErr) {
+ // Add a timeout similar to what we expect upstream.
+ ctx, cancel := context.WithTimeout(context.Background(), dsync.DefaultTimeouts.Acquire)
+ defer cancel()
+ resp := lockRPCRLock.NewResponse()
+ success, err := l.ll.RLock(ctx, *args)
+ if err == nil && !success {
+ err = errLockConflict
+ }
+ return l.makeResp(resp, err)
+}
+
+// RUnlockHandler - releases the acquired read lock.
+func (l *lockRESTServer) RUnlockHandler(args *dsync.LockArgs) (*dsync.LockResp, *grid.RemoteErr) {
+ resp := lockRPCRUnlock.NewResponse()
+
+ // Ignore the RUnlock() "reply" return value because if err == nil, "reply" is always true.
+ // Consequently, if err != nil, reply is always false
+ _, err := l.ll.RUnlock(context.Background(), *args)
+ return l.makeResp(resp, err)
+}
+
+// ForceUnlockHandler - query expired lock status.
+func (l *lockRESTServer) ForceUnlockHandler(args *dsync.LockArgs) (*dsync.LockResp, *grid.RemoteErr) {
+ resp := lockRPCForceUnlock.NewResponse()
+
+ _, err := l.ll.ForceUnlock(context.Background(), *args)
+ return l.makeResp(resp, err)
+}
+
+var (
+ // Static lock handlers.
+ // All have the same signature.
+ lockRPCForceUnlock = newLockHandler(grid.HandlerLockForceUnlock)
+ lockRPCRefresh = newLockHandler(grid.HandlerLockRefresh)
+ lockRPCLock = newLockHandler(grid.HandlerLockLock)
+ lockRPCUnlock = newLockHandler(grid.HandlerLockUnlock)
+ lockRPCRLock = newLockHandler(grid.HandlerLockRLock)
+ lockRPCRUnlock = newLockHandler(grid.HandlerLockRUnlock)
+)
+
+func newLockHandler(h grid.HandlerID) *grid.SingleHandler[*dsync.LockArgs, *dsync.LockResp] {
+ return grid.NewSingleHandler[*dsync.LockArgs, *dsync.LockResp](h, func() *dsync.LockArgs {
+ return &dsync.LockArgs{}
+ }, func() *dsync.LockResp {
+ return &dsync.LockResp{}
+ })
+}
+
+// registerLockRESTHandlers - register lock rest router.
+func registerLockRESTHandlers(gm *grid.Manager) {
+ lockServer := &lockRESTServer{
+ ll: newLocker(),
+ }
+
+ logger.FatalIf(lockRPCForceUnlock.Register(gm, lockServer.ForceUnlockHandler), "unable to register handler")
+ logger.FatalIf(lockRPCRefresh.Register(gm, lockServer.RefreshHandler), "unable to register handler")
+ logger.FatalIf(lockRPCLock.Register(gm, lockServer.LockHandler), "unable to register handler")
+ logger.FatalIf(lockRPCUnlock.Register(gm, lockServer.UnlockHandler), "unable to register handler")
+ logger.FatalIf(lockRPCRLock.Register(gm, lockServer.RLockHandler), "unable to register handler")
+ logger.FatalIf(lockRPCRUnlock.Register(gm, lockServer.RUnlockHandler), "unable to register handler")
+
+ globalLockServer = lockServer.ll
+
+ go lockMaintenance(GlobalContext)
+}
+
+func (l *lockRESTServer) makeResp(dst *dsync.LockResp, err error) (*dsync.LockResp, *grid.RemoteErr) {
+ *dst = dsync.LockResp{Code: dsync.RespOK}
+ switch err {
+ case nil:
+ case errLockNotInitialized:
+ dst.Code = dsync.RespLockNotInitialized
+ case errLockConflict:
+ dst.Code = dsync.RespLockConflict
+ case errLockNotFound:
+ dst.Code = dsync.RespLockNotFound
+ default:
+ dst.Code = dsync.RespErr
+ dst.Err = err.Error()
+ }
+ return dst, nil
+}
+
+const (
+ // Lock maintenance interval.
+ lockMaintenanceInterval = 1 * time.Minute
+
+ // Lock validity duration
+ lockValidityDuration = 1 * time.Minute
+)
+
+// lockMaintenance loops over all locks and discards locks
+// that have not been refreshed for some time.
+func lockMaintenance(ctx context.Context) {
+ if !globalIsDistErasure {
+ return
+ }
+
+ // Initialize a new ticker with 1 minute between each ticks.
+ lkTimer := time.NewTimer(lockMaintenanceInterval)
+ // Stop the timer upon returning.
+ defer lkTimer.Stop()
+
+ for {
+ // Verifies every minute for locks held more than 2 minutes.
+ select {
+ case <-ctx.Done():
+ return
+ case <-lkTimer.C:
+ globalLockServer.expireOldLocks(lockValidityDuration)
+
+ // Reset the timer for next cycle.
+ lkTimer.Reset(lockMaintenanceInterval)
+ }
+ }
+}
diff --git a/cmd/logging.go b/cmd/logging.go
new file mode 100644
index 00000000..9c24ae22
--- /dev/null
+++ b/cmd/logging.go
@@ -0,0 +1,230 @@
+package cmd
+
+import (
+ "context"
+ "errors"
+
+ "github.com/minio/minio/internal/grid"
+ "github.com/minio/minio/internal/logger"
+)
+
+func proxyLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "proxy", err, errKind...)
+}
+
+func replLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "replication", err, errKind...)
+}
+
+func replLogOnceIf(ctx context.Context, err error, id string, errKind ...any) {
+ logger.LogOnceIf(ctx, "replication", err, id, errKind...)
+}
+
+func iamLogOnceIf(ctx context.Context, err error, id string, errKind ...any) {
+ logger.LogOnceIf(ctx, "iam", err, id, errKind...)
+}
+
+func iamLogIf(ctx context.Context, err error, errKind ...any) {
+ if !errors.Is(err, grid.ErrDisconnected) {
+ logger.LogIf(ctx, "iam", err, errKind...)
+ }
+}
+
+func iamLogEvent(ctx context.Context, msg string, args ...any) {
+ logger.Event(ctx, "iam", msg, args...)
+}
+
+func rebalanceLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "rebalance", err, errKind...)
+}
+
+func rebalanceLogEvent(ctx context.Context, msg string, args ...any) {
+ logger.Event(ctx, "rebalance", msg, args...)
+}
+
+func adminLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "admin", err, errKind...)
+}
+
+func authNLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "authN", err, errKind...)
+}
+
+func authZLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "authZ", err, errKind...)
+}
+
+func peersLogIf(ctx context.Context, err error, errKind ...any) {
+ if !errors.Is(err, grid.ErrDisconnected) {
+ logger.LogIf(ctx, "peers", err, errKind...)
+ }
+}
+
+func peersLogAlwaysIf(ctx context.Context, err error, errKind ...any) {
+ if !errors.Is(err, grid.ErrDisconnected) {
+ logger.LogAlwaysIf(ctx, "peers", err, errKind...)
+ }
+}
+
+func peersLogOnceIf(ctx context.Context, err error, id string, errKind ...any) {
+ if !errors.Is(err, grid.ErrDisconnected) {
+ logger.LogOnceIf(ctx, "peers", err, id, errKind...)
+ }
+}
+
+func bugLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "internal", err, errKind...)
+}
+
+func healingLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "healing", err, errKind...)
+}
+
+func healingLogEvent(ctx context.Context, msg string, args ...any) {
+ logger.Event(ctx, "healing", msg, args...)
+}
+
+func healingLogOnceIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "healing", err, errKind...)
+}
+
+func batchLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "batch", err, errKind...)
+}
+
+func batchLogOnceIf(ctx context.Context, err error, id string, errKind ...any) {
+ logger.LogOnceIf(ctx, "batch", err, id, errKind...)
+}
+
+func bootLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "bootstrap", err, errKind...)
+}
+
+func bootLogOnceIf(ctx context.Context, err error, id string, errKind ...any) {
+ logger.LogOnceIf(ctx, "bootstrap", err, id, errKind...)
+}
+
+func dnsLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "dns", err, errKind...)
+}
+
+func internalLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "internal", err, errKind...)
+}
+
+func internalLogOnceIf(ctx context.Context, err error, id string, errKind ...any) {
+ logger.LogOnceIf(ctx, "internal", err, id, errKind...)
+}
+
+func transitionLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "transition", err, errKind...)
+}
+
+func configLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "config", err, errKind...)
+}
+
+func configLogOnceIf(ctx context.Context, err error, id string, errKind ...any) {
+ logger.LogOnceIf(ctx, "config", err, id, errKind...)
+}
+
+func configLogOnceConsoleIf(ctx context.Context, err error, id string, errKind ...any) {
+ logger.LogOnceConsoleIf(ctx, "config", err, id, errKind...)
+}
+
+func scannerLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "scanner", err, errKind...)
+}
+
+func scannerLogOnceIf(ctx context.Context, err error, id string, errKind ...any) {
+ logger.LogOnceIf(ctx, "scanner", err, id, errKind...)
+}
+
+func ilmLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "ilm", err, errKind...)
+}
+
+func ilmLogOnceIf(ctx context.Context, err error, id string, errKind ...any) {
+ logger.LogOnceIf(ctx, "ilm", err, id, errKind...)
+}
+
+func encLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "encryption", err, errKind...)
+}
+
+func encLogOnceIf(ctx context.Context, err error, id string, errKind ...any) {
+ logger.LogOnceIf(ctx, "encryption", err, id, errKind...)
+}
+
+func storageLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "storage", err, errKind...)
+}
+
+func storageLogAlwaysIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogAlwaysIf(ctx, "storage", err, errKind...)
+}
+
+func storageLogOnceIf(ctx context.Context, err error, id string, errKind ...any) {
+ logger.LogOnceIf(ctx, "storage", err, id, errKind...)
+}
+
+func decomLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "decom", err, errKind...)
+}
+
+func decomLogOnceIf(ctx context.Context, err error, id string, errKind ...any) {
+ logger.LogOnceIf(ctx, "decom", err, id, errKind...)
+}
+
+func decomLogEvent(ctx context.Context, msg string, args ...any) {
+ logger.Event(ctx, "decom", msg, args...)
+}
+
+func etcdLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "etcd", err, errKind...)
+}
+
+func etcdLogOnceIf(ctx context.Context, err error, id string, errKind ...any) {
+ logger.LogOnceIf(ctx, "etcd", err, id, errKind...)
+}
+
+func metricsLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "metrics", err, errKind...)
+}
+
+func s3LogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "s3", err, errKind...)
+}
+
+func sftpLogOnceIf(ctx context.Context, err error, id string, errKind ...any) {
+ logger.LogOnceIf(ctx, "sftp", err, id, errKind...)
+}
+
+func shutdownLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "shutdown", err, errKind...)
+}
+
+func stsLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "sts", err, errKind...)
+}
+
+func tierLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "tier", err, errKind...)
+}
+
+func kmsLogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "kms", err, errKind...)
+}
+
+// KMSLogger permits access to kms module specific logging
+type KMSLogger struct{}
+
+// LogOnceIf is the implementation of LogOnceIf, accessible using the Logger interface
+func (l KMSLogger) LogOnceIf(ctx context.Context, err error, id string, errKind ...any) {
+ logger.LogOnceIf(ctx, "kms", err, id, errKind...)
+}
+
+// LogIf is the implementation of LogIf, accessible using the Logger interface
+func (l KMSLogger) LogIf(ctx context.Context, err error, errKind ...any) {
+ logger.LogIf(ctx, "kms", err, errKind...)
+}
diff --git a/cmd/main.go b/cmd/main.go
new file mode 100644
index 00000000..53249af4
--- /dev/null
+++ b/cmd/main.go
@@ -0,0 +1,228 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "runtime"
+ "runtime/debug"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/minio/cli"
+ "github.com/minio/minio/internal/color"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/console"
+ "github.com/minio/pkg/v3/env"
+ "github.com/minio/pkg/v3/trie"
+ "github.com/minio/pkg/v3/words"
+)
+
+// GlobalFlags - global flags for minio.
+var GlobalFlags = []cli.Flag{
+ // Deprecated flag, so its hidden now - existing deployments will keep working.
+ cli.StringFlag{
+ Name: "config-dir, C",
+ Value: defaultConfigDir.Get(),
+ Usage: "[DEPRECATED] path to legacy configuration directory",
+ Hidden: true,
+ },
+ cli.StringFlag{
+ Name: "certs-dir, S",
+ Value: defaultCertsDir.Get(),
+ Usage: "path to certs directory",
+ },
+ cli.BoolFlag{
+ Name: "quiet",
+ Usage: "disable startup and info messages",
+ },
+ cli.BoolFlag{
+ Name: "anonymous",
+ Usage: "hide sensitive information from logging",
+ },
+ cli.BoolFlag{
+ Name: "json",
+ Usage: "output logs in JSON format",
+ },
+ // Deprecated flag, so its hidden now, existing deployments will keep working.
+ cli.BoolFlag{
+ Name: "compat",
+ Usage: "enable strict S3 compatibility by turning off certain performance optimizations",
+ Hidden: true,
+ },
+ // This flag is hidden and to be used only during certain performance testing.
+ cli.BoolFlag{
+ Name: "no-compat",
+ Usage: "disable strict S3 compatibility by turning on certain performance optimizations",
+ Hidden: true,
+ },
+}
+
+// Help template for minio.
+var minioHelpTemplate = `NAME:
+ {{.Name}} - {{.Usage}}
+
+DESCRIPTION:
+ {{.Description}}
+
+USAGE:
+ {{.HelpName}} {{if .VisibleFlags}}[FLAGS] {{end}}COMMAND{{if .VisibleFlags}}{{end}} [ARGS...]
+
+COMMANDS:
+ {{range .VisibleCommands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}}
+ {{end}}{{if .VisibleFlags}}
+FLAGS:
+ {{range .VisibleFlags}}{{.}}
+ {{end}}{{end}}
+VERSION:
+ {{.Version}}
+`
+
+func newApp(name string) *cli.App {
+ // Collection of minio commands currently supported are.
+ commands := []cli.Command{}
+
+ // Collection of minio commands currently supported in a trie tree.
+ commandsTree := trie.NewTrie()
+
+ // registerCommand registers a cli command.
+ registerCommand := func(command cli.Command) {
+ // avoid registering commands which are not being built (via
+ // go:build tags)
+ if command.Name == "" {
+ return
+ }
+ commands = append(commands, command)
+ commandsTree.Insert(command.Name)
+ }
+
+ findClosestCommands := func(command string) []string {
+ var closestCommands []string
+ closestCommands = append(closestCommands, commandsTree.PrefixMatch(command)...)
+
+ sort.Strings(closestCommands)
+ // Suggest other close commands - allow missed, wrongly added and
+ // even transposed characters
+ for _, value := range commandsTree.Walk(commandsTree.Root()) {
+ if sort.SearchStrings(closestCommands, value) < len(closestCommands) {
+ continue
+ }
+ // 2 is arbitrary and represents the max
+ // allowed number of typed errors
+ if words.DamerauLevenshteinDistance(command, value) < 2 {
+ closestCommands = append(closestCommands, value)
+ }
+ }
+
+ return closestCommands
+ }
+
+ // Register all commands.
+ registerCommand(serverCmd)
+ registerCommand(fmtGenCmd)
+
+ // Set up app.
+ cli.HelpFlag = cli.BoolFlag{
+ Name: "help, h",
+ Usage: "show help",
+ }
+ cli.VersionPrinter = printMinIOVersion
+
+ app := cli.NewApp()
+ app.Name = name
+ app.Author = "MinIO, Inc."
+ app.Version = ReleaseTag
+ app.Usage = "High Performance Object Storage"
+ app.Description = `Build high performance data infrastructure for machine learning, analytics and application data workloads with MinIO`
+ app.Flags = GlobalFlags
+ app.HideHelpCommand = true // Hide `help, h` command, we already have `minio --help`.
+ app.Commands = commands
+ app.CustomAppHelpTemplate = minioHelpTemplate
+ app.CommandNotFound = func(ctx *cli.Context, command string) {
+ console.Printf("‘%s’ is not a minio sub-command. See ‘minio --help’.\n", command)
+ closestCommands := findClosestCommands(command)
+ if len(closestCommands) > 0 {
+ console.Println()
+ console.Println("Did you mean one of these?")
+ for _, cmd := range closestCommands {
+ console.Printf("\t‘%s’\n", cmd)
+ }
+ }
+
+ os.Exit(1)
+ }
+
+ return app
+}
+
+func startupBanner(banner io.Writer) {
+ CopyrightYear = strconv.Itoa(time.Now().Year())
+ fmt.Fprintln(banner, color.Blue("Copyright:")+color.Bold(" 2015-%s MinIO, Inc.", CopyrightYear))
+ fmt.Fprintln(banner, color.Blue("License:")+color.Bold(" "+MinioLicense))
+ fmt.Fprintln(banner, color.Blue("Version:")+color.Bold(" %s (%s %s/%s)", ReleaseTag, runtime.Version(), runtime.GOOS, runtime.GOARCH))
+}
+
+func versionBanner(c *cli.Context) io.Reader {
+ banner := &strings.Builder{}
+ fmt.Fprintln(banner, color.Bold("%s version %s (commit-id=%s)", c.App.Name, c.App.Version, CommitID))
+ fmt.Fprintln(banner, color.Blue("Runtime:")+color.Bold(" %s %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH))
+ fmt.Fprintln(banner, color.Blue("License:")+color.Bold(" GNU AGPLv3 - https://www.gnu.org/licenses/agpl-3.0.html"))
+ fmt.Fprintln(banner, color.Blue("Copyright:")+color.Bold(" 2015-%s MinIO, Inc.", CopyrightYear))
+ return strings.NewReader(banner.String())
+}
+
+func printMinIOVersion(c *cli.Context) {
+ io.Copy(c.App.Writer, versionBanner(c))
+}
+
+var debugNoExit = env.Get("_MINIO_DEBUG_NO_EXIT", "") != ""
+
+// Main main for minio server.
+func Main(args []string) {
+ // Set the minio app name.
+ appName := filepath.Base(args[0])
+
+ if debugNoExit {
+ freeze := func(_ int) {
+ // Infinite blocking op
+ <-make(chan struct{})
+ }
+
+ // Override the logger os.Exit()
+ logger.ExitFunc = freeze
+
+ defer func() {
+ if err := recover(); err != nil {
+ fmt.Println("panic:", err)
+ fmt.Println("")
+ fmt.Println(string(debug.Stack()))
+ }
+ freeze(-1)
+ }()
+ }
+
+ // Run the app - exit on error.
+ if err := newApp(appName).Run(args); err != nil {
+ os.Exit(1) //nolint:gocritic
+ }
+}
diff --git a/cmd/metacache-bucket.go b/cmd/metacache-bucket.go
new file mode 100644
index 00000000..4df23d82
--- /dev/null
+++ b/cmd/metacache-bucket.go
@@ -0,0 +1,258 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "errors"
+ "maps"
+ "runtime/debug"
+ "sort"
+ "sync"
+ "time"
+
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/console"
+)
+
+// a bucketMetacache keeps track of all caches generated
+// for a bucket.
+type bucketMetacache struct {
+ // Name of bucket
+ bucket string
+
+ // caches indexed by id.
+ caches map[string]metacache
+ // cache ids indexed by root paths
+ cachesRoot map[string][]string `msg:"-"`
+
+ // Internal state
+ mu sync.RWMutex `msg:"-"`
+ updated bool `msg:"-"`
+}
+
+type deleteAllStorager interface {
+ deleteAll(ctx context.Context, bucket, prefix string)
+}
+
+// newBucketMetacache creates a new bucketMetacache.
+// Optionally remove all existing caches.
+func newBucketMetacache(bucket string, cleanup bool) *bucketMetacache {
+ if cleanup {
+ // Recursively delete all caches.
+ objAPI := newObjectLayerFn()
+ if objAPI != nil {
+ ez, ok := objAPI.(deleteAllStorager)
+ if ok {
+ ctx := context.Background()
+ ez.deleteAll(ctx, minioMetaBucket, metacachePrefixForID(bucket, slashSeparator))
+ }
+ }
+ }
+ return &bucketMetacache{
+ bucket: bucket,
+ caches: make(map[string]metacache, 10),
+ cachesRoot: make(map[string][]string, 10),
+ }
+}
+
+func (b *bucketMetacache) debugf(format string, data ...any) {
+ if serverDebugLog {
+ console.Debugf(format+"\n", data...)
+ }
+}
+
+// findCache will attempt to find a matching cache for the provided options.
+// If a cache with the same ID exists already it will be returned.
+// If none can be found a new is created with the provided ID.
+func (b *bucketMetacache) findCache(o listPathOptions) metacache {
+ if b == nil {
+ logger.Info("bucketMetacache.findCache: nil cache for bucket %s", o.Bucket)
+ return metacache{}
+ }
+
+ if o.Bucket != b.bucket {
+ logger.Info("bucketMetacache.findCache: bucket %s does not match this bucket %s", o.Bucket, b.bucket)
+ debug.PrintStack()
+ return metacache{}
+ }
+
+ // Grab a write lock, since we create one if we cannot find one.
+ b.mu.Lock()
+ defer b.mu.Unlock()
+
+ // Check if exists already.
+ if c, ok := b.caches[o.ID]; ok {
+ c.lastHandout = time.Now()
+ b.caches[o.ID] = c
+ b.debugf("returning existing %v", o.ID)
+ return c
+ }
+
+ if !o.Create {
+ return metacache{
+ id: o.ID,
+ bucket: o.Bucket,
+ status: scanStateNone,
+ }
+ }
+
+ // Create new and add.
+ best := o.newMetacache()
+ b.caches[o.ID] = best
+ b.cachesRoot[best.root] = append(b.cachesRoot[best.root], best.id)
+ b.updated = true
+ b.debugf("returning new cache %s, bucket: %v", best.id, best.bucket)
+ return best
+}
+
+// cleanup removes redundant and outdated entries.
+func (b *bucketMetacache) cleanup() {
+ // Entries to remove.
+ remove := make(map[string]struct{})
+
+ // Test on a copy
+ // cleanup is the only one deleting caches.
+ caches, _ := b.cloneCaches()
+
+ for id, cache := range caches {
+ if !cache.worthKeeping() {
+ b.debugf("cache %s not worth keeping", id)
+ remove[id] = struct{}{}
+ continue
+ }
+ if cache.id != id {
+ logger.Info("cache ID mismatch %s != %s", id, cache.id)
+ remove[id] = struct{}{}
+ continue
+ }
+ if cache.bucket != b.bucket {
+ logger.Info("cache bucket mismatch %s != %s", b.bucket, cache.bucket)
+ remove[id] = struct{}{}
+ continue
+ }
+ }
+
+ // If above limit, remove the caches with the oldest handout time.
+ if len(caches)-len(remove) > metacacheMaxEntries {
+ remainCaches := make([]metacache, 0, len(caches)-len(remove))
+ for id, cache := range caches {
+ if _, ok := remove[id]; ok {
+ continue
+ }
+ remainCaches = append(remainCaches, cache)
+ }
+ if len(remainCaches) > metacacheMaxEntries {
+ // Sort oldest last...
+ sort.Slice(remainCaches, func(i, j int) bool {
+ return remainCaches[i].lastHandout.Before(remainCaches[j].lastHandout)
+ })
+ // Keep first metacacheMaxEntries...
+ for _, cache := range remainCaches[metacacheMaxEntries:] {
+ if time.Since(cache.lastHandout) > metacacheMaxClientWait {
+ remove[cache.id] = struct{}{}
+ }
+ }
+ }
+ }
+
+ for id := range remove {
+ b.deleteCache(id)
+ }
+}
+
+// updateCacheEntry will update a cache.
+// Returns the updated status.
+func (b *bucketMetacache) updateCacheEntry(update metacache) (metacache, error) {
+ b.mu.Lock()
+ defer b.mu.Unlock()
+ existing, ok := b.caches[update.id]
+ if !ok {
+ return update, errFileNotFound
+ }
+ existing.update(update)
+ b.caches[update.id] = existing
+ b.updated = true
+ return existing, nil
+}
+
+// cloneCaches will return a clone of all current caches.
+func (b *bucketMetacache) cloneCaches() (map[string]metacache, map[string][]string) {
+ b.mu.RLock()
+ defer b.mu.RUnlock()
+ dst := make(map[string]metacache, len(b.caches))
+ maps.Copy(dst, b.caches)
+ // Copy indexes
+ dst2 := make(map[string][]string, len(b.cachesRoot))
+ for k, v := range b.cachesRoot {
+ tmp := make([]string, len(v))
+ copy(tmp, v)
+ dst2[k] = tmp
+ }
+
+ return dst, dst2
+}
+
+// deleteAll will delete all on disk data for ALL caches.
+// Deletes are performed concurrently.
+func (b *bucketMetacache) deleteAll() {
+ ctx := context.Background()
+
+ objAPI := newObjectLayerFn()
+ if objAPI == nil {
+ return
+ }
+
+ ez, ok := objAPI.(deleteAllStorager)
+ if !ok {
+ bugLogIf(ctx, errors.New("bucketMetacache: expected objAPI to be 'deleteAllStorager'"))
+ return
+ }
+
+ b.mu.Lock()
+ defer b.mu.Unlock()
+
+ b.updated = true
+ // Delete all.
+ ez.deleteAll(ctx, minioMetaBucket, metacachePrefixForID(b.bucket, slashSeparator))
+ b.caches = make(map[string]metacache, 10)
+ b.cachesRoot = make(map[string][]string, 10)
+}
+
+// deleteCache will delete a specific cache and all files related to it across the cluster.
+func (b *bucketMetacache) deleteCache(id string) {
+ b.mu.Lock()
+ c, ok := b.caches[id]
+ if ok {
+ // Delete from root map.
+ list := b.cachesRoot[c.root]
+ for i, lid := range list {
+ if id == lid {
+ list = append(list[:i], list[i+1:]...)
+ break
+ }
+ }
+ b.cachesRoot[c.root] = list
+ delete(b.caches, id)
+ b.updated = true
+ }
+ b.mu.Unlock()
+ if ok {
+ c.delete(context.Background())
+ }
+}
diff --git a/cmd/metacache-bucket_test.go b/cmd/metacache-bucket_test.go
new file mode 100644
index 00000000..768d7853
--- /dev/null
+++ b/cmd/metacache-bucket_test.go
@@ -0,0 +1,68 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "fmt"
+ "testing"
+)
+
+func Benchmark_bucketMetacache_findCache(b *testing.B) {
+ bm := newBucketMetacache("", false)
+ const elements = 50000
+ const paths = 100
+ if elements%paths != 0 {
+ b.Fatal("elements must be divisible by the number of paths")
+ }
+ var pathNames [paths]string
+ for i := range pathNames[:] {
+ pathNames[i] = fmt.Sprintf("prefix/%d", i)
+ }
+ for i := range elements {
+ bm.findCache(listPathOptions{
+ ID: mustGetUUID(),
+ Bucket: "",
+ BaseDir: pathNames[i%paths],
+ Prefix: "",
+ FilterPrefix: "",
+ Marker: "",
+ Limit: 0,
+ AskDisks: "strict",
+ Recursive: false,
+ Separator: slashSeparator,
+ Create: true,
+ })
+ }
+ b.ReportAllocs()
+
+ for i := 0; b.Loop(); i++ {
+ bm.findCache(listPathOptions{
+ ID: mustGetUUID(),
+ Bucket: "",
+ BaseDir: pathNames[i%paths],
+ Prefix: "",
+ FilterPrefix: "",
+ Marker: "",
+ Limit: 0,
+ AskDisks: "strict",
+ Recursive: false,
+ Separator: slashSeparator,
+ Create: true,
+ })
+ }
+}
diff --git a/cmd/metacache-entries.go b/cmd/metacache-entries.go
new file mode 100644
index 00000000..69c5e835
--- /dev/null
+++ b/cmd/metacache-entries.go
@@ -0,0 +1,976 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "os"
+ "path"
+ "sort"
+ "strings"
+
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/pkg/v3/console"
+)
+
+// metaCacheEntry is an object or a directory within an unknown bucket.
+type metaCacheEntry struct {
+ // name is the full name of the object including prefixes
+ name string
+ // Metadata. If none is present it is not an object but only a prefix.
+ // Entries without metadata will only be present in non-recursive scans.
+ metadata []byte
+
+ // cached contains the metadata if decoded.
+ cached *xlMetaV2
+
+ // Indicates the entry can be reused and only one reference to metadata is expected.
+ reusable bool
+}
+
+// isDir returns if the entry is representing a prefix directory.
+func (e metaCacheEntry) isDir() bool {
+ return len(e.metadata) == 0 && strings.HasSuffix(e.name, slashSeparator)
+}
+
+// isObject returns if the entry is representing an object.
+func (e metaCacheEntry) isObject() bool {
+ return len(e.metadata) > 0
+}
+
+// isObjectDir returns if the entry is representing an object/
+func (e metaCacheEntry) isObjectDir() bool {
+ return len(e.metadata) > 0 && strings.HasSuffix(e.name, slashSeparator)
+}
+
+// hasPrefix returns whether an entry has a specific prefix
+func (e metaCacheEntry) hasPrefix(s string) bool {
+ return strings.HasPrefix(e.name, s)
+}
+
+// matches returns if the entries have the same versions.
+// If strict is false we allow signatures to mismatch.
+func (e *metaCacheEntry) matches(other *metaCacheEntry, strict bool) (prefer *metaCacheEntry, matches bool) {
+ if e == nil && other == nil {
+ return nil, true
+ }
+ if e == nil {
+ return other, false
+ }
+ if other == nil {
+ return e, false
+ }
+
+ // Name should match...
+ if e.name != other.name {
+ if e.name < other.name {
+ return e, false
+ }
+ return other, false
+ }
+
+ if other.isDir() || e.isDir() {
+ if e.isDir() {
+ return e, other.isDir() == e.isDir()
+ }
+ return other, other.isDir() == e.isDir()
+ }
+ eVers, eErr := e.xlmeta()
+ oVers, oErr := other.xlmeta()
+ if eErr != nil || oErr != nil {
+ return nil, false
+ }
+
+ // check both fileInfo's have same number of versions, if not skip
+ // the `other` entry.
+ if len(eVers.versions) != len(oVers.versions) {
+ eTime := eVers.latestModtime()
+ oTime := oVers.latestModtime()
+ if !eTime.Equal(oTime) {
+ if eTime.After(oTime) {
+ return e, false
+ }
+ return other, false
+ }
+ // Tiebreak on version count.
+ if len(eVers.versions) > len(oVers.versions) {
+ return e, false
+ }
+ return other, false
+ }
+
+ // Check if each version matches...
+ for i, eVer := range eVers.versions {
+ oVer := oVers.versions[i]
+ if eVer.header != oVer.header {
+ if eVer.header.hasEC() != oVer.header.hasEC() {
+ // One version has EC and the other doesn't - may have been written later.
+ // Compare without considering EC.
+ a, b := eVer.header, oVer.header
+ a.EcN, a.EcM = 0, 0
+ b.EcN, b.EcM = 0, 0
+ if a == b {
+ continue
+ }
+ }
+ if !strict && eVer.header.matchesNotStrict(oVer.header) {
+ if prefer == nil {
+ if eVer.header.sortsBefore(oVer.header) {
+ prefer = e
+ } else {
+ prefer = other
+ }
+ }
+ continue
+ }
+ if prefer != nil {
+ return prefer, false
+ }
+
+ if eVer.header.sortsBefore(oVer.header) {
+ return e, false
+ }
+ return other, false
+ }
+ }
+ // If we match, return e
+ if prefer == nil {
+ prefer = e
+ }
+ return prefer, true
+}
+
+// isInDir returns whether the entry is in the dir when considering the separator.
+func (e metaCacheEntry) isInDir(dir, separator string) bool {
+ if len(dir) == 0 {
+ // Root
+ idx := strings.Index(e.name, separator)
+ return idx == -1 || idx == len(e.name)-len(separator)
+ }
+ ext := strings.TrimPrefix(e.name, dir)
+ if len(ext) != len(e.name) {
+ idx := strings.Index(ext, separator)
+ // If separator is not found or is last entry, ok.
+ return idx == -1 || idx == len(ext)-len(separator)
+ }
+ return false
+}
+
+// isLatestDeletemarker returns whether the latest version is a delete marker.
+// If metadata is NOT versioned false will always be returned.
+// If v2 and UNABLE to load metadata true will be returned.
+func (e *metaCacheEntry) isLatestDeletemarker() bool {
+ if e.cached != nil {
+ if len(e.cached.versions) == 0 {
+ return true
+ }
+ return e.cached.versions[0].header.Type == DeleteType
+ }
+ if !isXL2V1Format(e.metadata) {
+ return false
+ }
+ if meta, _, err := isIndexedMetaV2(e.metadata); meta != nil {
+ return meta.IsLatestDeleteMarker()
+ } else if err != nil {
+ return true
+ }
+ // Fall back...
+ xlMeta, err := e.xlmeta()
+ if err != nil || len(xlMeta.versions) == 0 {
+ return true
+ }
+ return xlMeta.versions[0].header.Type == DeleteType
+}
+
+// isAllFreeVersions returns if all objects are free versions.
+// If metadata is NOT versioned false will always be returned.
+// If v2 and UNABLE to load metadata true will be returned.
+func (e *metaCacheEntry) isAllFreeVersions() bool {
+ if e.cached != nil {
+ if len(e.cached.versions) == 0 {
+ return true
+ }
+ for _, v := range e.cached.versions {
+ if !v.header.FreeVersion() {
+ return false
+ }
+ }
+ return true
+ }
+ if !isXL2V1Format(e.metadata) {
+ return false
+ }
+ if meta, _, err := isIndexedMetaV2(e.metadata); meta != nil {
+ return meta.AllHidden(false)
+ } else if err != nil {
+ return true
+ }
+ // Fall back...
+ xlMeta, err := e.xlmeta()
+ if err != nil || len(xlMeta.versions) == 0 {
+ return true
+ }
+ // Check versions..
+ for _, v := range e.cached.versions {
+ if !v.header.FreeVersion() {
+ return false
+ }
+ }
+ return true
+}
+
+// fileInfo returns the decoded metadata.
+// If entry is a directory it is returned as that.
+// If versioned the latest version will be returned.
+func (e *metaCacheEntry) fileInfo(bucket string) (FileInfo, error) {
+ if e.isDir() {
+ return FileInfo{
+ Volume: bucket,
+ Name: e.name,
+ Mode: uint32(os.ModeDir),
+ }, nil
+ }
+ if e.cached != nil {
+ if len(e.cached.versions) == 0 {
+ // This special case is needed to handle xlMeta.versions == 0
+ return FileInfo{
+ Volume: bucket,
+ Name: e.name,
+ Deleted: true,
+ IsLatest: true,
+ ModTime: timeSentinel1970,
+ }, nil
+ }
+ return e.cached.ToFileInfo(bucket, e.name, "", false, true)
+ }
+ return getFileInfo(e.metadata, bucket, e.name, "", fileInfoOpts{})
+}
+
+// xlmeta returns the decoded metadata.
+// This should not be called on directories.
+func (e *metaCacheEntry) xlmeta() (*xlMetaV2, error) {
+ if e.isDir() {
+ return nil, errFileNotFound
+ }
+ if e.cached == nil {
+ if len(e.metadata) == 0 {
+ // only happens if the entry is not found.
+ return nil, errFileNotFound
+ }
+ var xl xlMetaV2
+ err := xl.LoadOrConvert(e.metadata)
+ if err != nil {
+ return nil, err
+ }
+ e.cached = &xl
+ }
+ return e.cached, nil
+}
+
+// fileInfoVersions returns the metadata as FileInfoVersions.
+// If entry is a directory it is returned as that.
+func (e *metaCacheEntry) fileInfoVersions(bucket string) (FileInfoVersions, error) {
+ if e.isDir() {
+ return FileInfoVersions{
+ Volume: bucket,
+ Name: e.name,
+ Versions: []FileInfo{
+ {
+ Volume: bucket,
+ Name: e.name,
+ Mode: uint32(os.ModeDir),
+ },
+ },
+ }, nil
+ }
+ // Too small gains to reuse cache here.
+ return getFileInfoVersions(e.metadata, bucket, e.name, true)
+}
+
+// metaCacheEntries is a slice of metacache entries.
+type metaCacheEntries []metaCacheEntry
+
+// less function for sorting.
+func (m metaCacheEntries) less(i, j int) bool {
+ return m[i].name < m[j].name
+}
+
+// sort entries by name.
+// m is sorted and a sorted metadata object is returned.
+// Changes to m will also be reflected in the returned object.
+func (m metaCacheEntries) sort() metaCacheEntriesSorted {
+ if m.isSorted() {
+ return metaCacheEntriesSorted{o: m}
+ }
+ sort.Slice(m, m.less)
+ return metaCacheEntriesSorted{o: m}
+}
+
+// isSorted returns whether the objects are sorted.
+// This is usually orders of magnitude faster than actually sorting.
+func (m metaCacheEntries) isSorted() bool {
+ return sort.SliceIsSorted(m, m.less)
+}
+
+// shallowClone will create a shallow clone of the array objects,
+// but object metadata will not be cloned.
+func (m metaCacheEntries) shallowClone() metaCacheEntries {
+ dst := make(metaCacheEntries, len(m))
+ copy(dst, m)
+ return dst
+}
+
+type metadataResolutionParams struct {
+ dirQuorum int // Number if disks needed for a directory to 'exist'.
+ objQuorum int // Number of disks needed for an object to 'exist'.
+
+ // An optimization request only an 'n' amount of versions from xl.meta
+ // to avoid resolving all versions to figure out the latest 'version'
+ // for ListObjects, ListObjectsV2
+ requestedVersions int
+
+ bucket string // Name of the bucket. Used for generating cached fileinfo.
+ strict bool // Versions must match exactly, including all metadata.
+
+ // Reusable slice for resolution
+ candidates [][]xlMetaV2ShallowVersion
+}
+
+// resolve multiple entries.
+// entries are resolved by majority, then if tied by mod-time and versions.
+// Names must match on all entries in m.
+func (m metaCacheEntries) resolve(r *metadataResolutionParams) (selected *metaCacheEntry, ok bool) {
+ if len(m) == 0 {
+ return nil, false
+ }
+
+ dirExists := 0
+ if cap(r.candidates) < len(m) {
+ r.candidates = make([][]xlMetaV2ShallowVersion, 0, len(m))
+ }
+ r.candidates = r.candidates[:0]
+ objsAgree := 0
+ objsValid := 0
+ for i := range m {
+ entry := &m[i]
+ // Empty entry
+ if entry.name == "" {
+ continue
+ }
+
+ if entry.isDir() {
+ dirExists++
+ selected = entry
+ continue
+ }
+
+ // Get new entry metadata,
+ // shallow decode.
+ xl, err := entry.xlmeta()
+ if err != nil {
+ continue
+ }
+ objsValid++
+
+ // Add all valid to candidates.
+ r.candidates = append(r.candidates, xl.versions)
+
+ // We select the first object we find as a candidate and see if all match that.
+ // This is to quickly identify if all agree.
+ if selected == nil {
+ selected = entry
+ objsAgree = 1
+ continue
+ }
+ // Names match, check meta...
+ if prefer, ok := entry.matches(selected, r.strict); ok {
+ selected = prefer
+ objsAgree++
+ continue
+ }
+ }
+
+ // Return dir entries, if enough...
+ if selected != nil && selected.isDir() && dirExists >= r.dirQuorum {
+ return selected, true
+ }
+
+ // If we would never be able to reach read quorum.
+ if objsValid < r.objQuorum {
+ return nil, false
+ }
+
+ // If all objects agree.
+ if selected != nil && objsAgree == objsValid {
+ return selected, true
+ }
+
+ // If cached is nil we shall skip the entry.
+ if selected.cached == nil {
+ return nil, false
+ }
+
+ // Merge if we have disagreement.
+ // Create a new merged result.
+ selected = &metaCacheEntry{
+ name: selected.name,
+ reusable: true,
+ cached: &xlMetaV2{metaV: selected.cached.metaV},
+ }
+ selected.cached.versions = mergeXLV2Versions(r.objQuorum, r.strict, r.requestedVersions, r.candidates...)
+ if len(selected.cached.versions) == 0 {
+ return nil, false
+ }
+
+ // Reserialize
+ var err error
+ selected.metadata, err = selected.cached.AppendTo(metaDataPoolGet())
+ if err != nil {
+ bugLogIf(context.Background(), err)
+ return nil, false
+ }
+ return selected, true
+}
+
+// firstFound returns the first found and the number of set entries.
+func (m metaCacheEntries) firstFound() (first *metaCacheEntry, n int) {
+ for i, entry := range m {
+ if entry.name != "" {
+ n++
+ if first == nil {
+ first = &m[i]
+ }
+ }
+ }
+ return first, n
+}
+
+// names will return all names in order.
+// Since this allocates it should not be used in critical functions.
+func (m metaCacheEntries) names() []string {
+ res := make([]string, 0, len(m))
+ for _, obj := range m {
+ res = append(res, obj.name)
+ }
+ return res
+}
+
+// metaCacheEntriesSorted contains metacache entries that are sorted.
+type metaCacheEntriesSorted struct {
+ o metaCacheEntries
+ // list id is not serialized
+ listID string
+ // Reuse buffers
+ reuse bool
+ // Contain the last skipped object after an ILM expiry evaluation
+ lastSkippedEntry string
+}
+
+// shallowClone will create a shallow clone of the array objects,
+// but object metadata will not be cloned.
+func (m metaCacheEntriesSorted) shallowClone() metaCacheEntriesSorted {
+ // We have value receiver so we already have a copy.
+ m.o = m.o.shallowClone()
+ return m
+}
+
+// fileInfoVersions converts the metadata to FileInfoVersions where possible.
+// Metadata that cannot be decoded is skipped.
+func (m *metaCacheEntriesSorted) fileInfoVersions(bucket, prefix, delimiter, afterV string) (versions []ObjectInfo) {
+ versions = make([]ObjectInfo, 0, m.len())
+ prevPrefix := ""
+ vcfg, _ := globalBucketVersioningSys.Get(bucket)
+
+ for _, entry := range m.o {
+ if entry.isObject() {
+ if delimiter != "" {
+ idx := strings.Index(strings.TrimPrefix(entry.name, prefix), delimiter)
+ if idx >= 0 {
+ idx = len(prefix) + idx + len(delimiter)
+ currPrefix := entry.name[:idx]
+ if currPrefix == prevPrefix {
+ continue
+ }
+ prevPrefix = currPrefix
+ versions = append(versions, ObjectInfo{
+ IsDir: true,
+ Bucket: bucket,
+ Name: currPrefix,
+ })
+ continue
+ }
+ }
+
+ fiv, err := entry.fileInfoVersions(bucket)
+ if err != nil {
+ continue
+ }
+
+ fiVersions := fiv.Versions
+ if afterV != "" {
+ vidMarkerIdx := fiv.findVersionIndex(afterV)
+ if vidMarkerIdx >= 0 {
+ fiVersions = fiVersions[vidMarkerIdx+1:]
+ }
+ afterV = ""
+ }
+
+ for _, version := range fiVersions {
+ if !version.VersionPurgeStatus().Empty() {
+ continue
+ }
+ versioned := vcfg != nil && vcfg.Versioned(entry.name)
+ versions = append(versions, version.ToObjectInfo(bucket, entry.name, versioned))
+ }
+
+ continue
+ }
+
+ if entry.isDir() {
+ if delimiter == "" {
+ continue
+ }
+ idx := strings.Index(strings.TrimPrefix(entry.name, prefix), delimiter)
+ if idx < 0 {
+ continue
+ }
+ idx = len(prefix) + idx + len(delimiter)
+ currPrefix := entry.name[:idx]
+ if currPrefix == prevPrefix {
+ continue
+ }
+ prevPrefix = currPrefix
+ versions = append(versions, ObjectInfo{
+ IsDir: true,
+ Bucket: bucket,
+ Name: currPrefix,
+ })
+ }
+ }
+
+ return versions
+}
+
+// fileInfos converts the metadata to ObjectInfo where possible.
+// Metadata that cannot be decoded is skipped.
+func (m *metaCacheEntriesSorted) fileInfos(bucket, prefix, delimiter string) (objects []ObjectInfo) {
+ objects = make([]ObjectInfo, 0, m.len())
+ prevPrefix := ""
+
+ vcfg, _ := globalBucketVersioningSys.Get(bucket)
+
+ for _, entry := range m.o {
+ if entry.isObject() {
+ if delimiter != "" {
+ idx := strings.Index(strings.TrimPrefix(entry.name, prefix), delimiter)
+ if idx >= 0 {
+ idx = len(prefix) + idx + len(delimiter)
+ currPrefix := entry.name[:idx]
+ if currPrefix == prevPrefix {
+ continue
+ }
+ prevPrefix = currPrefix
+ objects = append(objects, ObjectInfo{
+ IsDir: true,
+ Bucket: bucket,
+ Name: currPrefix,
+ })
+ continue
+ }
+ }
+
+ fi, err := entry.fileInfo(bucket)
+ if err == nil && fi.VersionPurgeStatus().Empty() {
+ versioned := vcfg != nil && vcfg.Versioned(entry.name)
+ objects = append(objects, fi.ToObjectInfo(bucket, entry.name, versioned))
+ }
+ continue
+ }
+ if entry.isDir() {
+ if delimiter == "" {
+ continue
+ }
+ idx := strings.Index(strings.TrimPrefix(entry.name, prefix), delimiter)
+ if idx < 0 {
+ continue
+ }
+ idx = len(prefix) + idx + len(delimiter)
+ currPrefix := entry.name[:idx]
+ if currPrefix == prevPrefix {
+ continue
+ }
+ prevPrefix = currPrefix
+ objects = append(objects, ObjectInfo{
+ IsDir: true,
+ Bucket: bucket,
+ Name: currPrefix,
+ })
+ }
+ }
+
+ return objects
+}
+
+// forwardTo will truncate m so only entries that are s or after is in the list.
+func (m *metaCacheEntriesSorted) forwardTo(s string) {
+ if s == "" {
+ return
+ }
+ idx := sort.Search(len(m.o), func(i int) bool {
+ return m.o[i].name >= s
+ })
+ if m.reuse {
+ for i, entry := range m.o[:idx] {
+ metaDataPoolPut(entry.metadata)
+ m.o[i].metadata = nil
+ }
+ }
+
+ m.o = m.o[idx:]
+}
+
+// forwardPast will truncate m so only entries that are after s is in the list.
+func (m *metaCacheEntriesSorted) forwardPast(s string) {
+ if s == "" {
+ return
+ }
+ idx := sort.Search(len(m.o), func(i int) bool {
+ return m.o[i].name > s
+ })
+ if m.reuse {
+ for i, entry := range m.o[:idx] {
+ metaDataPoolPut(entry.metadata)
+ m.o[i].metadata = nil
+ }
+ }
+ m.o = m.o[idx:]
+}
+
+// mergeEntryChannels will merge entries from in and return them sorted on out.
+// To signify no more results are on an input channel, close it.
+// The output channel will be closed when all inputs are emptied.
+// If file names are equal, compareMeta is called to select which one to choose.
+// The entry not chosen will be discarded.
+// If the context is canceled the function will return the error,
+// otherwise the function will return nil.
+func mergeEntryChannels(ctx context.Context, in []chan metaCacheEntry, out chan<- metaCacheEntry, readQuorum int) error {
+ defer xioutil.SafeClose(out)
+ top := make([]*metaCacheEntry, len(in))
+ nDone := 0
+ ctxDone := ctx.Done()
+
+ // Use simpler forwarder.
+ if len(in) == 1 {
+ for {
+ select {
+ case <-ctxDone:
+ return ctx.Err()
+ case v, ok := <-in[0]:
+ if !ok {
+ return nil
+ }
+ select {
+ case <-ctxDone:
+ return ctx.Err()
+ case out <- v:
+ }
+ }
+ }
+ }
+
+ selectFrom := func(idx int) error {
+ select {
+ case <-ctxDone:
+ return ctx.Err()
+ case entry, ok := <-in[idx]:
+ if !ok {
+ top[idx] = nil
+ nDone++
+ } else {
+ top[idx] = &entry
+ }
+ }
+ return nil
+ }
+ // Populate all...
+ for i := range in {
+ if err := selectFrom(i); err != nil {
+ return err
+ }
+ }
+ last := ""
+ var toMerge []int
+
+ // Choose the best to return.
+ for {
+ if nDone == len(in) {
+ return nil
+ }
+ best := top[0]
+ bestIdx := 0
+ toMerge = toMerge[:0]
+ for i, other := range top[1:] {
+ otherIdx := i + 1
+ if other == nil {
+ continue
+ }
+ if best == nil {
+ best = other
+ bestIdx = otherIdx
+ continue
+ }
+ if path.Clean(best.name) == path.Clean(other.name) {
+ // We may be in a situation where we have a directory and an object with the same name.
+ // In that case we will drop the directory entry.
+ // This should however not be confused with an object with a trailing slash.
+ dirMatches := best.isDir() == other.isDir()
+ suffixMatches := strings.HasSuffix(best.name, slashSeparator) == strings.HasSuffix(other.name, slashSeparator)
+
+ // Simple case. Both are same type with same suffix.
+ if dirMatches && suffixMatches {
+ toMerge = append(toMerge, otherIdx)
+ continue
+ }
+
+ if !dirMatches {
+ // We have an object `name` or 'name/' and a directory `name/`.
+ if other.isDir() {
+ if serverDebugLog {
+ console.Debugln("mergeEntryChannels: discarding directory", other.name, "for object", best.name)
+ }
+ // Discard the directory.
+ if err := selectFrom(otherIdx); err != nil {
+ return err
+ }
+ continue
+ }
+ // Replace directory with object.
+ if serverDebugLog {
+ console.Debugln("mergeEntryChannels: discarding directory", best.name, "for object", other.name)
+ }
+ toMerge = toMerge[:0]
+ best = other
+ bestIdx = otherIdx
+ continue
+ }
+ // Leave it to be resolved. Names are different.
+ }
+ if best.name > other.name {
+ toMerge = toMerge[:0]
+ best = other
+ bestIdx = otherIdx
+ }
+ }
+
+ // Merge any unmerged
+ if len(toMerge) > 0 {
+ versions := make([][]xlMetaV2ShallowVersion, 0, len(toMerge)+1)
+ xl, err := best.xlmeta()
+ if err == nil {
+ versions = append(versions, xl.versions)
+ }
+ for _, idx := range toMerge {
+ other := top[idx]
+ if other == nil {
+ continue
+ }
+ xl2, err := other.xlmeta()
+ if err != nil {
+ if err := selectFrom(idx); err != nil {
+ return err
+ }
+ continue
+ }
+ if xl == nil {
+ // Discard current "best"
+ if err := selectFrom(bestIdx); err != nil {
+ return err
+ }
+ bestIdx = idx
+ best = other
+ xl = xl2
+ } else {
+ // Mark read, unless we added it as new "best".
+ if err := selectFrom(idx); err != nil {
+ return err
+ }
+ }
+ versions = append(versions, xl2.versions)
+ }
+
+ if xl != nil && len(versions) > 0 {
+ // Merge all versions. 'strict' doesn't matter since we only need one.
+ xl.versions = mergeXLV2Versions(readQuorum, true, 0, versions...)
+ if meta, err := xl.AppendTo(metaDataPoolGet()); err == nil {
+ if best.reusable {
+ metaDataPoolPut(best.metadata)
+ }
+ best.metadata = meta
+ best.cached = xl
+ }
+ }
+ toMerge = toMerge[:0]
+ }
+ if best.name > last {
+ select {
+ case <-ctxDone:
+ return ctx.Err()
+ case out <- *best:
+ last = best.name
+ }
+ } else if serverDebugLog {
+ console.Debugln("mergeEntryChannels: discarding duplicate", best.name, "<=", last)
+ }
+ // Replace entry we just sent.
+ if err := selectFrom(bestIdx); err != nil {
+ return err
+ }
+ }
+}
+
+// merge will merge other into m.
+// If the same entries exists in both and metadata matches only one is added,
+// otherwise the entry from m will be placed first.
+// Operation time is expected to be O(n+m).
+func (m *metaCacheEntriesSorted) merge(other metaCacheEntriesSorted, limit int) {
+ merged := make(metaCacheEntries, 0, m.len()+other.len())
+ a := m.entries()
+ b := other.entries()
+ for len(a) > 0 && len(b) > 0 {
+ switch {
+ case a[0].name == b[0].name && bytes.Equal(a[0].metadata, b[0].metadata):
+ // Same, discard one.
+ merged = append(merged, a[0])
+ a = a[1:]
+ b = b[1:]
+ case a[0].name < b[0].name:
+ merged = append(merged, a[0])
+ a = a[1:]
+ default:
+ merged = append(merged, b[0])
+ b = b[1:]
+ }
+ if limit > 0 && len(merged) >= limit {
+ break
+ }
+ }
+ // Append anything left.
+ if limit < 0 || len(merged) < limit {
+ merged = append(merged, a...)
+ merged = append(merged, b...)
+ }
+ m.o = merged
+}
+
+// filterPrefix will filter m to only contain entries with the specified prefix.
+func (m *metaCacheEntriesSorted) filterPrefix(s string) {
+ if s == "" {
+ return
+ }
+ m.forwardTo(s)
+ for i, o := range m.o {
+ if !o.hasPrefix(s) {
+ m.o = m.o[:i]
+ break
+ }
+ }
+}
+
+// filterObjectsOnly will remove prefix directories.
+// Order is preserved, but the underlying slice is modified.
+func (m *metaCacheEntriesSorted) filterObjectsOnly() {
+ dst := m.o[:0]
+ for _, o := range m.o {
+ if !o.isDir() {
+ dst = append(dst, o)
+ }
+ }
+ m.o = dst
+}
+
+// filterPrefixesOnly will remove objects.
+// Order is preserved, but the underlying slice is modified.
+func (m *metaCacheEntriesSorted) filterPrefixesOnly() {
+ dst := m.o[:0]
+ for _, o := range m.o {
+ if o.isDir() {
+ dst = append(dst, o)
+ }
+ }
+ m.o = dst
+}
+
+// filterRecursiveEntries will keep entries only with the prefix that doesn't contain separator.
+// This can be used to remove recursive listings.
+// To return root elements only set prefix to an empty string.
+// Order is preserved, but the underlying slice is modified.
+func (m *metaCacheEntriesSorted) filterRecursiveEntries(prefix, separator string) {
+ dst := m.o[:0]
+ if prefix != "" {
+ m.forwardTo(prefix)
+ for _, o := range m.o {
+ ext := strings.TrimPrefix(o.name, prefix)
+ if len(ext) != len(o.name) {
+ if !strings.Contains(ext, separator) {
+ dst = append(dst, o)
+ }
+ }
+ }
+ } else {
+ // No prefix, simpler
+ for _, o := range m.o {
+ if !strings.Contains(o.name, separator) {
+ dst = append(dst, o)
+ }
+ }
+ }
+ m.o = dst
+}
+
+// truncate the number of entries to maximum n.
+func (m *metaCacheEntriesSorted) truncate(n int) {
+ if m == nil {
+ return
+ }
+ if len(m.o) > n {
+ if m.reuse {
+ for i, entry := range m.o[n:] {
+ metaDataPoolPut(entry.metadata)
+ m.o[n+i].metadata = nil
+ }
+ }
+ m.o = m.o[:n]
+ }
+}
+
+// len returns the number of objects and prefix dirs in m.
+func (m *metaCacheEntriesSorted) len() int {
+ if m == nil {
+ return 0
+ }
+ return len(m.o)
+}
+
+// entries returns the underlying objects as is currently represented.
+func (m *metaCacheEntriesSorted) entries() metaCacheEntries {
+ if m == nil {
+ return nil
+ }
+ return m.o
+}
diff --git a/cmd/metacache-entries_test.go b/cmd/metacache-entries_test.go
new file mode 100644
index 00000000..75af4d0f
--- /dev/null
+++ b/cmd/metacache-entries_test.go
@@ -0,0 +1,659 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "fmt"
+ "math/rand"
+ "reflect"
+ "sort"
+ "testing"
+ "time"
+)
+
+func Test_metaCacheEntries_sort(t *testing.T) {
+ entries := loadMetacacheSampleEntries(t)
+
+ o := entries.entries()
+ if !o.isSorted() {
+ t.Fatal("Expected sorted objects")
+ }
+
+ // Swap first and last
+ o[0], o[len(o)-1] = o[len(o)-1], o[0]
+ if o.isSorted() {
+ t.Fatal("Expected unsorted objects")
+ }
+
+ sorted := o.sort()
+ if !o.isSorted() {
+ t.Fatal("Expected sorted o objects")
+ }
+ if !sorted.entries().isSorted() {
+ t.Fatal("Expected sorted wrapped objects")
+ }
+ want := loadMetacacheSampleNames
+ for i, got := range o {
+ if got.name != want[i] {
+ t.Errorf("entry %d, want %q, got %q", i, want[i], got.name)
+ }
+ }
+}
+
+func Test_metaCacheEntries_forwardTo(t *testing.T) {
+ org := loadMetacacheSampleEntries(t)
+ entries := org
+ want := []string{"src/compress/zlib/reader_test.go", "src/compress/zlib/writer.go", "src/compress/zlib/writer_test.go"}
+ entries.forwardTo("src/compress/zlib/reader_test.go")
+ got := entries.entries().names()
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("got unexpected result: %#v", got)
+ }
+
+ // Try with prefix
+ entries = org
+ entries.forwardTo("src/compress/zlib/reader_t")
+ got = entries.entries().names()
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("got unexpected result: %#v", got)
+ }
+}
+
+func Test_metaCacheEntries_merge(t *testing.T) {
+ org := loadMetacacheSampleEntries(t)
+ a, b := org.shallowClone(), org.shallowClone()
+ be := b.entries()
+ for i := range be {
+ // Modify b so it isn't deduplicated.
+ be[i].metadata = []byte("something-else")
+ }
+ // Merge b into a
+ a.merge(b, -1)
+ //nolint:gocritic
+ want := append(loadMetacacheSampleNames, loadMetacacheSampleNames...)
+ sort.Strings(want)
+ got := a.entries().names()
+ if len(got) != len(want) {
+ t.Errorf("unexpected count, want %v, got %v", len(want), len(got))
+ }
+
+ for i, name := range got {
+ if want[i] != name {
+ t.Errorf("unexpected name, want %q, got %q", want[i], name)
+ }
+ }
+}
+
+func Test_metaCacheEntries_filterObjects(t *testing.T) {
+ data := loadMetacacheSampleEntries(t)
+ data.filterObjectsOnly()
+ got := data.entries().names()
+ want := []string{"src/compress/bzip2/bit_reader.go", "src/compress/bzip2/bzip2.go", "src/compress/bzip2/bzip2_test.go", "src/compress/bzip2/huffman.go", "src/compress/bzip2/move_to_front.go", "src/compress/bzip2/testdata/Isaac.Newton-Opticks.txt.bz2", "src/compress/bzip2/testdata/e.txt.bz2", "src/compress/bzip2/testdata/fail-issue5747.bz2", "src/compress/bzip2/testdata/pass-random1.bin", "src/compress/bzip2/testdata/pass-random1.bz2", "src/compress/bzip2/testdata/pass-random2.bin", "src/compress/bzip2/testdata/pass-random2.bz2", "src/compress/bzip2/testdata/pass-sawtooth.bz2", "src/compress/bzip2/testdata/random.data.bz2", "src/compress/flate/deflate.go", "src/compress/flate/deflate_test.go", "src/compress/flate/deflatefast.go", "src/compress/flate/dict_decoder.go", "src/compress/flate/dict_decoder_test.go", "src/compress/flate/example_test.go", "src/compress/flate/flate_test.go", "src/compress/flate/huffman_bit_writer.go", "src/compress/flate/huffman_bit_writer_test.go", "src/compress/flate/huffman_code.go", "src/compress/flate/inflate.go", "src/compress/flate/inflate_test.go", "src/compress/flate/reader_test.go", "src/compress/flate/testdata/huffman-null-max.dyn.expect", "src/compress/flate/testdata/huffman-null-max.dyn.expect-noinput", "src/compress/flate/testdata/huffman-null-max.golden", "src/compress/flate/testdata/huffman-null-max.in", "src/compress/flate/testdata/huffman-null-max.wb.expect", "src/compress/flate/testdata/huffman-null-max.wb.expect-noinput", "src/compress/flate/testdata/huffman-pi.dyn.expect", "src/compress/flate/testdata/huffman-pi.dyn.expect-noinput", "src/compress/flate/testdata/huffman-pi.golden", "src/compress/flate/testdata/huffman-pi.in", "src/compress/flate/testdata/huffman-pi.wb.expect", "src/compress/flate/testdata/huffman-pi.wb.expect-noinput", "src/compress/flate/testdata/huffman-rand-1k.dyn.expect", "src/compress/flate/testdata/huffman-rand-1k.dyn.expect-noinput", "src/compress/flate/testdata/huffman-rand-1k.golden", "src/compress/flate/testdata/huffman-rand-1k.in", "src/compress/flate/testdata/huffman-rand-1k.wb.expect", "src/compress/flate/testdata/huffman-rand-1k.wb.expect-noinput", "src/compress/flate/testdata/huffman-rand-limit.dyn.expect", "src/compress/flate/testdata/huffman-rand-limit.dyn.expect-noinput", "src/compress/flate/testdata/huffman-rand-limit.golden", "src/compress/flate/testdata/huffman-rand-limit.in", "src/compress/flate/testdata/huffman-rand-limit.wb.expect", "src/compress/flate/testdata/huffman-rand-limit.wb.expect-noinput", "src/compress/flate/testdata/huffman-rand-max.golden", "src/compress/flate/testdata/huffman-rand-max.in", "src/compress/flate/testdata/huffman-shifts.dyn.expect", "src/compress/flate/testdata/huffman-shifts.dyn.expect-noinput", "src/compress/flate/testdata/huffman-shifts.golden", "src/compress/flate/testdata/huffman-shifts.in", "src/compress/flate/testdata/huffman-shifts.wb.expect", "src/compress/flate/testdata/huffman-shifts.wb.expect-noinput", "src/compress/flate/testdata/huffman-text-shift.dyn.expect", "src/compress/flate/testdata/huffman-text-shift.dyn.expect-noinput", "src/compress/flate/testdata/huffman-text-shift.golden", "src/compress/flate/testdata/huffman-text-shift.in", "src/compress/flate/testdata/huffman-text-shift.wb.expect", "src/compress/flate/testdata/huffman-text-shift.wb.expect-noinput", "src/compress/flate/testdata/huffman-text.dyn.expect", "src/compress/flate/testdata/huffman-text.dyn.expect-noinput", "src/compress/flate/testdata/huffman-text.golden", "src/compress/flate/testdata/huffman-text.in", "src/compress/flate/testdata/huffman-text.wb.expect", "src/compress/flate/testdata/huffman-text.wb.expect-noinput", "src/compress/flate/testdata/huffman-zero.dyn.expect", "src/compress/flate/testdata/huffman-zero.dyn.expect-noinput", "src/compress/flate/testdata/huffman-zero.golden", "src/compress/flate/testdata/huffman-zero.in", "src/compress/flate/testdata/huffman-zero.wb.expect", "src/compress/flate/testdata/huffman-zero.wb.expect-noinput", "src/compress/flate/testdata/null-long-match.dyn.expect-noinput", "src/compress/flate/testdata/null-long-match.wb.expect-noinput", "src/compress/flate/token.go", "src/compress/flate/writer_test.go", "src/compress/gzip/example_test.go", "src/compress/gzip/gunzip.go", "src/compress/gzip/gunzip_test.go", "src/compress/gzip/gzip.go", "src/compress/gzip/gzip_test.go", "src/compress/gzip/issue14937_test.go", "src/compress/gzip/testdata/issue6550.gz.base64", "src/compress/lzw/reader.go", "src/compress/lzw/reader_test.go", "src/compress/lzw/writer.go", "src/compress/lzw/writer_test.go", "src/compress/testdata/e.txt", "src/compress/testdata/gettysburg.txt", "src/compress/testdata/pi.txt", "src/compress/zlib/example_test.go", "src/compress/zlib/reader.go", "src/compress/zlib/reader_test.go", "src/compress/zlib/writer.go", "src/compress/zlib/writer_test.go"}
+ if !reflect.DeepEqual(want, got) {
+ t.Errorf("got unexpected result: %#v", got)
+ }
+}
+
+func Test_metaCacheEntries_filterPrefixes(t *testing.T) {
+ data := loadMetacacheSampleEntries(t)
+ data.filterPrefixesOnly()
+ got := data.entries().names()
+ want := []string{"src/compress/bzip2/", "src/compress/bzip2/testdata/", "src/compress/flate/", "src/compress/flate/testdata/", "src/compress/gzip/", "src/compress/gzip/testdata/", "src/compress/lzw/", "src/compress/testdata/", "src/compress/zlib/"}
+ if !reflect.DeepEqual(want, got) {
+ t.Errorf("got unexpected result: %#v", got)
+ }
+}
+
+func Test_metaCacheEntries_filterRecursive(t *testing.T) {
+ data := loadMetacacheSampleEntries(t)
+ data.filterRecursiveEntries("src/compress/bzip2/", slashSeparator)
+ got := data.entries().names()
+ want := []string{"src/compress/bzip2/", "src/compress/bzip2/bit_reader.go", "src/compress/bzip2/bzip2.go", "src/compress/bzip2/bzip2_test.go", "src/compress/bzip2/huffman.go", "src/compress/bzip2/move_to_front.go"}
+ if !reflect.DeepEqual(want, got) {
+ t.Errorf("got unexpected result: %#v", got)
+ }
+}
+
+func Test_metaCacheEntries_filterRecursiveRoot(t *testing.T) {
+ data := loadMetacacheSampleEntries(t)
+ data.filterRecursiveEntries("", slashSeparator)
+ got := data.entries().names()
+ want := []string{}
+ if !reflect.DeepEqual(want, got) {
+ t.Errorf("got unexpected result: %#v", got)
+ }
+}
+
+func Test_metaCacheEntries_filterRecursiveRootSep(t *testing.T) {
+ data := loadMetacacheSampleEntries(t)
+ // This will remove anything with "bzip2/" in the path since it is separator
+ data.filterRecursiveEntries("", "bzip2/")
+ got := data.entries().names()
+ want := []string{"src/compress/flate/", "src/compress/flate/deflate.go", "src/compress/flate/deflate_test.go", "src/compress/flate/deflatefast.go", "src/compress/flate/dict_decoder.go", "src/compress/flate/dict_decoder_test.go", "src/compress/flate/example_test.go", "src/compress/flate/flate_test.go", "src/compress/flate/huffman_bit_writer.go", "src/compress/flate/huffman_bit_writer_test.go", "src/compress/flate/huffman_code.go", "src/compress/flate/inflate.go", "src/compress/flate/inflate_test.go", "src/compress/flate/reader_test.go", "src/compress/flate/testdata/", "src/compress/flate/testdata/huffman-null-max.dyn.expect", "src/compress/flate/testdata/huffman-null-max.dyn.expect-noinput", "src/compress/flate/testdata/huffman-null-max.golden", "src/compress/flate/testdata/huffman-null-max.in", "src/compress/flate/testdata/huffman-null-max.wb.expect", "src/compress/flate/testdata/huffman-null-max.wb.expect-noinput", "src/compress/flate/testdata/huffman-pi.dyn.expect", "src/compress/flate/testdata/huffman-pi.dyn.expect-noinput", "src/compress/flate/testdata/huffman-pi.golden", "src/compress/flate/testdata/huffman-pi.in", "src/compress/flate/testdata/huffman-pi.wb.expect", "src/compress/flate/testdata/huffman-pi.wb.expect-noinput", "src/compress/flate/testdata/huffman-rand-1k.dyn.expect", "src/compress/flate/testdata/huffman-rand-1k.dyn.expect-noinput", "src/compress/flate/testdata/huffman-rand-1k.golden", "src/compress/flate/testdata/huffman-rand-1k.in", "src/compress/flate/testdata/huffman-rand-1k.wb.expect", "src/compress/flate/testdata/huffman-rand-1k.wb.expect-noinput", "src/compress/flate/testdata/huffman-rand-limit.dyn.expect", "src/compress/flate/testdata/huffman-rand-limit.dyn.expect-noinput", "src/compress/flate/testdata/huffman-rand-limit.golden", "src/compress/flate/testdata/huffman-rand-limit.in", "src/compress/flate/testdata/huffman-rand-limit.wb.expect", "src/compress/flate/testdata/huffman-rand-limit.wb.expect-noinput", "src/compress/flate/testdata/huffman-rand-max.golden", "src/compress/flate/testdata/huffman-rand-max.in", "src/compress/flate/testdata/huffman-shifts.dyn.expect", "src/compress/flate/testdata/huffman-shifts.dyn.expect-noinput", "src/compress/flate/testdata/huffman-shifts.golden", "src/compress/flate/testdata/huffman-shifts.in", "src/compress/flate/testdata/huffman-shifts.wb.expect", "src/compress/flate/testdata/huffman-shifts.wb.expect-noinput", "src/compress/flate/testdata/huffman-text-shift.dyn.expect", "src/compress/flate/testdata/huffman-text-shift.dyn.expect-noinput", "src/compress/flate/testdata/huffman-text-shift.golden", "src/compress/flate/testdata/huffman-text-shift.in", "src/compress/flate/testdata/huffman-text-shift.wb.expect", "src/compress/flate/testdata/huffman-text-shift.wb.expect-noinput", "src/compress/flate/testdata/huffman-text.dyn.expect", "src/compress/flate/testdata/huffman-text.dyn.expect-noinput", "src/compress/flate/testdata/huffman-text.golden", "src/compress/flate/testdata/huffman-text.in", "src/compress/flate/testdata/huffman-text.wb.expect", "src/compress/flate/testdata/huffman-text.wb.expect-noinput", "src/compress/flate/testdata/huffman-zero.dyn.expect", "src/compress/flate/testdata/huffman-zero.dyn.expect-noinput", "src/compress/flate/testdata/huffman-zero.golden", "src/compress/flate/testdata/huffman-zero.in", "src/compress/flate/testdata/huffman-zero.wb.expect", "src/compress/flate/testdata/huffman-zero.wb.expect-noinput", "src/compress/flate/testdata/null-long-match.dyn.expect-noinput", "src/compress/flate/testdata/null-long-match.wb.expect-noinput", "src/compress/flate/token.go", "src/compress/flate/writer_test.go", "src/compress/gzip/", "src/compress/gzip/example_test.go", "src/compress/gzip/gunzip.go", "src/compress/gzip/gunzip_test.go", "src/compress/gzip/gzip.go", "src/compress/gzip/gzip_test.go", "src/compress/gzip/issue14937_test.go", "src/compress/gzip/testdata/", "src/compress/gzip/testdata/issue6550.gz.base64", "src/compress/lzw/", "src/compress/lzw/reader.go", "src/compress/lzw/reader_test.go", "src/compress/lzw/writer.go", "src/compress/lzw/writer_test.go", "src/compress/testdata/", "src/compress/testdata/e.txt", "src/compress/testdata/gettysburg.txt", "src/compress/testdata/pi.txt", "src/compress/zlib/", "src/compress/zlib/example_test.go", "src/compress/zlib/reader.go", "src/compress/zlib/reader_test.go", "src/compress/zlib/writer.go", "src/compress/zlib/writer_test.go"}
+ if !reflect.DeepEqual(want, got) {
+ t.Errorf("got unexpected result: %#v", got)
+ }
+}
+
+func Test_metaCacheEntries_filterPrefix(t *testing.T) {
+ data := loadMetacacheSampleEntries(t)
+ data.filterPrefix("src/compress/bzip2/")
+ got := data.entries().names()
+ want := []string{"src/compress/bzip2/", "src/compress/bzip2/bit_reader.go", "src/compress/bzip2/bzip2.go", "src/compress/bzip2/bzip2_test.go", "src/compress/bzip2/huffman.go", "src/compress/bzip2/move_to_front.go", "src/compress/bzip2/testdata/", "src/compress/bzip2/testdata/Isaac.Newton-Opticks.txt.bz2", "src/compress/bzip2/testdata/e.txt.bz2", "src/compress/bzip2/testdata/fail-issue5747.bz2", "src/compress/bzip2/testdata/pass-random1.bin", "src/compress/bzip2/testdata/pass-random1.bz2", "src/compress/bzip2/testdata/pass-random2.bin", "src/compress/bzip2/testdata/pass-random2.bz2", "src/compress/bzip2/testdata/pass-sawtooth.bz2", "src/compress/bzip2/testdata/random.data.bz2"}
+ if !reflect.DeepEqual(want, got) {
+ t.Errorf("got unexpected result: %#v", got)
+ }
+}
+
+func Test_metaCacheEntry_isInDir(t *testing.T) {
+ tests := []struct {
+ testName string
+ entry string
+ dir string
+ sep string
+ want bool
+ }{
+ {
+ testName: "basic-file",
+ entry: "src/file",
+ dir: "src/",
+ sep: slashSeparator,
+ want: true,
+ },
+ {
+ testName: "basic-dir",
+ entry: "src/dir/",
+ dir: "src/",
+ sep: slashSeparator,
+ want: true,
+ },
+ {
+ testName: "deeper-file",
+ entry: "src/dir/somewhere.ext",
+ dir: "src/",
+ sep: slashSeparator,
+ want: false,
+ },
+ {
+ testName: "deeper-dir",
+ entry: "src/dir/somewhere/",
+ dir: "src/",
+ sep: slashSeparator,
+ want: false,
+ },
+ {
+ testName: "root-dir",
+ entry: "doc/",
+ dir: "",
+ sep: slashSeparator,
+ want: true,
+ },
+ {
+ testName: "root-file",
+ entry: "word.doc",
+ dir: "",
+ sep: slashSeparator,
+ want: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.testName, func(t *testing.T) {
+ e := metaCacheEntry{
+ name: tt.entry,
+ }
+ if got := e.isInDir(tt.dir, tt.sep); got != tt.want {
+ t.Errorf("isInDir() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func Test_metaCacheEntries_resolve(t *testing.T) {
+ baseTime, err := time.Parse("2006/01/02", "2015/02/25")
+ if err != nil {
+ t.Fatal(err)
+ }
+ inputs := []xlMetaV2{
+ 0: {
+ versions: []xlMetaV2ShallowVersion{
+ {header: xlMetaV2VersionHeader{
+ VersionID: [16]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
+ ModTime: baseTime.Add(30 * time.Minute).UnixNano(),
+ Signature: [4]byte{1, 1, 1, 1},
+ Type: ObjectType,
+ Flags: 0,
+ }},
+ },
+ },
+ // Mismatches Modtime+Signature and older...
+ 1: {
+ versions: []xlMetaV2ShallowVersion{
+ {header: xlMetaV2VersionHeader{
+ VersionID: [16]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
+ ModTime: baseTime.Add(15 * time.Minute).UnixNano(),
+ Signature: [4]byte{2, 1, 1, 1},
+ Type: ObjectType,
+ Flags: 0,
+ }},
+ },
+ },
+ // Has another version prior to the one we want.
+ 2: {
+ versions: []xlMetaV2ShallowVersion{
+ {header: xlMetaV2VersionHeader{
+ VersionID: [16]byte{2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
+ ModTime: baseTime.Add(60 * time.Minute).UnixNano(),
+ Signature: [4]byte{2, 1, 1, 1},
+ Type: ObjectType,
+ Flags: 0,
+ }},
+ {header: xlMetaV2VersionHeader{
+ VersionID: [16]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
+ ModTime: baseTime.Add(30 * time.Minute).UnixNano(),
+ Signature: [4]byte{1, 1, 1, 1},
+ Type: ObjectType,
+ Flags: 0,
+ }},
+ },
+ },
+ // Has a completely different version id
+ 3: {
+ versions: []xlMetaV2ShallowVersion{
+ {header: xlMetaV2VersionHeader{
+ VersionID: [16]byte{3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
+ ModTime: baseTime.Add(60 * time.Minute).UnixNano(),
+ Signature: [4]byte{1, 1, 1, 1},
+ Type: ObjectType,
+ Flags: 0,
+ }},
+ },
+ },
+ 4: {
+ versions: []xlMetaV2ShallowVersion{},
+ },
+ // Has a zero version id
+ 5: {
+ versions: []xlMetaV2ShallowVersion{
+ {header: xlMetaV2VersionHeader{
+ VersionID: [16]byte{},
+ ModTime: baseTime.Add(60 * time.Minute).UnixNano(),
+ Signature: [4]byte{5, 1, 1, 1},
+ Type: ObjectType,
+ Flags: 0,
+ }},
+ },
+ },
+ // Zero version, modtime newer..
+ 6: {
+ versions: []xlMetaV2ShallowVersion{
+ {header: xlMetaV2VersionHeader{
+ VersionID: [16]byte{},
+ ModTime: baseTime.Add(90 * time.Minute).UnixNano(),
+ Signature: [4]byte{6, 1, 1, 1},
+ Type: ObjectType,
+ Flags: 0,
+ }},
+ },
+ },
+ 7: {
+ versions: []xlMetaV2ShallowVersion{
+ {header: xlMetaV2VersionHeader{
+ VersionID: [16]byte{},
+ ModTime: baseTime.Add(90 * time.Minute).UnixNano(),
+ Signature: [4]byte{6, 1, 1, 1},
+ Type: ObjectType,
+ Flags: 0,
+ }},
+ {header: xlMetaV2VersionHeader{
+ VersionID: [16]byte{2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
+ ModTime: baseTime.Add(60 * time.Minute).UnixNano(),
+ Signature: [4]byte{2, 1, 1, 1},
+ Type: ObjectType,
+ Flags: 0,
+ }},
+ {header: xlMetaV2VersionHeader{
+ VersionID: [16]byte{3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
+ ModTime: baseTime.Add(60 * time.Minute).UnixNano(),
+ Signature: [4]byte{1, 1, 1, 1},
+ Type: ObjectType,
+ Flags: 0,
+ }},
+
+ {header: xlMetaV2VersionHeader{
+ VersionID: [16]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
+ ModTime: baseTime.Add(30 * time.Minute).UnixNano(),
+ Signature: [4]byte{1, 1, 1, 1},
+ Type: ObjectType,
+ Flags: 0,
+ }},
+ },
+ },
+ // Delete marker.
+ 8: {
+ versions: []xlMetaV2ShallowVersion{
+ {header: xlMetaV2VersionHeader{
+ VersionID: [16]byte{7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7},
+ ModTime: baseTime.Add(90 * time.Minute).UnixNano(),
+ Signature: [4]byte{6, 1, 1, 1},
+ Type: DeleteType,
+ Flags: 0,
+ }},
+ },
+ },
+ // Delete marker and version from 1
+ 9: {
+ versions: []xlMetaV2ShallowVersion{
+ {header: xlMetaV2VersionHeader{
+ VersionID: [16]byte{7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7},
+ ModTime: baseTime.Add(90 * time.Minute).UnixNano(),
+ Signature: [4]byte{6, 1, 1, 1},
+ Type: DeleteType,
+ Flags: 0,
+ }},
+ {header: xlMetaV2VersionHeader{
+ VersionID: [16]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
+ ModTime: baseTime.Add(15 * time.Minute).UnixNano(),
+ Signature: [4]byte{2, 1, 1, 1},
+ Type: ObjectType,
+ Flags: 0,
+ }},
+ },
+ },
+ }
+ inputSerialized := make([]metaCacheEntry, len(inputs))
+ for i, xl := range inputs {
+ xl.sortByModTime()
+ var err error
+ entry := metaCacheEntry{
+ name: "testobject",
+ }
+ entry.metadata, err = xl.AppendTo(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ inputSerialized[i] = entry
+ }
+
+ tests := []struct {
+ name string
+ m metaCacheEntries
+ r metadataResolutionParams
+ wantSelected *metaCacheEntry
+ wantOk bool
+ }{
+ {
+ name: "consistent",
+ m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], inputSerialized[0]},
+ r: metadataResolutionParams{dirQuorum: 4, objQuorum: 4, strict: false},
+ wantSelected: &inputSerialized[0],
+ wantOk: true,
+ },
+ {
+ name: "consistent-strict",
+ m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], inputSerialized[0]},
+ r: metadataResolutionParams{dirQuorum: 4, objQuorum: 4, strict: false},
+ wantSelected: &inputSerialized[0],
+ wantOk: true,
+ },
+ {
+ name: "one zero, below quorum",
+ m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], metaCacheEntry{}},
+ r: metadataResolutionParams{dirQuorum: 4, objQuorum: 4, strict: false},
+ wantSelected: nil,
+ wantOk: false,
+ },
+ {
+ name: "one zero, below quorum, strict",
+ m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], metaCacheEntry{}},
+ r: metadataResolutionParams{dirQuorum: 4, objQuorum: 4, strict: true},
+ wantSelected: nil,
+ wantOk: false,
+ },
+ {
+ name: "one zero, at quorum",
+ m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], metaCacheEntry{}},
+ r: metadataResolutionParams{dirQuorum: 3, objQuorum: 3, strict: false},
+ wantSelected: &inputSerialized[0],
+ wantOk: true,
+ },
+ {
+ name: "one zero, at quorum, strict",
+ m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], metaCacheEntry{}},
+ r: metadataResolutionParams{dirQuorum: 3, objQuorum: 3, strict: true},
+ wantSelected: &inputSerialized[0],
+ wantOk: true,
+ },
+ {
+ name: "modtime, signature mismatch",
+ m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], inputSerialized[1]},
+ r: metadataResolutionParams{dirQuorum: 4, objQuorum: 4, strict: false},
+ wantSelected: &inputSerialized[0],
+ wantOk: true,
+ },
+ {
+ name: "modtime,signature mismatch, strict",
+ m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], inputSerialized[1]},
+ r: metadataResolutionParams{dirQuorum: 4, objQuorum: 4, strict: true},
+ wantSelected: nil,
+ wantOk: false,
+ },
+ {
+ name: "modtime, signature mismatch, at quorum",
+ m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], inputSerialized[1]},
+ r: metadataResolutionParams{dirQuorum: 3, objQuorum: 3, strict: false},
+ wantSelected: &inputSerialized[0],
+ wantOk: true,
+ },
+ {
+ name: "modtime,signature mismatch, at quorum, strict",
+ m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], inputSerialized[1]},
+ r: metadataResolutionParams{dirQuorum: 3, objQuorum: 3, strict: true},
+ wantSelected: &inputSerialized[0],
+ wantOk: true,
+ },
+ {
+ name: "additional version",
+ m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], inputSerialized[2]},
+ r: metadataResolutionParams{dirQuorum: 4, objQuorum: 4, strict: false},
+ wantSelected: &inputSerialized[0],
+ wantOk: true,
+ },
+ {
+ // Since we have the same version in all inputs, that is strictly ok.
+ name: "additional version, strict",
+ m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], inputSerialized[2]},
+ r: metadataResolutionParams{dirQuorum: 4, objQuorum: 4, strict: true},
+ wantSelected: &inputSerialized[0],
+ wantOk: true,
+ },
+ {
+ // Since we have the same version in all inputs, that is strictly ok.
+ name: "additional version, quorum one",
+ m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], inputSerialized[2]},
+ r: metadataResolutionParams{dirQuorum: 1, objQuorum: 1, strict: true},
+ // We get the both versions, since we only request quorum 1
+ wantSelected: &inputSerialized[2],
+ wantOk: true,
+ },
+ {
+ name: "additional version, quorum two",
+ m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[0], inputSerialized[2]},
+ r: metadataResolutionParams{dirQuorum: 2, objQuorum: 2, strict: true},
+ wantSelected: &inputSerialized[0],
+ wantOk: true,
+ },
+ {
+ name: "2 additional versions, quorum two",
+ m: metaCacheEntries{inputSerialized[0], inputSerialized[0], inputSerialized[2], inputSerialized[2]},
+ r: metadataResolutionParams{dirQuorum: 2, objQuorum: 2, strict: true},
+ wantSelected: &inputSerialized[2],
+ wantOk: true,
+ },
+ {
+ // inputSerialized[1] have older versions of the second in inputSerialized[2]
+ name: "modtimemismatch",
+ m: metaCacheEntries{inputSerialized[1], inputSerialized[1], inputSerialized[2], inputSerialized[2]},
+ r: metadataResolutionParams{dirQuorum: 2, objQuorum: 2, strict: false},
+ wantSelected: &inputSerialized[2],
+ wantOk: true,
+ },
+ {
+ // inputSerialized[1] have older versions of the second in inputSerialized[2]
+ name: "modtimemismatch,strict",
+ m: metaCacheEntries{inputSerialized[1], inputSerialized[1], inputSerialized[2], inputSerialized[2]},
+ r: metadataResolutionParams{dirQuorum: 2, objQuorum: 2, strict: true},
+ wantSelected: &inputSerialized[2],
+ wantOk: true,
+ },
+ {
+ // inputSerialized[1] have older versions of the second in inputSerialized[2], but
+ // since it is not strict, we should get it that one (with latest modtime)
+ name: "modtimemismatch,not strict",
+ m: metaCacheEntries{inputSerialized[1], inputSerialized[1], inputSerialized[2], inputSerialized[2]},
+ r: metadataResolutionParams{dirQuorum: 4, objQuorum: 4, strict: false},
+ wantSelected: &inputSerialized[0],
+ wantOk: true,
+ },
+ {
+ name: "one-q1",
+ m: metaCacheEntries{inputSerialized[0], inputSerialized[4], inputSerialized[4], inputSerialized[4]},
+ r: metadataResolutionParams{dirQuorum: 1, objQuorum: 1, strict: false},
+ wantSelected: &inputSerialized[0],
+ wantOk: true,
+ },
+ {
+ name: "one-q1-strict",
+ m: metaCacheEntries{inputSerialized[0], inputSerialized[4], inputSerialized[4], inputSerialized[4]},
+ r: metadataResolutionParams{dirQuorum: 1, objQuorum: 1, strict: true},
+ wantSelected: &inputSerialized[0],
+ wantOk: true,
+ },
+ {
+ name: "one-q2",
+ m: metaCacheEntries{inputSerialized[0], inputSerialized[4], inputSerialized[4], inputSerialized[4]},
+ r: metadataResolutionParams{dirQuorum: 2, objQuorum: 2, strict: false},
+ wantSelected: nil,
+ wantOk: false,
+ },
+ {
+ name: "one-q2-strict",
+ m: metaCacheEntries{inputSerialized[0], inputSerialized[4], inputSerialized[4], inputSerialized[4]},
+ r: metadataResolutionParams{dirQuorum: 2, objQuorum: 2, strict: true},
+ wantSelected: nil,
+ wantOk: false,
+ },
+ {
+ name: "two-diff-q2",
+ m: metaCacheEntries{inputSerialized[0], inputSerialized[3], inputSerialized[4], inputSerialized[4]},
+ r: metadataResolutionParams{dirQuorum: 2, objQuorum: 2, strict: false},
+ wantSelected: nil,
+ wantOk: false,
+ },
+ {
+ name: "zeroid",
+ m: metaCacheEntries{inputSerialized[5], inputSerialized[5], inputSerialized[6], inputSerialized[6]},
+ r: metadataResolutionParams{dirQuorum: 2, objQuorum: 2, strict: false},
+ wantSelected: &inputSerialized[6],
+ wantOk: true,
+ },
+ {
+ // When ID is zero, do not allow non-strict matches to reach quorum.
+ name: "zeroid-belowq",
+ m: metaCacheEntries{inputSerialized[5], inputSerialized[5], inputSerialized[6], inputSerialized[6]},
+ r: metadataResolutionParams{dirQuorum: 3, objQuorum: 3, strict: false},
+ wantSelected: nil,
+ wantOk: false,
+ },
+ {
+ name: "merge4",
+ m: metaCacheEntries{inputSerialized[2], inputSerialized[3], inputSerialized[5], inputSerialized[6]},
+ r: metadataResolutionParams{dirQuorum: 1, objQuorum: 1, strict: false},
+ wantSelected: &inputSerialized[7],
+ wantOk: true,
+ },
+ {
+ name: "deletemarker",
+ m: metaCacheEntries{inputSerialized[8], inputSerialized[4], inputSerialized[4], inputSerialized[4]},
+ r: metadataResolutionParams{dirQuorum: 1, objQuorum: 1, strict: false},
+ wantSelected: &inputSerialized[8],
+ wantOk: true,
+ },
+ {
+ name: "deletemarker-nonq",
+ m: metaCacheEntries{inputSerialized[8], inputSerialized[8], inputSerialized[4], inputSerialized[4]},
+ r: metadataResolutionParams{dirQuorum: 3, objQuorum: 3, strict: false},
+ wantSelected: nil,
+ wantOk: false,
+ },
+ {
+ name: "deletemarker-nonq",
+ m: metaCacheEntries{inputSerialized[8], inputSerialized[8], inputSerialized[8], inputSerialized[1]},
+ r: metadataResolutionParams{dirQuorum: 3, objQuorum: 3, strict: false},
+ wantSelected: &inputSerialized[8],
+ wantOk: true,
+ },
+ {
+ name: "deletemarker-mixed",
+ m: metaCacheEntries{inputSerialized[8], inputSerialized[8], inputSerialized[1], inputSerialized[1]},
+ r: metadataResolutionParams{dirQuorum: 2, objQuorum: 2, strict: false},
+ wantSelected: &inputSerialized[9],
+ wantOk: true,
+ },
+ {
+ name: "deletemarker-q3",
+ m: metaCacheEntries{inputSerialized[8], inputSerialized[9], inputSerialized[9], inputSerialized[1]},
+ r: metadataResolutionParams{dirQuorum: 3, objQuorum: 3, strict: false},
+ wantSelected: &inputSerialized[9],
+ wantOk: true,
+ },
+ {
+ name: "deletemarker-q3-strict",
+ m: metaCacheEntries{inputSerialized[8], inputSerialized[9], inputSerialized[9], inputSerialized[1]},
+ r: metadataResolutionParams{dirQuorum: 3, objQuorum: 3, strict: true},
+ wantSelected: &inputSerialized[9],
+ wantOk: true,
+ },
+ }
+
+ for testID, tt := range tests {
+ rng := rand.New(rand.NewSource(0))
+ // Run for a number of times, shuffling the input to ensure that output is consistent.
+ for i := range 10 {
+ t.Run(fmt.Sprintf("test-%d-%s-run-%d", testID, tt.name, i), func(t *testing.T) {
+ if i > 0 {
+ rng.Shuffle(len(tt.m), func(i, j int) {
+ tt.m[i], tt.m[j] = tt.m[j], tt.m[i]
+ })
+ }
+ gotSelected, gotOk := tt.m.resolve(&tt.r)
+ if gotOk != tt.wantOk {
+ t.Errorf("resolve() gotOk = %v, want %v", gotOk, tt.wantOk)
+ }
+ if gotSelected != nil {
+ gotSelected.cached = nil
+ gotSelected.reusable = false
+ }
+ if !reflect.DeepEqual(gotSelected, tt.wantSelected) {
+ wantM, _ := tt.wantSelected.xlmeta()
+ gotM, _ := gotSelected.xlmeta()
+ t.Errorf("resolve() gotSelected = \n%#v, want \n%#v", *gotM, *wantM)
+ }
+ })
+ }
+ }
+}
diff --git a/cmd/metacache-manager.go b/cmd/metacache-manager.go
new file mode 100644
index 00000000..d23a8aa1
--- /dev/null
+++ b/cmd/metacache-manager.go
@@ -0,0 +1,210 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "runtime/debug"
+ "sync"
+ "time"
+
+ "github.com/minio/minio/internal/logger"
+)
+
+// localMetacacheMgr is the *local* manager for this peer.
+// It should never be used directly since buckets are
+// distributed deterministically.
+// Therefore no cluster locks are required.
+var localMetacacheMgr = &metacacheManager{
+ buckets: make(map[string]*bucketMetacache),
+ trash: make(map[string]metacache),
+}
+
+type metacacheManager struct {
+ mu sync.RWMutex
+ init sync.Once
+ buckets map[string]*bucketMetacache
+ trash map[string]metacache // Recently deleted lists.
+}
+
+const metacacheMaxEntries = 5000
+
+// initManager will start async saving the cache.
+func (m *metacacheManager) initManager() {
+ // Add a transient bucket.
+ // Start saver when object layer is ready.
+ go func() {
+ objAPI := newObjectLayerFn()
+ for objAPI == nil {
+ time.Sleep(time.Second)
+ objAPI = newObjectLayerFn()
+ }
+
+ t := time.NewTicker(time.Minute)
+ defer t.Stop()
+
+ var exit bool
+ for !exit {
+ select {
+ case <-t.C:
+ case <-GlobalContext.Done():
+ exit = true
+ }
+ m.mu.RLock()
+ for _, v := range m.buckets {
+ if !exit {
+ v.cleanup()
+ }
+ }
+ m.mu.RUnlock()
+ m.mu.Lock()
+ for k, v := range m.trash {
+ if time.Since(v.lastUpdate) > metacacheMaxRunningAge {
+ v.delete(context.Background())
+ delete(m.trash, k)
+ }
+ }
+ m.mu.Unlock()
+ }
+ }()
+}
+
+// updateCacheEntry will update non-transient state.
+func (m *metacacheManager) updateCacheEntry(update metacache) (metacache, error) {
+ m.mu.RLock()
+ if meta, ok := m.trash[update.id]; ok {
+ m.mu.RUnlock()
+ return meta, nil
+ }
+
+ b, ok := m.buckets[update.bucket]
+ m.mu.RUnlock()
+ if ok {
+ return b.updateCacheEntry(update)
+ }
+
+ // We should have either a trashed bucket or this
+ return metacache{}, errVolumeNotFound
+}
+
+// getBucket will get a bucket metacache or load it from disk if needed.
+func (m *metacacheManager) getBucket(ctx context.Context, bucket string) *bucketMetacache {
+ m.init.Do(m.initManager)
+
+ // Return a transient bucket for invalid or system buckets.
+ m.mu.RLock()
+ b, ok := m.buckets[bucket]
+ if ok {
+ m.mu.RUnlock()
+ if b.bucket != bucket {
+ logger.Info("getBucket: cached bucket %s does not match this bucket %s", b.bucket, bucket)
+ debug.PrintStack()
+ }
+ return b
+ }
+
+ m.mu.RUnlock()
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ // See if someone else fetched it while we waited for the lock.
+ b, ok = m.buckets[bucket]
+ if ok {
+ if b.bucket != bucket {
+ logger.Info("getBucket: newly cached bucket %s does not match this bucket %s", b.bucket, bucket)
+ debug.PrintStack()
+ }
+ return b
+ }
+
+ // New bucket. If we fail return the transient bucket.
+ b = newBucketMetacache(bucket, true)
+ m.buckets[bucket] = b
+ return b
+}
+
+// deleteBucketCache will delete the bucket cache if it exists.
+func (m *metacacheManager) deleteBucketCache(bucket string) {
+ m.init.Do(m.initManager)
+ m.mu.Lock()
+ b, ok := m.buckets[bucket]
+ if !ok {
+ m.mu.Unlock()
+ return
+ }
+ delete(m.buckets, bucket)
+ m.mu.Unlock()
+
+ // Since deletes may take some time we try to do it without
+ // holding lock to m all the time.
+ b.mu.Lock()
+ defer b.mu.Unlock()
+ for k, v := range b.caches {
+ if time.Since(v.lastUpdate) > metacacheMaxRunningAge {
+ v.delete(context.Background())
+ continue
+ }
+ v.error = "Bucket deleted"
+ v.status = scanStateError
+ m.mu.Lock()
+ m.trash[k] = v
+ m.mu.Unlock()
+ }
+}
+
+// deleteAll will delete all caches.
+func (m *metacacheManager) deleteAll() {
+ m.init.Do(m.initManager)
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ for bucket, b := range m.buckets {
+ b.deleteAll()
+ delete(m.buckets, bucket)
+ }
+}
+
+// checkMetacacheState should be used if data is not updating.
+// Should only be called if a failure occurred.
+func (o listPathOptions) checkMetacacheState(ctx context.Context, rpc *peerRESTClient) error {
+ // We operate on a copy...
+ o.Create = false
+ c, err := rpc.GetMetacacheListing(ctx, o)
+ if err != nil {
+ return err
+ }
+ cache := *c
+
+ if cache.status == scanStateNone || cache.fileNotFound {
+ return errFileNotFound
+ }
+ if cache.status == scanStateSuccess || cache.status == scanStateStarted {
+ if time.Since(cache.lastUpdate) > metacacheMaxRunningAge {
+ // We got a stale entry, mark error on handling server.
+ err := fmt.Errorf("timeout: list %s not updated", cache.id)
+ cache.error = err.Error()
+ cache.status = scanStateError
+ rpc.UpdateMetacacheListing(ctx, cache)
+ return err
+ }
+ return nil
+ }
+ if cache.error != "" {
+ return fmt.Errorf("async cache listing failed with: %s", cache.error)
+ }
+ return nil
+}
diff --git a/cmd/metacache-marker.go b/cmd/metacache-marker.go
new file mode 100644
index 00000000..4548af2b
--- /dev/null
+++ b/cmd/metacache-marker.go
@@ -0,0 +1,90 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+// markerTagVersion is the marker version.
+// Should not need to be updated unless a fundamental change is made to the marker format.
+const markerTagVersion = "v2"
+
+// parseMarker will parse a marker possibly encoded with encodeMarker
+func (o *listPathOptions) parseMarker() {
+ s := o.Marker
+ if !strings.Contains(s, "[minio_cache:"+markerTagVersion) {
+ return
+ }
+ start := strings.LastIndex(s, "[")
+ o.Marker = s[:start]
+ end := strings.LastIndex(s, "]")
+ tag := strings.Trim(s[start:end], "[]")
+ tags := strings.SplitSeq(tag, ",")
+ for tag := range tags {
+ kv := strings.Split(tag, ":")
+ if len(kv) < 2 {
+ continue
+ }
+ switch kv[0] {
+ case "minio_cache":
+ if kv[1] != markerTagVersion {
+ continue
+ }
+ case "id":
+ o.ID = kv[1]
+ case "return":
+ o.ID = mustGetUUID()
+ o.Create = true
+ case "p": // pool
+ v, err := strconv.ParseInt(kv[1], 10, 64)
+ if err != nil {
+ o.ID = mustGetUUID()
+ o.Create = true
+ continue
+ }
+ o.pool = int(v)
+ case "s": // set
+ v, err := strconv.ParseInt(kv[1], 10, 64)
+ if err != nil {
+ o.ID = mustGetUUID()
+ o.Create = true
+ continue
+ }
+ o.set = int(v)
+ default:
+ // Ignore unknown
+ }
+ }
+}
+
+// encodeMarker will encode a uuid and return it as a marker.
+// uuid cannot contain '[', ':' or ','.
+func (o listPathOptions) encodeMarker(marker string) string {
+ if o.ID == "" {
+ // Mark as returning listing...
+ return fmt.Sprintf("%s[minio_cache:%s,return:]", marker, markerTagVersion)
+ }
+ if strings.ContainsAny(o.ID, "[:,") {
+ internalLogIf(context.Background(), fmt.Errorf("encodeMarker: uuid %s contained invalid characters", o.ID))
+ }
+ return fmt.Sprintf("%s[minio_cache:%s,id:%s,p:%d,s:%d]", marker, markerTagVersion, o.ID, o.pool, o.set)
+}
diff --git a/cmd/metacache-server-pool.go b/cmd/metacache-server-pool.go
new file mode 100644
index 00000000..bcdfed8f
--- /dev/null
+++ b/cmd/metacache-server-pool.go
@@ -0,0 +1,456 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ pathutil "path"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/minio/minio/internal/grid"
+ xioutil "github.com/minio/minio/internal/ioutil"
+)
+
+func renameAllBucketMetacache(epPath string) error {
+ // Rename all previous `.minio.sys/buckets//.metacache` to
+ // to `.minio.sys/tmp/` for deletion.
+ return readDirFn(pathJoin(epPath, minioMetaBucket, bucketMetaPrefix), func(name string, typ os.FileMode) error {
+ if typ == os.ModeDir {
+ tmpMetacacheOld := pathutil.Join(epPath, minioMetaTmpDeletedBucket, mustGetUUID())
+ if err := renameAll(pathJoin(epPath, minioMetaBucket, metacachePrefixForID(name, slashSeparator)),
+ tmpMetacacheOld, epPath); err != nil && err != errFileNotFound {
+ return fmt.Errorf("unable to rename (%s -> %s) %w",
+ pathJoin(epPath, minioMetaBucket+metacachePrefixForID(minioMetaBucket, slashSeparator)),
+ tmpMetacacheOld,
+ osErrToFileErr(err))
+ }
+ }
+ return nil
+ })
+}
+
+// listPath will return the requested entries.
+// If no more entries are in the listing io.EOF is returned,
+// otherwise nil or an unexpected error is returned.
+// The listPathOptions given will be checked and modified internally.
+// Required important fields are Bucket, Prefix, Separator.
+// Other important fields are Limit, Marker.
+// List ID always derived from the Marker.
+func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) (entries metaCacheEntriesSorted, err error) {
+ if err := checkListObjsArgs(ctx, o.Bucket, o.Prefix, o.Marker); err != nil {
+ return entries, err
+ }
+
+ // Marker points to before the prefix, just ignore it.
+ if o.Marker < o.Prefix {
+ o.Marker = ""
+ }
+
+ // Marker is set validate pre-condition.
+ if o.Marker != "" && o.Prefix != "" {
+ // Marker not common with prefix is not implemented. Send an empty response
+ if !HasPrefix(o.Marker, o.Prefix) {
+ return entries, io.EOF
+ }
+ }
+
+ // With max keys of zero we have reached eof, return right here.
+ if o.Limit == 0 {
+ return entries, io.EOF
+ }
+
+ // For delimiter and prefix as '/' we do not list anything at all
+ // along // with the prefix. On a flat namespace with 'prefix'
+ // as '/' we don't have any entries, since all the keys are
+ // of form 'keyName/...'
+ if strings.HasPrefix(o.Prefix, SlashSeparator) {
+ return entries, io.EOF
+ }
+
+ // If delimiter is slashSeparator we must return directories of
+ // the non-recursive scan unless explicitly requested.
+ o.IncludeDirectories = o.Separator == slashSeparator
+ if (o.Separator == slashSeparator || o.Separator == "") && !o.Recursive {
+ o.Recursive = o.Separator != slashSeparator
+ o.Separator = slashSeparator
+ } else {
+ // Default is recursive, if delimiter is set then list non recursive.
+ o.Recursive = true
+ }
+
+ // Decode and get the optional list id from the marker.
+ o.parseMarker()
+ if o.BaseDir == "" {
+ o.BaseDir = baseDirFromPrefix(o.Prefix)
+ }
+ o.Transient = o.Transient || isReservedOrInvalidBucket(o.Bucket, false)
+ o.SetFilter()
+ if o.Transient {
+ o.Create = false
+ }
+
+ // We have 2 cases:
+ // 1) Cold listing, just list.
+ // 2) Returning, but with no id. Start async listing.
+ // 3) Returning, with ID, stream from list.
+ //
+ // If we don't have a list id we must ask the server if it has a cache or create a new.
+ if o.ID != "" && !o.Transient {
+ // Create or ping with handout...
+ rpc := globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix))
+ var c *metacache
+ if rpc == nil {
+ resp := localMetacacheMgr.getBucket(ctx, o.Bucket).findCache(*o)
+ c = &resp
+ } else {
+ rctx, cancel := context.WithTimeout(ctx, 5*time.Second)
+ c, err = rpc.GetMetacacheListing(rctx, *o)
+ cancel()
+ }
+ if err != nil {
+ if errors.Is(err, context.Canceled) {
+ // Context is canceled, return at once.
+ // request canceled, no entries to return
+ return entries, io.EOF
+ }
+ if !IsErr(err, context.DeadlineExceeded, grid.ErrDisconnected) {
+ // Report error once per bucket, but continue listing.x
+ storageLogOnceIf(ctx, err, "GetMetacacheListing:"+o.Bucket)
+ }
+ o.Transient = true
+ o.Create = false
+ o.ID = mustGetUUID()
+ } else {
+ if c.fileNotFound {
+ // No cache found, no entries found.
+ return entries, io.EOF
+ }
+ if c.status == scanStateError || c.status == scanStateNone {
+ o.ID = ""
+ o.Create = false
+ o.debugln("scan status", c.status, " - waiting a roundtrip to create")
+ } else {
+ // Continue listing
+ o.ID = c.id
+ go c.keepAlive(ctx, rpc)
+ }
+ }
+ }
+
+ if o.ID != "" && !o.Transient {
+ // We have an existing list ID, continue streaming.
+ if o.Create {
+ o.debugln("Creating", o)
+ entries, err = z.listAndSave(ctx, o)
+ if err == nil || err == io.EOF {
+ return entries, err
+ }
+ entries.truncate(0)
+ } else {
+ if o.pool < len(z.serverPools) && o.set < len(z.serverPools[o.pool].sets) {
+ o.debugln("Resuming", o)
+ entries, err = z.serverPools[o.pool].sets[o.set].streamMetadataParts(ctx, *o)
+ entries.reuse = true // We read from stream and are not sharing results.
+ if err == nil {
+ return entries, nil
+ }
+ } else {
+ err = fmt.Errorf("invalid pool/set")
+ o.pool, o.set = 0, 0
+ }
+ }
+ if IsErr(err, []error{
+ nil,
+ context.Canceled,
+ context.DeadlineExceeded,
+ // io.EOF is expected and should be returned but no need to log it.
+ io.EOF,
+ }...) {
+ // Expected good errors we don't need to return error.
+ return entries, err
+ }
+ entries.truncate(0)
+ go func() {
+ rpc := globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix))
+ if rpc != nil {
+ ctx, cancel := context.WithTimeout(GlobalContext, 5*time.Second)
+ defer cancel()
+ c, err := rpc.GetMetacacheListing(ctx, *o)
+ if err == nil {
+ c.error = "no longer used"
+ c.status = scanStateError
+ rpc.UpdateMetacacheListing(ctx, *c)
+ }
+ }
+ }()
+ o.ID = ""
+ }
+
+ if contextCanceled(ctx) {
+ return entries, ctx.Err()
+ }
+ // Do listing in-place.
+ // Create output for our results.
+ // Create filter for results.
+ o.debugln("Raw List", o)
+ filterCh := make(chan metaCacheEntry, o.Limit)
+ listCtx, cancelList := context.WithCancel(ctx)
+ filteredResults := o.gatherResults(listCtx, filterCh)
+ var wg sync.WaitGroup
+ wg.Add(1)
+ var listErr error
+
+ go func(o listPathOptions) {
+ defer wg.Done()
+ if o.Lifecycle == nil {
+ // No filtering ahead, ask drives to stop
+ // listing exactly at a specific limit.
+ o.StopDiskAtLimit = true
+ }
+ listErr = z.listMerged(listCtx, o, filterCh)
+ o.debugln("listMerged returned with", listErr)
+ }(*o)
+
+ entries, err = filteredResults()
+ cancelList()
+ wg.Wait()
+ if listErr != nil && !errors.Is(listErr, context.Canceled) {
+ return entries, listErr
+ }
+ entries.reuse = true
+ truncated := entries.len() > o.Limit || err == nil
+ entries.truncate(o.Limit)
+ if !o.Transient && truncated {
+ if o.ID == "" {
+ entries.listID = mustGetUUID()
+ } else {
+ entries.listID = o.ID
+ }
+ }
+ if !truncated {
+ return entries, io.EOF
+ }
+ return entries, nil
+}
+
+// listMerged will list across all sets and return a merged results stream.
+// The result channel is closed when no more results are expected.
+func (z *erasureServerPools) listMerged(ctx context.Context, o listPathOptions, results chan<- metaCacheEntry) error {
+ var mu sync.Mutex
+ var wg sync.WaitGroup
+ var errs []error
+ allAtEOF := true
+ var inputs []chan metaCacheEntry
+ mu.Lock()
+ // Ask all sets and merge entries.
+ listCtx, cancelList := context.WithCancel(ctx)
+ defer cancelList()
+ for _, pool := range z.serverPools {
+ for _, set := range pool.sets {
+ wg.Add(1)
+ innerResults := make(chan metaCacheEntry, 100)
+ inputs = append(inputs, innerResults)
+ go func(i int, set *erasureObjects) {
+ defer wg.Done()
+ err := set.listPath(listCtx, o, innerResults)
+ mu.Lock()
+ defer mu.Unlock()
+ if err == nil {
+ allAtEOF = false
+ }
+ errs[i] = err
+ }(len(errs), set)
+ errs = append(errs, nil)
+ }
+ }
+ mu.Unlock()
+
+ // Gather results to a single channel.
+ // Quorum is one since we are merging across sets.
+ err := mergeEntryChannels(ctx, inputs, results, 1)
+
+ cancelList()
+ wg.Wait()
+
+ // we should return 'errs' from per disk
+ if isAllNotFound(errs) {
+ if isAllVolumeNotFound(errs) {
+ return errVolumeNotFound
+ }
+ return nil
+ }
+
+ if err != nil {
+ return err
+ }
+
+ if contextCanceled(ctx) {
+ return ctx.Err()
+ }
+
+ for _, err := range errs {
+ if errors.Is(err, io.EOF) {
+ continue
+ }
+ if err == nil || contextCanceled(ctx) || errors.Is(err, context.Canceled) {
+ allAtEOF = false
+ continue
+ }
+ storageLogIf(ctx, err)
+ return err
+ }
+ if allAtEOF {
+ return io.EOF
+ }
+ return nil
+}
+
+// triggerExpiryAndRepl applies lifecycle and replication actions on the listing
+// It returns true if the listing is non-versioned and the given object is expired.
+func triggerExpiryAndRepl(ctx context.Context, o listPathOptions, obj metaCacheEntry) (skip bool) {
+ versioned := o.Versioning != nil && o.Versioning.Versioned(obj.name)
+
+ // skip latest object from listing only for regular
+ // listObjects calls, versioned based listing cannot
+ // filter out between versions 'obj' cannot be truncated
+ // in such a manner, so look for skipping an object only
+ // for regular ListObjects() call only.
+ if !o.Versioned && !o.V1 {
+ fi, err := obj.fileInfo(o.Bucket)
+ if err != nil {
+ return skip
+ }
+ objInfo := fi.ToObjectInfo(o.Bucket, obj.name, versioned)
+ if o.Lifecycle != nil {
+ act := evalActionFromLifecycle(ctx, *o.Lifecycle, o.Retention, o.Replication.Config, objInfo).Action
+ skip = act.Delete() && !act.DeleteRestored()
+ }
+ }
+
+ fiv, err := obj.fileInfoVersions(o.Bucket)
+ if err != nil {
+ return skip
+ }
+
+ // Expire all versions if needed, if not attempt to queue for replication.
+ for _, version := range fiv.Versions {
+ objInfo := version.ToObjectInfo(o.Bucket, obj.name, versioned)
+
+ if o.Lifecycle != nil {
+ evt := evalActionFromLifecycle(ctx, *o.Lifecycle, o.Retention, o.Replication.Config, objInfo)
+ if evt.Action.Delete() {
+ globalExpiryState.enqueueByDays(objInfo, evt, lcEventSrc_s3ListObjects)
+ if !evt.Action.DeleteRestored() {
+ continue
+ } // queue version for replication upon expired restored copies if needed.
+ }
+ }
+
+ queueReplicationHeal(ctx, o.Bucket, objInfo, o.Replication, 0)
+ }
+ return skip
+}
+
+func (z *erasureServerPools) listAndSave(ctx context.Context, o *listPathOptions) (entries metaCacheEntriesSorted, err error) {
+ // Use ID as the object name...
+ o.pool = z.getAvailablePoolIdx(ctx, minioMetaBucket, o.ID, 10<<20)
+ if o.pool < 0 {
+ // No space or similar, don't persist the listing.
+ o.pool = 0
+ o.Create = false
+ o.ID = ""
+ o.Transient = true
+ return entries, errDiskFull
+ }
+ o.set = z.serverPools[o.pool].getHashedSetIndex(o.ID)
+ saver := z.serverPools[o.pool].sets[o.set]
+
+ // Disconnect from call above, but cancel on exit.
+ listCtx, cancel := context.WithCancel(GlobalContext)
+ saveCh := make(chan metaCacheEntry, metacacheBlockSize)
+ inCh := make(chan metaCacheEntry, metacacheBlockSize)
+ outCh := make(chan metaCacheEntry, o.Limit)
+
+ filteredResults := o.gatherResults(ctx, outCh)
+
+ mc := o.newMetacache()
+ meta := metaCacheRPC{meta: &mc, cancel: cancel, rpc: globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix)), o: *o}
+
+ // Save listing...
+ go func() {
+ if err := saver.saveMetaCacheStream(listCtx, &meta, saveCh); err != nil {
+ meta.setErr(err.Error())
+ }
+ cancel()
+ }()
+
+ // Do listing...
+ go func(o listPathOptions) {
+ err := z.listMerged(listCtx, o, inCh)
+ if err != nil {
+ meta.setErr(err.Error())
+ }
+ o.debugln("listAndSave: listing", o.ID, "finished with ", err)
+ }(*o)
+
+ // Keep track of when we return since we no longer have to send entries to output.
+ var funcReturned bool
+ var funcReturnedMu sync.Mutex
+ defer func() {
+ funcReturnedMu.Lock()
+ funcReturned = true
+ funcReturnedMu.Unlock()
+ }()
+ // Write listing to results and saver.
+ go func() {
+ var returned bool
+ for entry := range inCh {
+ if o.shouldSkip(ctx, entry) {
+ continue
+ }
+ if !returned {
+ funcReturnedMu.Lock()
+ returned = funcReturned
+ funcReturnedMu.Unlock()
+ outCh <- entry
+ if returned {
+ xioutil.SafeClose(outCh)
+ }
+ }
+ entry.reusable = returned
+ saveCh <- entry
+ }
+ if !returned {
+ xioutil.SafeClose(outCh)
+ }
+ xioutil.SafeClose(saveCh)
+ }()
+
+ entries, err = filteredResults()
+ if err == nil {
+ // Check if listing recorded an error.
+ err = meta.getErr()
+ }
+ return entries, err
+}
diff --git a/cmd/metacache-set.go b/cmd/metacache-set.go
new file mode 100644
index 00000000..c43d18d7
--- /dev/null
+++ b/cmd/metacache-set.go
@@ -0,0 +1,1204 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "encoding/gob"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "maps"
+ "math/rand"
+ "strconv"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ jsoniter "github.com/json-iterator/go"
+ "github.com/minio/minio/internal/bucket/lifecycle"
+ "github.com/minio/minio/internal/bucket/object/lock"
+ "github.com/minio/minio/internal/bucket/versioning"
+ "github.com/minio/minio/internal/color"
+ "github.com/minio/minio/internal/hash"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/pkg/v3/console"
+)
+
+//go:generate msgp -file $GOFILE -unexported
+
+type listPathOptions struct {
+ // ID of the listing.
+ // This will be used to persist the list.
+ ID string
+
+ // Bucket of the listing.
+ Bucket string
+
+ // Directory inside the bucket.
+ // When unset listPath will set this based on Prefix
+ BaseDir string
+
+ // Scan/return only content with prefix.
+ Prefix string
+
+ // FilterPrefix will return only results with this prefix when scanning.
+ // Should never contain a slash.
+ // Prefix should still be set.
+ FilterPrefix string
+
+ // Marker to resume listing.
+ // The response will be the first entry >= this object name.
+ Marker string
+
+ // Limit the number of results.
+ Limit int
+
+ // The number of disks to ask.
+ AskDisks string
+
+ // InclDeleted will keep all entries where latest version is a delete marker.
+ InclDeleted bool
+
+ // Scan recursively.
+ // If false only main directory will be scanned.
+ // Should always be true if Separator is n SlashSeparator.
+ Recursive bool
+
+ // Separator to use.
+ Separator string
+
+ // Create indicates that the lister should not attempt to load an existing cache.
+ Create bool
+
+ // Include pure directories.
+ IncludeDirectories bool
+
+ // Transient is set if the cache is transient due to an error or being a reserved bucket.
+ // This means the cache metadata will not be persisted on disk.
+ // A transient result will never be returned from the cache so knowing the list id is required.
+ Transient bool
+
+ // Versioned is this a ListObjectVersions call.
+ Versioned bool
+ // V1 listing type
+ V1 bool
+
+ // Versioning config is used for if the path
+ // has versioning enabled.
+ Versioning *versioning.Versioning `msg:"-"`
+
+ // Lifecycle performs filtering based on lifecycle.
+ // This will filter out objects if the most recent version should be deleted by lifecycle.
+ // Is not transferred across request calls.
+ Lifecycle *lifecycle.Lifecycle `msg:"-"`
+
+ // Retention configuration, needed to be passed along with lifecycle if set.
+ Retention lock.Retention `msg:"-"`
+
+ // Replication configuration
+ Replication replicationConfig `msg:"-"`
+
+ // StopDiskAtLimit will stop listing on each disk when limit number off objects has been returned.
+ StopDiskAtLimit bool
+
+ // pool and set of where the cache is located.
+ pool, set int
+}
+
+func init() {
+ gob.Register(listPathOptions{})
+}
+
+func (o *listPathOptions) setBucketMeta(ctx context.Context) {
+ lc, _ := globalLifecycleSys.Get(o.Bucket)
+ vc, _ := globalBucketVersioningSys.Get(o.Bucket)
+
+ // Check if bucket is object locked.
+ rcfg, _ := globalBucketObjectLockSys.Get(o.Bucket)
+ replCfg, _, _ := globalBucketMetadataSys.GetReplicationConfig(ctx, o.Bucket)
+ tgts, _ := globalBucketTargetSys.ListBucketTargets(ctx, o.Bucket)
+ o.Lifecycle = lc
+ o.Versioning = vc
+ o.Replication = replicationConfig{
+ Config: replCfg,
+ remotes: tgts,
+ }
+ o.Retention = rcfg
+}
+
+// newMetacache constructs a new metacache from the options.
+func (o listPathOptions) newMetacache() metacache {
+ return metacache{
+ id: o.ID,
+ bucket: o.Bucket,
+ root: o.BaseDir,
+ recursive: o.Recursive,
+ status: scanStateStarted,
+ error: "",
+ started: UTCNow(),
+ lastHandout: UTCNow(),
+ lastUpdate: UTCNow(),
+ ended: time.Time{},
+ dataVersion: metacacheStreamVersion,
+ filter: o.FilterPrefix,
+ }
+}
+
+func (o *listPathOptions) debugf(format string, data ...any) {
+ if serverDebugLog {
+ console.Debugf(format+"\n", data...)
+ }
+}
+
+func (o *listPathOptions) debugln(data ...any) {
+ if serverDebugLog {
+ console.Debugln(data...)
+ }
+}
+
+func (o *listPathOptions) shouldSkip(ctx context.Context, entry metaCacheEntry) (yes bool) {
+ if !o.IncludeDirectories && (entry.isDir() || (!o.Versioned && entry.isObjectDir() && entry.isLatestDeletemarker())) {
+ return true
+ }
+ if o.Marker != "" && entry.name < o.Marker {
+ return true
+ }
+ if !strings.HasPrefix(entry.name, o.Prefix) {
+ return true
+ }
+ if o.Separator != "" && entry.isDir() && !strings.Contains(strings.TrimPrefix(entry.name, o.Prefix), o.Separator) {
+ return true
+ }
+ if !o.Recursive && !entry.isInDir(o.Prefix, o.Separator) {
+ return true
+ }
+ if !o.InclDeleted && entry.isObject() && entry.isLatestDeletemarker() && !entry.isObjectDir() {
+ return true
+ }
+ if o.Lifecycle != nil || o.Replication.Config != nil {
+ return triggerExpiryAndRepl(ctx, *o, entry)
+ }
+ return false
+}
+
+// gatherResults will collect all results on the input channel and filter results according
+// to the options or to the current bucket ILM expiry rules.
+// Caller should close the channel when done.
+// The returned function will return the results once there is enough or input is closed,
+// or the context is canceled.
+func (o *listPathOptions) gatherResults(ctx context.Context, in <-chan metaCacheEntry) func() (metaCacheEntriesSorted, error) {
+ resultsDone := make(chan metaCacheEntriesSorted)
+ // Copy so we can mutate
+ resCh := resultsDone
+ var done atomic.Bool
+ resErr := io.EOF
+
+ go func() {
+ var results metaCacheEntriesSorted
+ var returned bool
+ for entry := range in {
+ if returned {
+ // past limit
+ continue
+ }
+ returned = done.Load()
+ if returned {
+ resCh = nil
+ continue
+ }
+ if yes := o.shouldSkip(ctx, entry); yes {
+ // when we have not enough results, record the skipped entry
+ if o.Limit > 0 && results.len() < o.Limit {
+ results.lastSkippedEntry = entry.name
+ }
+ continue
+ }
+ if o.Limit > 0 && results.len() >= o.Limit {
+ // We have enough and we have more.
+ // Do not return io.EOF
+ if resCh != nil {
+ resErr = nil
+ select {
+ case resCh <- results:
+ case <-ctx.Done():
+ }
+ resCh = nil
+ returned = true
+ }
+ continue
+ }
+ results.o = append(results.o, entry)
+ }
+ if resCh != nil {
+ resErr = io.EOF
+ select {
+ case <-ctx.Done():
+ // Nobody wants it.
+ case resCh <- results:
+ }
+ }
+ }()
+ return func() (metaCacheEntriesSorted, error) {
+ select {
+ case <-ctx.Done():
+ done.Store(true)
+ return metaCacheEntriesSorted{}, ctx.Err()
+ case r := <-resultsDone:
+ return r, resErr
+ }
+ }
+}
+
+// findFirstPart will find the part with 0 being the first that corresponds to the marker in the options.
+// io.ErrUnexpectedEOF is returned if the place containing the marker hasn't been scanned yet.
+// io.EOF indicates the marker is beyond the end of the stream and does not exist.
+func (o *listPathOptions) findFirstPart(fi FileInfo) (int, error) {
+ search := o.Marker
+ if search == "" {
+ search = o.Prefix
+ }
+ if search == "" {
+ return 0, nil
+ }
+ o.debugln("searching for ", search)
+ var tmp metacacheBlock
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ i := 0
+ for {
+ partKey := fmt.Sprintf("%s-metacache-part-%d", ReservedMetadataPrefixLower, i)
+ v, ok := fi.Metadata[partKey]
+ if !ok {
+ o.debugln("no match in metadata, waiting")
+ return -1, io.ErrUnexpectedEOF
+ }
+ err := json.Unmarshal([]byte(v), &tmp)
+ if !ok {
+ bugLogIf(context.Background(), err)
+ return -1, err
+ }
+ if tmp.First == "" && tmp.Last == "" && tmp.EOS {
+ return 0, errFileNotFound
+ }
+ if tmp.First >= search {
+ o.debugln("First >= search", v)
+ return i, nil
+ }
+ if tmp.Last >= search {
+ o.debugln("Last >= search", v)
+ return i, nil
+ }
+ if tmp.EOS {
+ o.debugln("no match, at EOS", v)
+ return -3, io.EOF
+ }
+ o.debugln("First ", tmp.First, "<", search, " search", i)
+ i++
+ }
+}
+
+// updateMetacacheListing will update the metacache listing.
+func (o *listPathOptions) updateMetacacheListing(m metacache, rpc *peerRESTClient) (metacache, error) {
+ if rpc == nil {
+ return localMetacacheMgr.updateCacheEntry(m)
+ }
+ return rpc.UpdateMetacacheListing(context.Background(), m)
+}
+
+func getMetacacheBlockInfo(fi FileInfo, block int) (*metacacheBlock, error) {
+ var tmp metacacheBlock
+ partKey := fmt.Sprintf("%s-metacache-part-%d", ReservedMetadataPrefixLower, block)
+ v, ok := fi.Metadata[partKey]
+ if !ok {
+ return nil, io.ErrUnexpectedEOF
+ }
+ return &tmp, json.Unmarshal([]byte(v), &tmp)
+}
+
+const metacachePrefix = ".metacache"
+
+func metacachePrefixForID(bucket, id string) string {
+ return pathJoin(bucketMetaPrefix, bucket, metacachePrefix, id)
+}
+
+// objectPath returns the object path of the cache.
+func (o *listPathOptions) objectPath(block int) string {
+ return pathJoin(metacachePrefixForID(o.Bucket, o.ID), "block-"+strconv.Itoa(block)+".s2")
+}
+
+func (o *listPathOptions) SetFilter() {
+ switch {
+ case metacacheSharePrefix:
+ return
+ case o.Prefix == o.BaseDir:
+ // No additional prefix
+ return
+ }
+ // Remove basedir.
+ o.FilterPrefix = strings.TrimPrefix(o.Prefix, o.BaseDir)
+ // Remove leading and trailing slashes.
+ o.FilterPrefix = strings.Trim(o.FilterPrefix, slashSeparator)
+
+ if strings.Contains(o.FilterPrefix, slashSeparator) {
+ // Sanity check, should not happen.
+ o.FilterPrefix = ""
+ }
+}
+
+// filter will apply the options and return the number of objects requested by the limit.
+// Will return io.EOF if there are no more entries with the same filter.
+// The last entry can be used as a marker to resume the listing.
+func (r *metacacheReader) filter(o listPathOptions) (entries metaCacheEntriesSorted, err error) {
+ // Forward to prefix, if any
+ err = r.forwardTo(o.Prefix)
+ if err != nil {
+ return entries, err
+ }
+ if o.Marker != "" {
+ err = r.forwardTo(o.Marker)
+ if err != nil {
+ return entries, err
+ }
+ }
+ o.debugln("forwarded to ", o.Prefix, "marker:", o.Marker, "sep:", o.Separator)
+
+ // Filter
+ if !o.Recursive {
+ entries.o = make(metaCacheEntries, 0, o.Limit)
+ pastPrefix := false
+ err := r.readFn(func(entry metaCacheEntry) bool {
+ if o.Prefix != "" && !strings.HasPrefix(entry.name, o.Prefix) {
+ // We are past the prefix, don't continue.
+ pastPrefix = true
+ return false
+ }
+ if !o.IncludeDirectories && (entry.isDir() || (!o.Versioned && entry.isObjectDir() && entry.isLatestDeletemarker())) {
+ return true
+ }
+ if !entry.isInDir(o.Prefix, o.Separator) {
+ return true
+ }
+ if !o.InclDeleted && entry.isObject() && entry.isLatestDeletemarker() && !entry.isObjectDir() {
+ return true
+ }
+ if !o.InclDeleted && entry.isAllFreeVersions() {
+ return true
+ }
+ entries.o = append(entries.o, entry)
+ return entries.len() < o.Limit
+ })
+ if (err != nil && errors.Is(err, io.EOF)) || pastPrefix || r.nextEOF() {
+ return entries, io.EOF
+ }
+ return entries, err
+ }
+
+ // We should not need to filter more.
+ return r.readN(o.Limit, o.InclDeleted, o.IncludeDirectories, o.Versioned, o.Prefix)
+}
+
+func (er *erasureObjects) streamMetadataParts(ctx context.Context, o listPathOptions) (entries metaCacheEntriesSorted, err error) {
+ retries := 0
+ rpc := globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix))
+
+ const (
+ retryDelay = 50 * time.Millisecond
+ retryDelay250 = 250 * time.Millisecond
+ )
+
+ for {
+ if contextCanceled(ctx) {
+ return entries, ctx.Err()
+ }
+
+ // If many failures, check the cache state.
+ if retries > 10 {
+ err := o.checkMetacacheState(ctx, rpc)
+ if err != nil {
+ return entries, fmt.Errorf("remote listing canceled: %w", err)
+ }
+ retries = 1
+ }
+
+ // All operations are performed without locks, so we must be careful and allow for failures.
+ // Read metadata associated with the object from a disk.
+ if retries > 0 {
+ for _, disk := range er.getDisks() {
+ if disk == nil {
+ continue
+ }
+ if !disk.IsOnline() {
+ continue
+ }
+ _, err := disk.ReadVersion(ctx, "", minioMetaBucket,
+ o.objectPath(0), "", ReadOptions{})
+ if err != nil {
+ time.Sleep(retryDelay250)
+ retries++
+ continue
+ }
+ break
+ }
+ }
+ retryWait := func() {
+ retries++
+ if retries == 1 {
+ time.Sleep(retryDelay)
+ } else {
+ time.Sleep(retryDelay250)
+ }
+ }
+ // Load first part metadata...
+ // Read metadata associated with the object from all disks.
+ fi, metaArr, onlineDisks, err := er.getObjectFileInfo(ctx, minioMetaBucket, o.objectPath(0), ObjectOptions{}, true)
+ if err != nil {
+ switch toObjectErr(err, minioMetaBucket, o.objectPath(0)).(type) {
+ case ObjectNotFound, InsufficientReadQuorum:
+ retryWait()
+ continue
+ }
+ // Allow one fast retry for other errors.
+ if retries > 0 {
+ return entries, fmt.Errorf("reading first part metadata: %v", err)
+ }
+ retryWait()
+ continue
+ }
+
+ partN, err := o.findFirstPart(fi)
+ switch {
+ case err == nil:
+ case errors.Is(err, io.ErrUnexpectedEOF):
+ if retries == 10 {
+ err := o.checkMetacacheState(ctx, rpc)
+ if err != nil {
+ return entries, fmt.Errorf("remote listing canceled: %w", err)
+ }
+ retries = -1
+ }
+ retryWait()
+ continue
+ case errors.Is(err, io.EOF):
+ return entries, io.EOF
+ }
+
+ // We got a stream to start at.
+ loadedPart := 0
+ for {
+ if contextCanceled(ctx) {
+ return entries, ctx.Err()
+ }
+
+ if partN != loadedPart {
+ if retries > 10 {
+ err := o.checkMetacacheState(ctx, rpc)
+ if err != nil {
+ return entries, fmt.Errorf("waiting for next part %d: %w", partN, err)
+ }
+ retries = 1
+ }
+
+ if retries > 0 {
+ // Load from one disk only
+ for _, disk := range er.getDisks() {
+ if disk == nil {
+ continue
+ }
+ if !disk.IsOnline() {
+ continue
+ }
+ _, err := disk.ReadVersion(ctx, "", minioMetaBucket,
+ o.objectPath(partN), "", ReadOptions{})
+ if err != nil {
+ time.Sleep(retryDelay250)
+ retries++
+ continue
+ }
+ break
+ }
+ }
+
+ // Load partN metadata...
+ fi, metaArr, onlineDisks, err = er.getObjectFileInfo(ctx, minioMetaBucket, o.objectPath(partN), ObjectOptions{}, true)
+ if err != nil {
+ time.Sleep(retryDelay250)
+ retries++
+ continue
+ }
+ loadedPart = partN
+ bi, err := getMetacacheBlockInfo(fi, partN)
+ internalLogIf(ctx, err)
+ if err == nil {
+ if bi.pastPrefix(o.Prefix) {
+ return entries, io.EOF
+ }
+ }
+ }
+
+ pr, pw := io.Pipe()
+ go func() {
+ werr := er.getObjectWithFileInfo(ctx, minioMetaBucket, o.objectPath(partN), 0,
+ fi.Size, pw, fi, metaArr, onlineDisks)
+ pw.CloseWithError(werr)
+ }()
+
+ tmp := newMetacacheReader(pr)
+ e, err := tmp.filter(o)
+ pr.CloseWithError(err)
+ tmp.Close()
+ entries.o = append(entries.o, e.o...)
+ if o.Limit > 0 && entries.len() > o.Limit {
+ entries.truncate(o.Limit)
+ return entries, nil
+ }
+ if err == nil {
+ // We stopped within the listing, we are done for now...
+ return entries, nil
+ }
+ if err != nil && !errors.Is(err, io.EOF) {
+ switch toObjectErr(err, minioMetaBucket, o.objectPath(partN)).(type) {
+ case ObjectNotFound:
+ retries++
+ time.Sleep(retryDelay250)
+ continue
+ case InsufficientReadQuorum:
+ retries++
+ loadedPart = -1
+ time.Sleep(retryDelay250)
+ continue
+ default:
+ internalLogIf(ctx, err)
+ return entries, err
+ }
+ }
+
+ // We finished at the end of the block.
+ // And should not expect any more results.
+ bi, err := getMetacacheBlockInfo(fi, partN)
+ internalLogIf(ctx, err)
+ if err != nil || bi.EOS {
+ // We are done and there are no more parts.
+ return entries, io.EOF
+ }
+ if bi.endedPrefix(o.Prefix) {
+ // Nothing more for prefix.
+ return entries, io.EOF
+ }
+ partN++
+ retries = 0
+ }
+ }
+}
+
+// getListQuorum interprets list quorum values and returns appropriate
+// acceptable quorum expected for list operations
+func getListQuorum(quorum string, driveCount int) int {
+ switch quorum {
+ case "disk":
+ return 1
+ case "reduced":
+ return 2
+ case "optimal":
+ return (driveCount + 1) / 2
+ case "auto":
+ return -1
+ }
+ // defaults to 'strict'
+ return driveCount
+}
+
+func calcCommonWritesDeletes(infos []DiskInfo, readQuorum int) (commonWrite, commonDelete uint64) {
+ deletes := make([]uint64, len(infos))
+ writes := make([]uint64, len(infos))
+ for index, di := range infos {
+ deletes[index] = di.Metrics.TotalDeletes
+ writes[index] = di.Metrics.TotalWrites
+ }
+
+ filter := func(list []uint64) (commonCount uint64) {
+ maxCnt := 0
+ signatureMap := map[uint64]int{}
+ for _, v := range list {
+ signatureMap[v]++
+ }
+ for ops, count := range signatureMap {
+ if maxCnt < count && commonCount < ops {
+ maxCnt = count
+ commonCount = ops
+ }
+ }
+ if maxCnt < readQuorum {
+ return 0
+ }
+ return commonCount
+ }
+
+ commonWrite = filter(writes)
+ commonDelete = filter(deletes)
+ return commonWrite, commonDelete
+}
+
+func calcCommonCounter(infos []DiskInfo, readQuorum int) (commonCount uint64) {
+ filter := func() (commonCount uint64) {
+ maxCnt := 0
+ signatureMap := map[uint64]int{}
+ for _, info := range infos {
+ if info.Error != "" {
+ continue
+ }
+ mutations := info.Metrics.TotalDeletes + info.Metrics.TotalWrites
+ signatureMap[mutations]++
+ }
+ for ops, count := range signatureMap {
+ if maxCnt < count && commonCount < ops {
+ maxCnt = count
+ commonCount = ops
+ }
+ }
+ if maxCnt < readQuorum {
+ return 0
+ }
+ return commonCount
+ }
+
+ return filter()
+}
+
+func getQuorumDiskInfos(disks []StorageAPI, infos []DiskInfo, readQuorum int) (newDisks []StorageAPI, newInfos []DiskInfo) {
+ commonMutations := calcCommonCounter(infos, readQuorum)
+ for i, info := range infos {
+ mutations := info.Metrics.TotalDeletes + info.Metrics.TotalWrites
+ if mutations >= commonMutations {
+ newDisks = append(newDisks, disks[i])
+ newInfos = append(newInfos, infos[i])
+ }
+ }
+
+ return newDisks, newInfos
+}
+
+func getQuorumDisks(disks []StorageAPI, infos []DiskInfo, readQuorum int) (newDisks []StorageAPI) {
+ newDisks, _ = getQuorumDiskInfos(disks, infos, readQuorum)
+ return newDisks
+}
+
+// Will return io.EOF if continuing would not yield more results.
+func (er *erasureObjects) listPath(ctx context.Context, o listPathOptions, results chan<- metaCacheEntry) (err error) {
+ defer xioutil.SafeClose(results)
+ o.debugf(color.Green("listPath:")+" with options: %#v", o)
+
+ // get prioritized non-healing disks for listing
+ disks, infos, _ := er.getOnlineDisksWithHealingAndInfo(true)
+ askDisks := getListQuorum(o.AskDisks, er.setDriveCount)
+ if askDisks == -1 {
+ newDisks := getQuorumDisks(disks, infos, (len(disks)+1)/2)
+ if newDisks != nil {
+ // If we found disks signature in quorum, we proceed to list
+ // from a single drive, shuffling of the drives is subsequently.
+ disks = newDisks
+ askDisks = 1
+ } else {
+ // If we did not find suitable disks, perform strict quorum listing
+ // as no disk agrees on quorum anymore.
+ askDisks = getListQuorum("strict", er.setDriveCount)
+ }
+ }
+
+ var fallbackDisks []StorageAPI
+
+ // Special case: ask all disks if the drive count is 4
+ if er.setDriveCount == 4 || askDisks > len(disks) {
+ askDisks = len(disks) // use all available drives
+ }
+
+ // However many we ask, versions must exist on ~50%
+ listingQuorum := (askDisks + 1) / 2
+
+ if askDisks > 0 && len(disks) > askDisks {
+ rand.Shuffle(len(disks), func(i, j int) {
+ disks[i], disks[j] = disks[j], disks[i]
+ })
+ fallbackDisks = disks[askDisks:]
+ disks = disks[:askDisks]
+ }
+
+ // How to resolve results.
+ resolver := metadataResolutionParams{
+ dirQuorum: listingQuorum,
+ objQuorum: listingQuorum,
+ bucket: o.Bucket,
+ }
+
+ // Maximum versions requested for "latest" object
+ // resolution on versioned buckets, this is to be only
+ // used when o.Versioned is false
+ if !o.Versioned {
+ resolver.requestedVersions = 1
+ }
+ var limit int
+ if o.Limit > 0 && o.StopDiskAtLimit {
+ // Over-read by 4 + 1 for every 16 in limit to give some space for resolver,
+ // allow for truncating the list and know if we have more results.
+ limit = o.Limit + 4 + (o.Limit / 16)
+ }
+ ctxDone := ctx.Done()
+ return listPathRaw(ctx, listPathRawOptions{
+ disks: disks,
+ fallbackDisks: fallbackDisks,
+ bucket: o.Bucket,
+ path: o.BaseDir,
+ recursive: o.Recursive,
+ filterPrefix: o.FilterPrefix,
+ minDisks: listingQuorum,
+ forwardTo: o.Marker,
+ perDiskLimit: limit,
+ agreed: func(entry metaCacheEntry) {
+ select {
+ case <-ctxDone:
+ case results <- entry:
+ }
+ },
+ partial: func(entries metaCacheEntries, errs []error) {
+ // Results Disagree :-(
+ entry, ok := entries.resolve(&resolver)
+ if ok {
+ select {
+ case <-ctxDone:
+ case results <- *entry:
+ }
+ }
+ },
+ })
+}
+
+//msgp:ignore metaCacheRPC
+type metaCacheRPC struct {
+ o listPathOptions
+ mu sync.Mutex
+ meta *metacache
+ rpc *peerRESTClient
+ cancel context.CancelFunc
+}
+
+func (m *metaCacheRPC) setErr(err string) {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ meta := *m.meta
+ if meta.status != scanStateError {
+ meta.error = err
+ meta.status = scanStateError
+ } else {
+ // An error is already set.
+ return
+ }
+ meta, _ = m.o.updateMetacacheListing(meta, m.rpc)
+ *m.meta = meta
+}
+
+// getErr will return an error if the listing failed.
+// The error is not type safe.
+func (m *metaCacheRPC) getErr() error {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ if m.meta.status == scanStateError {
+ return errors.New(m.meta.error)
+ }
+ return nil
+}
+
+func (er *erasureObjects) saveMetaCacheStream(ctx context.Context, mc *metaCacheRPC, entries <-chan metaCacheEntry) (err error) {
+ o := mc.o
+ o.debugf(color.Green("saveMetaCacheStream:")+" with options: %#v", o)
+
+ metaMu := &mc.mu
+ rpc := mc.rpc
+ cancel := mc.cancel
+ defer func() {
+ o.debugln(color.Green("saveMetaCacheStream:")+"err:", err)
+ if err != nil && !errors.Is(err, io.EOF) {
+ go mc.setErr(err.Error())
+ cancel()
+ }
+ }()
+
+ defer cancel()
+ // Save continuous updates
+ go func() {
+ var err error
+ ticker := time.NewTicker(10 * time.Second)
+ defer ticker.Stop()
+ var exit bool
+ for !exit {
+ select {
+ case <-ticker.C:
+ case <-ctx.Done():
+ exit = true
+ }
+ metaMu.Lock()
+ meta := *mc.meta
+ meta, err = o.updateMetacacheListing(meta, rpc)
+ if err == nil && time.Since(meta.lastHandout) > metacacheMaxClientWait {
+ cancel()
+ exit = true
+ meta.status = scanStateError
+ meta.error = fmt.Sprintf("listing canceled since time since last handout was %v ago", time.Since(meta.lastHandout).Round(time.Second))
+ o.debugln(color.Green("saveMetaCacheStream: ") + meta.error)
+ meta, err = o.updateMetacacheListing(meta, rpc)
+ }
+ if err == nil {
+ *mc.meta = meta
+ if meta.status == scanStateError {
+ cancel()
+ exit = true
+ }
+ }
+ metaMu.Unlock()
+ }
+ }()
+
+ const retryDelay = 200 * time.Millisecond
+ const maxTries = 5
+
+ // Keep destination...
+ // Write results to disk.
+ bw := newMetacacheBlockWriter(entries, func(b *metacacheBlock) error {
+ // if the block is 0 bytes and its a first block skip it.
+ // skip only this for Transient caches.
+ if len(b.data) == 0 && b.n == 0 && o.Transient {
+ return nil
+ }
+ o.debugln(color.Green("saveMetaCacheStream:")+" saving block", b.n, "to", o.objectPath(b.n))
+ r, err := hash.NewReader(ctx, bytes.NewReader(b.data), int64(len(b.data)), "", "", int64(len(b.data)))
+ bugLogIf(ctx, err)
+ custom := b.headerKV()
+ _, err = er.putMetacacheObject(ctx, o.objectPath(b.n), NewPutObjReader(r), ObjectOptions{
+ UserDefined: custom,
+ })
+ if err != nil {
+ mc.setErr(err.Error())
+ cancel()
+ return err
+ }
+ if b.n == 0 {
+ return nil
+ }
+ // Update block 0 metadata.
+ var retries int
+ for {
+ meta := b.headerKV()
+ fi := FileInfo{
+ Metadata: make(map[string]string, len(meta)),
+ }
+ maps.Copy(fi.Metadata, meta)
+ err := er.updateObjectMetaWithOpts(ctx, minioMetaBucket, o.objectPath(0), fi, er.getDisks(), UpdateMetadataOpts{NoPersistence: true})
+ if err == nil {
+ break
+ }
+ switch err.(type) {
+ case ObjectNotFound:
+ return err
+ case StorageErr:
+ return err
+ case InsufficientReadQuorum:
+ default:
+ internalLogIf(ctx, err)
+ }
+ if retries >= maxTries {
+ return err
+ }
+ retries++
+ time.Sleep(retryDelay)
+ }
+ return nil
+ })
+
+ // Blocks while consuming entries or an error occurs.
+ err = bw.Close()
+ if err != nil {
+ mc.setErr(err.Error())
+ }
+ metaMu.Lock()
+ defer metaMu.Unlock()
+ if mc.meta.error != "" {
+ return err
+ }
+ // Save success
+ mc.meta.status = scanStateSuccess
+ meta, err := o.updateMetacacheListing(*mc.meta, rpc)
+ if err == nil {
+ *mc.meta = meta
+ }
+ return nil
+}
+
+//msgp:ignore listPathRawOptions
+type listPathRawOptions struct {
+ disks []StorageAPI
+ fallbackDisks []StorageAPI
+ bucket, path string
+ recursive bool
+
+ // Only return results with this prefix.
+ filterPrefix string
+
+ // Forward to this prefix before returning results.
+ forwardTo string
+
+ // Minimum number of good disks to continue.
+ // An error will be returned if this many disks returned an error.
+ minDisks int
+ reportNotFound bool
+
+ // perDiskLimit will limit each disk to return n objects.
+ // If <= 0 all results will be returned until canceled.
+ perDiskLimit int
+
+ // Callbacks with results:
+ // If set to nil, it will not be called.
+
+ // agreed is called if all disks agreed.
+ agreed func(entry metaCacheEntry)
+
+ // partial will be called when there is disagreement between disks.
+ // if disk did not return any result, but also haven't errored
+ // the entry will be empty and errs will
+ partial func(entries metaCacheEntries, errs []error)
+
+ // finished will be called when all streams have finished and
+ // more than one disk returned an error.
+ // Will not be called if everything operates as expected.
+ finished func(errs []error)
+}
+
+// listPathRaw will list a path on the provided drives.
+// See listPathRawOptions on how results are delivered.
+// Directories are always returned.
+// Cache will be bypassed.
+// Context cancellation will be respected but may take a while to effectuate.
+func listPathRaw(ctx context.Context, opts listPathRawOptions) (err error) {
+ disks := opts.disks
+ if len(disks) == 0 {
+ return fmt.Errorf("listPathRaw: 0 drives provided")
+ }
+
+ // Cancel upstream if we finish before we expect.
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ // Keep track of fallback disks
+ var fdMu sync.Mutex
+ fds := opts.fallbackDisks
+ fallback := func(err error) StorageAPI {
+ if _, ok := err.(StorageErr); ok {
+ // Attempt to grab a fallback disk
+ fdMu.Lock()
+ defer fdMu.Unlock()
+ if len(fds) == 0 {
+ return nil
+ }
+ fdsCopy := fds
+ for _, fd := range fdsCopy {
+ // Grab a fallback disk
+ fds = fds[1:]
+ if fd != nil && fd.IsOnline() {
+ return fd
+ }
+ }
+ }
+ // Either no more disks for fallback or
+ // not a storage error.
+ return nil
+ }
+ readers := make([]*metacacheReader, len(disks))
+ defer func() {
+ for _, r := range readers {
+ r.Close()
+ }
+ }()
+ for i := range disks {
+ r, w := io.Pipe()
+ // Make sure we close the pipe so blocked writes doesn't stay around.
+ defer r.CloseWithError(context.Canceled)
+
+ readers[i] = newMetacacheReader(r)
+ d := disks[i]
+
+ // Send request to each disk.
+ go func() {
+ var werr error
+ if d == nil {
+ werr = errDiskNotFound
+ } else {
+ werr = d.WalkDir(ctx, WalkDirOptions{
+ Limit: opts.perDiskLimit,
+ Bucket: opts.bucket,
+ BaseDir: opts.path,
+ Recursive: opts.recursive,
+ ReportNotFound: opts.reportNotFound,
+ FilterPrefix: opts.filterPrefix,
+ ForwardTo: opts.forwardTo,
+ }, w)
+ }
+
+ // fallback only when set.
+ for {
+ fd := fallback(werr)
+ if fd == nil {
+ break
+ }
+ // This fallback is only set when
+ // askDisks is less than total
+ // number of disks per set.
+ werr = fd.WalkDir(ctx, WalkDirOptions{
+ Limit: opts.perDiskLimit,
+ Bucket: opts.bucket,
+ BaseDir: opts.path,
+ Recursive: opts.recursive,
+ ReportNotFound: opts.reportNotFound,
+ FilterPrefix: opts.filterPrefix,
+ ForwardTo: opts.forwardTo,
+ }, w)
+ if werr == nil {
+ break
+ }
+ }
+ w.CloseWithError(werr)
+ }()
+ }
+
+ topEntries := make(metaCacheEntries, len(readers))
+ errs := make([]error, len(readers))
+ for {
+ // Get the top entry from each
+ var current metaCacheEntry
+ var atEOF, fnf, vnf, hasErr, agree int
+ for i := range topEntries {
+ topEntries[i] = metaCacheEntry{}
+ }
+ if contextCanceled(ctx) {
+ return ctx.Err()
+ }
+ for i, r := range readers {
+ if errs[i] != nil {
+ hasErr++
+ continue
+ }
+ entry, err := r.peek()
+ switch err {
+ case io.EOF:
+ atEOF++
+ continue
+ case nil:
+ default:
+ switch err.Error() {
+ case errFileNotFound.Error():
+ atEOF++
+ fnf++
+ continue
+ case errVolumeNotFound.Error():
+ atEOF++
+ fnf++
+ vnf++
+ continue
+ }
+ hasErr++
+ errs[i] = fmt.Errorf("drive: %s returned err: %v", disks[i], err)
+ continue
+ }
+ // If no current, add it.
+ if current.name == "" {
+ topEntries[i] = entry
+ current = entry
+ agree++
+ continue
+ }
+ // If exact match, we agree.
+ if _, ok := current.matches(&entry, true); ok {
+ topEntries[i] = entry
+ agree++
+ continue
+ }
+ // If only the name matches we didn't agree, but add it for resolution.
+ if entry.name == current.name {
+ topEntries[i] = entry
+ continue
+ }
+ // We got different entries
+ if entry.name > current.name {
+ continue
+ }
+ // We got a new, better current.
+ // Clear existing entries.
+ for i := range topEntries[:i] {
+ topEntries[i] = metaCacheEntry{}
+ }
+ agree = 1
+ current = entry
+ topEntries[i] = entry
+ }
+
+ // Since minDisks is set to quorum, we return if we have enough.
+ if vnf > 0 && vnf >= len(readers)-opts.minDisks {
+ return errVolumeNotFound
+ }
+ // Since minDisks is set to quorum, we return if we have enough.
+ if fnf > 0 && fnf >= len(readers)-opts.minDisks {
+ return errFileNotFound
+ }
+
+ // Stop if we exceed number of bad disks.
+ if hasErr > 0 && hasErr+fnf > len(disks)-opts.minDisks {
+ if opts.finished != nil {
+ opts.finished(errs)
+ }
+ return errors.Join(errs...)
+ }
+
+ // Break if all at EOF or error.
+ if atEOF+hasErr == len(readers) {
+ if hasErr > 0 && opts.finished != nil {
+ opts.finished(errs)
+ }
+ break
+ }
+
+ if agree == len(readers) {
+ // Everybody agreed
+ for _, r := range readers {
+ r.skip(1)
+ }
+ if opts.agreed != nil {
+ opts.agreed(current)
+ }
+ continue
+ }
+ if opts.partial != nil {
+ opts.partial(topEntries, errs)
+ }
+ // Skip the inputs we used.
+ for i, r := range readers {
+ if topEntries[i].name != "" {
+ r.skip(1)
+ }
+ }
+ }
+ return nil
+}
diff --git a/cmd/metacache-set_gen.go b/cmd/metacache-set_gen.go
new file mode 100644
index 00000000..8b29bfca
--- /dev/null
+++ b/cmd/metacache-set_gen.go
@@ -0,0 +1,560 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "github.com/tinylib/msgp/msgp"
+)
+
+// DecodeMsg implements msgp.Decodable
+func (z *listPathOptions) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ID":
+ z.ID, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "ID")
+ return
+ }
+ case "Bucket":
+ z.Bucket, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "BaseDir":
+ z.BaseDir, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "BaseDir")
+ return
+ }
+ case "Prefix":
+ z.Prefix, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Prefix")
+ return
+ }
+ case "FilterPrefix":
+ z.FilterPrefix, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "FilterPrefix")
+ return
+ }
+ case "Marker":
+ z.Marker, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Marker")
+ return
+ }
+ case "Limit":
+ z.Limit, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "Limit")
+ return
+ }
+ case "AskDisks":
+ z.AskDisks, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "AskDisks")
+ return
+ }
+ case "InclDeleted":
+ z.InclDeleted, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "InclDeleted")
+ return
+ }
+ case "Recursive":
+ z.Recursive, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "Recursive")
+ return
+ }
+ case "Separator":
+ z.Separator, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Separator")
+ return
+ }
+ case "Create":
+ z.Create, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "Create")
+ return
+ }
+ case "IncludeDirectories":
+ z.IncludeDirectories, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "IncludeDirectories")
+ return
+ }
+ case "Transient":
+ z.Transient, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "Transient")
+ return
+ }
+ case "Versioned":
+ z.Versioned, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "Versioned")
+ return
+ }
+ case "V1":
+ z.V1, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "V1")
+ return
+ }
+ case "StopDiskAtLimit":
+ z.StopDiskAtLimit, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "StopDiskAtLimit")
+ return
+ }
+ case "pool":
+ z.pool, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "pool")
+ return
+ }
+ case "set":
+ z.set, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "set")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *listPathOptions) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 19
+ // write "ID"
+ err = en.Append(0xde, 0x0, 0x13, 0xa2, 0x49, 0x44)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.ID)
+ if err != nil {
+ err = msgp.WrapError(err, "ID")
+ return
+ }
+ // write "Bucket"
+ err = en.Append(0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Bucket)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ // write "BaseDir"
+ err = en.Append(0xa7, 0x42, 0x61, 0x73, 0x65, 0x44, 0x69, 0x72)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.BaseDir)
+ if err != nil {
+ err = msgp.WrapError(err, "BaseDir")
+ return
+ }
+ // write "Prefix"
+ err = en.Append(0xa6, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Prefix)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefix")
+ return
+ }
+ // write "FilterPrefix"
+ err = en.Append(0xac, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.FilterPrefix)
+ if err != nil {
+ err = msgp.WrapError(err, "FilterPrefix")
+ return
+ }
+ // write "Marker"
+ err = en.Append(0xa6, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Marker)
+ if err != nil {
+ err = msgp.WrapError(err, "Marker")
+ return
+ }
+ // write "Limit"
+ err = en.Append(0xa5, 0x4c, 0x69, 0x6d, 0x69, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.Limit)
+ if err != nil {
+ err = msgp.WrapError(err, "Limit")
+ return
+ }
+ // write "AskDisks"
+ err = en.Append(0xa8, 0x41, 0x73, 0x6b, 0x44, 0x69, 0x73, 0x6b, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.AskDisks)
+ if err != nil {
+ err = msgp.WrapError(err, "AskDisks")
+ return
+ }
+ // write "InclDeleted"
+ err = en.Append(0xab, 0x49, 0x6e, 0x63, 0x6c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.InclDeleted)
+ if err != nil {
+ err = msgp.WrapError(err, "InclDeleted")
+ return
+ }
+ // write "Recursive"
+ err = en.Append(0xa9, 0x52, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.Recursive)
+ if err != nil {
+ err = msgp.WrapError(err, "Recursive")
+ return
+ }
+ // write "Separator"
+ err = en.Append(0xa9, 0x53, 0x65, 0x70, 0x61, 0x72, 0x61, 0x74, 0x6f, 0x72)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Separator)
+ if err != nil {
+ err = msgp.WrapError(err, "Separator")
+ return
+ }
+ // write "Create"
+ err = en.Append(0xa6, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.Create)
+ if err != nil {
+ err = msgp.WrapError(err, "Create")
+ return
+ }
+ // write "IncludeDirectories"
+ err = en.Append(0xb2, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x69, 0x65, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.IncludeDirectories)
+ if err != nil {
+ err = msgp.WrapError(err, "IncludeDirectories")
+ return
+ }
+ // write "Transient"
+ err = en.Append(0xa9, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x65, 0x6e, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.Transient)
+ if err != nil {
+ err = msgp.WrapError(err, "Transient")
+ return
+ }
+ // write "Versioned"
+ err = en.Append(0xa9, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.Versioned)
+ if err != nil {
+ err = msgp.WrapError(err, "Versioned")
+ return
+ }
+ // write "V1"
+ err = en.Append(0xa2, 0x56, 0x31)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.V1)
+ if err != nil {
+ err = msgp.WrapError(err, "V1")
+ return
+ }
+ // write "StopDiskAtLimit"
+ err = en.Append(0xaf, 0x53, 0x74, 0x6f, 0x70, 0x44, 0x69, 0x73, 0x6b, 0x41, 0x74, 0x4c, 0x69, 0x6d, 0x69, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.StopDiskAtLimit)
+ if err != nil {
+ err = msgp.WrapError(err, "StopDiskAtLimit")
+ return
+ }
+ // write "pool"
+ err = en.Append(0xa4, 0x70, 0x6f, 0x6f, 0x6c)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.pool)
+ if err != nil {
+ err = msgp.WrapError(err, "pool")
+ return
+ }
+ // write "set"
+ err = en.Append(0xa3, 0x73, 0x65, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.set)
+ if err != nil {
+ err = msgp.WrapError(err, "set")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *listPathOptions) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 19
+ // string "ID"
+ o = append(o, 0xde, 0x0, 0x13, 0xa2, 0x49, 0x44)
+ o = msgp.AppendString(o, z.ID)
+ // string "Bucket"
+ o = append(o, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ o = msgp.AppendString(o, z.Bucket)
+ // string "BaseDir"
+ o = append(o, 0xa7, 0x42, 0x61, 0x73, 0x65, 0x44, 0x69, 0x72)
+ o = msgp.AppendString(o, z.BaseDir)
+ // string "Prefix"
+ o = append(o, 0xa6, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78)
+ o = msgp.AppendString(o, z.Prefix)
+ // string "FilterPrefix"
+ o = append(o, 0xac, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78)
+ o = msgp.AppendString(o, z.FilterPrefix)
+ // string "Marker"
+ o = append(o, 0xa6, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72)
+ o = msgp.AppendString(o, z.Marker)
+ // string "Limit"
+ o = append(o, 0xa5, 0x4c, 0x69, 0x6d, 0x69, 0x74)
+ o = msgp.AppendInt(o, z.Limit)
+ // string "AskDisks"
+ o = append(o, 0xa8, 0x41, 0x73, 0x6b, 0x44, 0x69, 0x73, 0x6b, 0x73)
+ o = msgp.AppendString(o, z.AskDisks)
+ // string "InclDeleted"
+ o = append(o, 0xab, 0x49, 0x6e, 0x63, 0x6c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64)
+ o = msgp.AppendBool(o, z.InclDeleted)
+ // string "Recursive"
+ o = append(o, 0xa9, 0x52, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65)
+ o = msgp.AppendBool(o, z.Recursive)
+ // string "Separator"
+ o = append(o, 0xa9, 0x53, 0x65, 0x70, 0x61, 0x72, 0x61, 0x74, 0x6f, 0x72)
+ o = msgp.AppendString(o, z.Separator)
+ // string "Create"
+ o = append(o, 0xa6, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65)
+ o = msgp.AppendBool(o, z.Create)
+ // string "IncludeDirectories"
+ o = append(o, 0xb2, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x69, 0x65, 0x73)
+ o = msgp.AppendBool(o, z.IncludeDirectories)
+ // string "Transient"
+ o = append(o, 0xa9, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x65, 0x6e, 0x74)
+ o = msgp.AppendBool(o, z.Transient)
+ // string "Versioned"
+ o = append(o, 0xa9, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x64)
+ o = msgp.AppendBool(o, z.Versioned)
+ // string "V1"
+ o = append(o, 0xa2, 0x56, 0x31)
+ o = msgp.AppendBool(o, z.V1)
+ // string "StopDiskAtLimit"
+ o = append(o, 0xaf, 0x53, 0x74, 0x6f, 0x70, 0x44, 0x69, 0x73, 0x6b, 0x41, 0x74, 0x4c, 0x69, 0x6d, 0x69, 0x74)
+ o = msgp.AppendBool(o, z.StopDiskAtLimit)
+ // string "pool"
+ o = append(o, 0xa4, 0x70, 0x6f, 0x6f, 0x6c)
+ o = msgp.AppendInt(o, z.pool)
+ // string "set"
+ o = append(o, 0xa3, 0x73, 0x65, 0x74)
+ o = msgp.AppendInt(o, z.set)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *listPathOptions) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "ID":
+ z.ID, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ID")
+ return
+ }
+ case "Bucket":
+ z.Bucket, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "BaseDir":
+ z.BaseDir, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "BaseDir")
+ return
+ }
+ case "Prefix":
+ z.Prefix, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefix")
+ return
+ }
+ case "FilterPrefix":
+ z.FilterPrefix, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "FilterPrefix")
+ return
+ }
+ case "Marker":
+ z.Marker, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Marker")
+ return
+ }
+ case "Limit":
+ z.Limit, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Limit")
+ return
+ }
+ case "AskDisks":
+ z.AskDisks, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AskDisks")
+ return
+ }
+ case "InclDeleted":
+ z.InclDeleted, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "InclDeleted")
+ return
+ }
+ case "Recursive":
+ z.Recursive, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Recursive")
+ return
+ }
+ case "Separator":
+ z.Separator, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Separator")
+ return
+ }
+ case "Create":
+ z.Create, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Create")
+ return
+ }
+ case "IncludeDirectories":
+ z.IncludeDirectories, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "IncludeDirectories")
+ return
+ }
+ case "Transient":
+ z.Transient, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Transient")
+ return
+ }
+ case "Versioned":
+ z.Versioned, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Versioned")
+ return
+ }
+ case "V1":
+ z.V1, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "V1")
+ return
+ }
+ case "StopDiskAtLimit":
+ z.StopDiskAtLimit, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "StopDiskAtLimit")
+ return
+ }
+ case "pool":
+ z.pool, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "pool")
+ return
+ }
+ case "set":
+ z.set, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "set")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *listPathOptions) Msgsize() (s int) {
+ s = 3 + 3 + msgp.StringPrefixSize + len(z.ID) + 7 + msgp.StringPrefixSize + len(z.Bucket) + 8 + msgp.StringPrefixSize + len(z.BaseDir) + 7 + msgp.StringPrefixSize + len(z.Prefix) + 13 + msgp.StringPrefixSize + len(z.FilterPrefix) + 7 + msgp.StringPrefixSize + len(z.Marker) + 6 + msgp.IntSize + 9 + msgp.StringPrefixSize + len(z.AskDisks) + 12 + msgp.BoolSize + 10 + msgp.BoolSize + 10 + msgp.StringPrefixSize + len(z.Separator) + 7 + msgp.BoolSize + 19 + msgp.BoolSize + 10 + msgp.BoolSize + 10 + msgp.BoolSize + 3 + msgp.BoolSize + 16 + msgp.BoolSize + 5 + msgp.IntSize + 4 + msgp.IntSize
+ return
+}
diff --git a/cmd/metacache-set_gen_test.go b/cmd/metacache-set_gen_test.go
new file mode 100644
index 00000000..bee9830b
--- /dev/null
+++ b/cmd/metacache-set_gen_test.go
@@ -0,0 +1,123 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+func TestMarshalUnmarshallistPathOptions(t *testing.T) {
+ v := listPathOptions{}
+ 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 BenchmarkMarshalMsglistPathOptions(b *testing.B) {
+ v := listPathOptions{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsglistPathOptions(b *testing.B) {
+ v := listPathOptions{}
+ 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 BenchmarkUnmarshallistPathOptions(b *testing.B) {
+ v := listPathOptions{}
+ 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 TestEncodeDecodelistPathOptions(t *testing.T) {
+ v := listPathOptions{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodelistPathOptions Msgsize() is inaccurate")
+ }
+
+ vn := listPathOptions{}
+ 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 BenchmarkEncodelistPathOptions(b *testing.B) {
+ v := listPathOptions{}
+ 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 BenchmarkDecodelistPathOptions(b *testing.B) {
+ v := listPathOptions{}
+ 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)
+ }
+ }
+}
diff --git a/cmd/metacache-stream.go b/cmd/metacache-stream.go
new file mode 100644
index 00000000..cf61895f
--- /dev/null
+++ b/cmd/metacache-stream.go
@@ -0,0 +1,871 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+ "sync"
+
+ jsoniter "github.com/json-iterator/go"
+ "github.com/klauspost/compress/s2"
+ "github.com/minio/minio/internal/bpool"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/tinylib/msgp/msgp"
+ "github.com/valyala/bytebufferpool"
+)
+
+// metadata stream format:
+//
+// The stream is s2 compressed.
+// https://github.com/klauspost/compress/tree/master/s2#s2-compression
+// This ensures integrity and reduces the size typically by at least 50%.
+//
+// All stream elements are msgpack encoded.
+//
+// 1 Integer, metacacheStreamVersion of the writer.
+// This can be used for managing breaking changes.
+//
+// For each element:
+// 1. Bool. If false at end of stream.
+// 2. String. Name of object. Directories contains a trailing slash.
+// 3. Binary. Blob of metadata. Length 0 on directories.
+// ... Next element.
+//
+// Streams can be assumed to be sorted in ascending order.
+// If the stream ends before a false boolean it can be assumed it was truncated.
+
+const metacacheStreamVersion = 2
+
+// metacacheWriter provides a serializer of metacache objects.
+type metacacheWriter struct {
+ streamErr error
+ mw *msgp.Writer
+ creator func() error
+ closer func() error
+ blockSize int
+ streamWg sync.WaitGroup
+ reuseBlocks bool
+}
+
+// newMetacacheWriter will create a serializer that will write objects in given order to the output.
+// Provide a block size that affects latency. If 0 a default of 128KiB will be used.
+// Block size can be up to 4MiB.
+func newMetacacheWriter(out io.Writer, blockSize int) *metacacheWriter {
+ if blockSize < 8<<10 {
+ blockSize = 128 << 10
+ }
+ w := metacacheWriter{
+ mw: nil,
+ blockSize: blockSize,
+ }
+ w.creator = func() error {
+ s2w := s2.NewWriter(out, s2.WriterBlockSize(blockSize), s2.WriterConcurrency(2))
+ w.mw = msgp.NewWriter(s2w)
+ w.creator = nil
+ if err := w.mw.WriteByte(metacacheStreamVersion); err != nil {
+ return err
+ }
+
+ w.closer = func() (err error) {
+ defer func() {
+ cerr := s2w.Close()
+ if err == nil && cerr != nil {
+ err = cerr
+ }
+ }()
+ if w.streamErr != nil {
+ return w.streamErr
+ }
+ if err = w.mw.WriteBool(false); err != nil {
+ return err
+ }
+ return w.mw.Flush()
+ }
+ return nil
+ }
+ return &w
+}
+
+// write one or more objects to the stream in order.
+// It is favorable to send as many objects as possible in a single write,
+// but no more than math.MaxUint32
+func (w *metacacheWriter) write(objs ...metaCacheEntry) error {
+ if w == nil {
+ return errors.New("metacacheWriter: nil writer")
+ }
+ if len(objs) == 0 {
+ return nil
+ }
+ if w.creator != nil {
+ err := w.creator()
+ w.creator = nil
+ if err != nil {
+ return fmt.Errorf("metacacheWriter: unable to create writer: %w", err)
+ }
+ if w.mw == nil {
+ return errors.New("metacacheWriter: writer not initialized")
+ }
+ }
+ for _, o := range objs {
+ if len(o.name) == 0 {
+ return errors.New("metacacheWriter: no name provided")
+ }
+ // Indicate EOS
+ err := w.mw.WriteBool(true)
+ if err != nil {
+ return err
+ }
+ err = w.mw.WriteString(o.name)
+ if err != nil {
+ return err
+ }
+ err = w.mw.WriteBytes(o.metadata)
+ if err != nil {
+ return err
+ }
+ if w.reuseBlocks || o.reusable {
+ metaDataPoolPut(o.metadata)
+ }
+ }
+
+ return nil
+}
+
+// stream entries to the output.
+// The returned channel should be closed when done.
+// Any error is reported when closing the metacacheWriter.
+func (w *metacacheWriter) stream() (chan<- metaCacheEntry, error) {
+ if w.creator != nil {
+ err := w.creator()
+ w.creator = nil
+ if err != nil {
+ return nil, fmt.Errorf("metacacheWriter: unable to create writer: %w", err)
+ }
+ if w.mw == nil {
+ return nil, errors.New("metacacheWriter: writer not initialized")
+ }
+ }
+ objs := make(chan metaCacheEntry, 100)
+ w.streamErr = nil
+ w.streamWg.Add(1)
+ go func() {
+ defer w.streamWg.Done()
+ for o := range objs {
+ if len(o.name) == 0 || w.streamErr != nil {
+ continue
+ }
+ // Indicate EOS
+ err := w.mw.WriteBool(true)
+ if err != nil {
+ w.streamErr = err
+ continue
+ }
+ err = w.mw.WriteString(o.name)
+ if err != nil {
+ w.streamErr = err
+ continue
+ }
+ err = w.mw.WriteBytes(o.metadata)
+ if w.reuseBlocks || o.reusable {
+ metaDataPoolPut(o.metadata)
+ }
+ if err != nil {
+ w.streamErr = err
+ continue
+ }
+ }
+ }()
+
+ return objs, nil
+}
+
+// Close and release resources.
+func (w *metacacheWriter) Close() error {
+ if w == nil || w.closer == nil {
+ return nil
+ }
+ w.streamWg.Wait()
+ err := w.closer()
+ w.closer = nil
+ return err
+}
+
+// Reset and start writing to new writer.
+// Close must have been called before this.
+func (w *metacacheWriter) Reset(out io.Writer) {
+ w.streamErr = nil
+ w.creator = func() error {
+ s2w := s2.NewWriter(out, s2.WriterBlockSize(w.blockSize), s2.WriterConcurrency(2))
+ w.mw = msgp.NewWriter(s2w)
+ w.creator = nil
+ if err := w.mw.WriteByte(metacacheStreamVersion); err != nil {
+ return err
+ }
+
+ w.closer = func() error {
+ if w.streamErr != nil {
+ return w.streamErr
+ }
+ if err := w.mw.WriteBool(false); err != nil {
+ return err
+ }
+ if err := w.mw.Flush(); err != nil {
+ return err
+ }
+ return s2w.Close()
+ }
+ return nil
+ }
+}
+
+var s2DecPool = bpool.Pool[*s2.Reader]{New: func() *s2.Reader {
+ // Default alloc block for network transfer.
+ return s2.NewReader(nil, s2.ReaderAllocBlock(16<<10))
+}}
+
+// metacacheReader allows reading a cache stream.
+type metacacheReader struct {
+ mr *msgp.Reader
+ current metaCacheEntry
+ err error // stateful error
+ closer func()
+ creator func() error
+}
+
+// newMetacacheReader creates a new cache reader.
+// Nothing will be read from the stream yet.
+func newMetacacheReader(r io.Reader) *metacacheReader {
+ dec := s2DecPool.Get()
+ dec.Reset(r)
+ mr := msgpNewReader(dec)
+ return &metacacheReader{
+ mr: mr,
+ closer: func() {
+ dec.Reset(nil)
+ s2DecPool.Put(dec)
+ readMsgpReaderPoolPut(mr)
+ },
+ creator: func() error {
+ v, err := mr.ReadByte()
+ if err != nil {
+ return err
+ }
+ switch v {
+ case 1, 2:
+ default:
+ return fmt.Errorf("metacacheReader: Unknown version: %d", v)
+ }
+ return nil
+ },
+ }
+}
+
+func (r *metacacheReader) checkInit() {
+ if r.creator == nil || r.err != nil {
+ return
+ }
+ r.err = r.creator()
+ r.creator = nil
+}
+
+// peek will return the name of the next object.
+// Will return io.EOF if there are no more objects.
+// Should be used sparingly.
+func (r *metacacheReader) peek() (metaCacheEntry, error) {
+ r.checkInit()
+ if r.err != nil {
+ return metaCacheEntry{}, r.err
+ }
+ if r.current.name != "" {
+ return r.current, nil
+ }
+ if more, err := r.mr.ReadBool(); !more {
+ switch err {
+ case nil:
+ r.err = io.EOF
+ return metaCacheEntry{}, io.EOF
+ case io.EOF:
+ r.err = io.ErrUnexpectedEOF
+ return metaCacheEntry{}, io.ErrUnexpectedEOF
+ }
+ r.err = err
+ return metaCacheEntry{}, err
+ }
+
+ var err error
+ if r.current.name, err = r.mr.ReadString(); err != nil {
+ if err == io.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ r.err = err
+ return metaCacheEntry{}, err
+ }
+ r.current.metadata, err = r.mr.ReadBytes(r.current.metadata[:0])
+ if err == io.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ r.err = err
+ return r.current, err
+}
+
+// next will read one entry from the stream.
+// Generally not recommended for fast operation.
+func (r *metacacheReader) next() (metaCacheEntry, error) {
+ r.checkInit()
+ if r.err != nil {
+ return metaCacheEntry{}, r.err
+ }
+ var m metaCacheEntry
+ var err error
+ if r.current.name != "" {
+ m.name = r.current.name
+ m.metadata = r.current.metadata
+ r.current.name = ""
+ r.current.metadata = nil
+ return m, nil
+ }
+ if more, err := r.mr.ReadBool(); !more {
+ switch err {
+ case nil:
+ r.err = io.EOF
+ return m, io.EOF
+ case io.EOF:
+ r.err = io.ErrUnexpectedEOF
+ return m, io.ErrUnexpectedEOF
+ }
+ r.err = err
+ return m, err
+ }
+ if m.name, err = r.mr.ReadString(); err != nil {
+ if err == io.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ r.err = err
+ return m, err
+ }
+ m.metadata, err = r.mr.ReadBytes(metaDataPoolGet())
+ if err == io.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ if len(m.metadata) == 0 && cap(m.metadata) >= metaDataReadDefault {
+ metaDataPoolPut(m.metadata)
+ m.metadata = nil
+ }
+ r.err = err
+ return m, err
+}
+
+// next will read one entry from the stream.
+// Generally not recommended for fast operation.
+func (r *metacacheReader) nextEOF() bool {
+ r.checkInit()
+ if r.err != nil {
+ return r.err == io.EOF
+ }
+ if r.current.name != "" {
+ return false
+ }
+ _, err := r.peek()
+ if err != nil {
+ r.err = err
+ return r.err == io.EOF
+ }
+ return false
+}
+
+// forwardTo will forward to the first entry that is >= s.
+// Will return io.EOF if end of stream is reached without finding any.
+func (r *metacacheReader) forwardTo(s string) error {
+ r.checkInit()
+ if r.err != nil {
+ return r.err
+ }
+
+ if s == "" {
+ return nil
+ }
+ if r.current.name != "" {
+ if r.current.name >= s {
+ return nil
+ }
+ r.current.name = ""
+ r.current.metadata = nil
+ }
+ // temporary name buffer.
+ tmp := make([]byte, 0, 256)
+ for {
+ if more, err := r.mr.ReadBool(); !more {
+ switch err {
+ case nil:
+ r.err = io.EOF
+ return io.EOF
+ case io.EOF:
+ r.err = io.ErrUnexpectedEOF
+ return io.ErrUnexpectedEOF
+ }
+ r.err = err
+ return err
+ }
+ // Read name without allocating more than 1 buffer.
+ sz, err := r.mr.ReadStringHeader()
+ if err != nil {
+ r.err = err
+ return err
+ }
+ if cap(tmp) < int(sz) {
+ tmp = make([]byte, 0, sz+256)
+ }
+ tmp = tmp[:sz]
+ _, err = r.mr.R.ReadFull(tmp)
+ if err != nil {
+ r.err = err
+ return err
+ }
+ if string(tmp) >= s {
+ r.current.name = string(tmp)
+ r.current.metadata, r.err = r.mr.ReadBytes(nil)
+ return r.err
+ }
+ // Skip metadata
+ err = r.mr.Skip()
+ if err != nil {
+ if err == io.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ r.err = err
+ return err
+ }
+ }
+}
+
+// readN will return all the requested number of entries in order
+// or all if n < 0.
+// Will return io.EOF if end of stream is reached.
+// If requesting 0 objects nil error will always be returned regardless of at end of stream.
+// Use peek to determine if at end of stream.
+func (r *metacacheReader) readN(n int, inclDeleted, inclDirs, inclVersions bool, prefix string) (metaCacheEntriesSorted, error) {
+ r.checkInit()
+ if n == 0 {
+ return metaCacheEntriesSorted{}, nil
+ }
+ if r.err != nil {
+ return metaCacheEntriesSorted{}, r.err
+ }
+
+ var res metaCacheEntries
+ if n > 0 {
+ res = make(metaCacheEntries, 0, n)
+ }
+ if prefix != "" {
+ if err := r.forwardTo(prefix); err != nil {
+ return metaCacheEntriesSorted{}, err
+ }
+ }
+ next, err := r.peek()
+ if err != nil {
+ return metaCacheEntriesSorted{}, err
+ }
+ if !next.hasPrefix(prefix) {
+ return metaCacheEntriesSorted{}, io.EOF
+ }
+
+ if r.current.name != "" {
+ if (inclDeleted || !r.current.isLatestDeletemarker()) && r.current.hasPrefix(prefix) && (inclDirs || r.current.isObject()) {
+ res = append(res, r.current)
+ }
+ r.current.name = ""
+ r.current.metadata = nil
+ }
+
+ for n < 0 || len(res) < n {
+ if more, err := r.mr.ReadBool(); !more {
+ switch err {
+ case nil:
+ r.err = io.EOF
+ return metaCacheEntriesSorted{o: res}, io.EOF
+ case io.EOF:
+ r.err = io.ErrUnexpectedEOF
+ return metaCacheEntriesSorted{o: res}, io.ErrUnexpectedEOF
+ }
+ r.err = err
+ return metaCacheEntriesSorted{o: res}, err
+ }
+ var err error
+ var meta metaCacheEntry
+ if meta.name, err = r.mr.ReadString(); err != nil {
+ if err == io.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ r.err = err
+ return metaCacheEntriesSorted{o: res}, err
+ }
+ if !meta.hasPrefix(prefix) {
+ r.mr.R.Skip(1)
+ return metaCacheEntriesSorted{o: res}, io.EOF
+ }
+ if meta.metadata, err = r.mr.ReadBytes(metaDataPoolGet()); err != nil {
+ if err == io.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ r.err = err
+ return metaCacheEntriesSorted{o: res}, err
+ }
+ if len(meta.metadata) == 0 {
+ metaDataPoolPut(meta.metadata)
+ meta.metadata = nil
+ }
+ if !inclDirs && (meta.isDir() || (!inclVersions && meta.isObjectDir() && meta.isLatestDeletemarker())) {
+ continue
+ }
+ if !inclDeleted && meta.isLatestDeletemarker() && meta.isObject() && !meta.isObjectDir() {
+ continue
+ }
+ res = append(res, meta)
+ }
+ return metaCacheEntriesSorted{o: res}, nil
+}
+
+// readAll will return all remaining objects on the dst channel and close it when done.
+// The context allows the operation to be canceled.
+func (r *metacacheReader) readAll(ctx context.Context, dst chan<- metaCacheEntry) error {
+ r.checkInit()
+ if r.err != nil {
+ return r.err
+ }
+ defer xioutil.SafeClose(dst)
+ if r.current.name != "" {
+ select {
+ case <-ctx.Done():
+ r.err = ctx.Err()
+ return ctx.Err()
+ case dst <- r.current:
+ }
+ r.current.name = ""
+ r.current.metadata = nil
+ }
+ for {
+ if more, err := r.mr.ReadBool(); !more {
+ if err == io.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ r.err = err
+ return err
+ }
+
+ var err error
+ var meta metaCacheEntry
+ if meta.name, err = r.mr.ReadString(); err != nil {
+ if err == io.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ r.err = err
+ return err
+ }
+ if meta.metadata, err = r.mr.ReadBytes(metaDataPoolGet()); err != nil {
+ if err == io.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ r.err = err
+ return err
+ }
+ if len(meta.metadata) == 0 {
+ metaDataPoolPut(meta.metadata)
+ meta.metadata = nil
+ }
+ select {
+ case <-ctx.Done():
+ r.err = ctx.Err()
+ return ctx.Err()
+ case dst <- meta:
+ }
+ }
+}
+
+// readFn will return all remaining objects
+// and provide a callback for each entry read in order
+// as long as true is returned on the callback.
+func (r *metacacheReader) readFn(fn func(entry metaCacheEntry) bool) error {
+ r.checkInit()
+ if r.err != nil {
+ return r.err
+ }
+ if r.current.name != "" {
+ fn(r.current)
+ r.current.name = ""
+ r.current.metadata = nil
+ }
+ for {
+ if more, err := r.mr.ReadBool(); !more {
+ switch err {
+ case io.EOF:
+ r.err = io.ErrUnexpectedEOF
+ return io.ErrUnexpectedEOF
+ case nil:
+ r.err = io.EOF
+ return io.EOF
+ }
+ return err
+ }
+
+ var err error
+ var meta metaCacheEntry
+ if meta.name, err = r.mr.ReadString(); err != nil {
+ if err == io.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ r.err = err
+ return err
+ }
+ if meta.metadata, err = r.mr.ReadBytes(nil); err != nil {
+ if err == io.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ r.err = err
+ return err
+ }
+ // Send it!
+ if !fn(meta) {
+ return nil
+ }
+ }
+}
+
+// readNames will return all the requested number of names in order
+// or all if n < 0.
+// Will return io.EOF if end of stream is reached.
+func (r *metacacheReader) readNames(n int) ([]string, error) {
+ r.checkInit()
+ if r.err != nil {
+ return nil, r.err
+ }
+ if n == 0 {
+ return nil, nil
+ }
+ var res []string
+ if n > 0 {
+ res = make([]string, 0, n)
+ }
+ if r.current.name != "" {
+ res = append(res, r.current.name)
+ r.current.name = ""
+ r.current.metadata = nil
+ }
+ for n < 0 || len(res) < n {
+ if more, err := r.mr.ReadBool(); !more {
+ switch err {
+ case nil:
+ r.err = io.EOF
+ return res, io.EOF
+ case io.EOF:
+ r.err = io.ErrUnexpectedEOF
+ return res, io.ErrUnexpectedEOF
+ }
+ return res, err
+ }
+
+ var err error
+ var name string
+ if name, err = r.mr.ReadString(); err != nil {
+ r.err = err
+ return res, err
+ }
+ if err = r.mr.Skip(); err != nil {
+ if err == io.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ r.err = err
+ return res, err
+ }
+ res = append(res, name)
+ }
+ return res, nil
+}
+
+// skip n entries on the input stream.
+// If there are less entries left io.EOF is returned.
+func (r *metacacheReader) skip(n int) error {
+ r.checkInit()
+ if r.err != nil {
+ return r.err
+ }
+ if n <= 0 {
+ return nil
+ }
+ if r.current.name != "" {
+ n--
+ r.current.name = ""
+ r.current.metadata = nil
+ }
+ for n > 0 {
+ if more, err := r.mr.ReadBool(); !more {
+ switch err {
+ case nil:
+ r.err = io.EOF
+ return io.EOF
+ case io.EOF:
+ r.err = io.ErrUnexpectedEOF
+ return io.ErrUnexpectedEOF
+ }
+ return err
+ }
+
+ if err := r.mr.Skip(); err != nil {
+ if err == io.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ r.err = err
+ return err
+ }
+ if err := r.mr.Skip(); err != nil {
+ if err == io.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ r.err = err
+ return err
+ }
+ n--
+ }
+ return nil
+}
+
+// Close and release resources.
+func (r *metacacheReader) Close() error {
+ if r == nil || r.closer == nil {
+ return nil
+ }
+ r.closer()
+ r.closer = nil
+ r.creator = nil
+ return nil
+}
+
+// metacacheBlockWriter collects blocks and provides a callback to store them.
+type metacacheBlockWriter struct {
+ wg sync.WaitGroup
+ streamErr error
+ blockEntries int
+}
+
+// newMetacacheBlockWriter provides a streaming block writer.
+// Each block is the size of the capacity of the input channel.
+// The caller should close to indicate the stream has ended.
+func newMetacacheBlockWriter(in <-chan metaCacheEntry, nextBlock func(b *metacacheBlock) error) *metacacheBlockWriter {
+ w := metacacheBlockWriter{blockEntries: cap(in)}
+ w.wg.Add(1)
+ go func() {
+ defer w.wg.Done()
+ var current metacacheBlock
+ var n int
+
+ buf := bytebufferpool.Get()
+ defer func() {
+ buf.Reset()
+ bytebufferpool.Put(buf)
+ }()
+
+ block := newMetacacheWriter(buf, 1<<20)
+ defer block.Close()
+ finishBlock := func() {
+ if err := block.Close(); err != nil {
+ w.streamErr = err
+ return
+ }
+ current.data = buf.Bytes()
+ w.streamErr = nextBlock(¤t)
+ // Prepare for next
+ current.n++
+ buf.Reset()
+ block.Reset(buf)
+ current.First = ""
+ }
+ for o := range in {
+ if len(o.name) == 0 || w.streamErr != nil {
+ continue
+ }
+ if current.First == "" {
+ current.First = o.name
+ }
+
+ if n >= w.blockEntries-1 {
+ finishBlock()
+ n = 0
+ }
+ n++
+
+ w.streamErr = block.write(o)
+ if w.streamErr != nil {
+ continue
+ }
+ current.Last = o.name
+ }
+ if n > 0 || current.n == 0 {
+ current.EOS = true
+ finishBlock()
+ }
+ }()
+ return &w
+}
+
+// Close the stream.
+// The incoming channel must be closed before calling this.
+// Returns the first error the occurred during the writing if any.
+func (w *metacacheBlockWriter) Close() error {
+ w.wg.Wait()
+ return w.streamErr
+}
+
+type metacacheBlock struct {
+ data []byte
+ n int
+ First string `json:"f"`
+ Last string `json:"l"`
+ EOS bool `json:"eos,omitempty"`
+}
+
+func (b metacacheBlock) headerKV() map[string]string {
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ v, err := json.Marshal(b)
+ if err != nil {
+ bugLogIf(context.Background(), err) // Unlikely
+ return nil
+ }
+ return map[string]string{fmt.Sprintf("%s-metacache-part-%d", ReservedMetadataPrefixLower, b.n): string(v)}
+}
+
+// pastPrefix returns true if the given prefix is before start of the block.
+func (b metacacheBlock) pastPrefix(prefix string) bool {
+ if prefix == "" || strings.HasPrefix(b.First, prefix) {
+ return false
+ }
+ // We have checked if prefix matches, so we can do direct compare.
+ return b.First > prefix
+}
+
+// endedPrefix returns true if the given prefix ends within the block.
+func (b metacacheBlock) endedPrefix(prefix string) bool {
+ if prefix == "" || strings.HasPrefix(b.Last, prefix) {
+ return false
+ }
+
+ // We have checked if prefix matches, so we can do direct compare.
+ return b.Last > prefix
+}
diff --git a/cmd/metacache-stream_test.go b/cmd/metacache-stream_test.go
new file mode 100644
index 00000000..36286f68
--- /dev/null
+++ b/cmd/metacache-stream_test.go
@@ -0,0 +1,427 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "io"
+ "os"
+ "reflect"
+ "sync"
+ "testing"
+)
+
+var loadMetacacheSampleNames = []string{"src/compress/bzip2/", "src/compress/bzip2/bit_reader.go", "src/compress/bzip2/bzip2.go", "src/compress/bzip2/bzip2_test.go", "src/compress/bzip2/huffman.go", "src/compress/bzip2/move_to_front.go", "src/compress/bzip2/testdata/", "src/compress/bzip2/testdata/Isaac.Newton-Opticks.txt.bz2", "src/compress/bzip2/testdata/e.txt.bz2", "src/compress/bzip2/testdata/fail-issue5747.bz2", "src/compress/bzip2/testdata/pass-random1.bin", "src/compress/bzip2/testdata/pass-random1.bz2", "src/compress/bzip2/testdata/pass-random2.bin", "src/compress/bzip2/testdata/pass-random2.bz2", "src/compress/bzip2/testdata/pass-sawtooth.bz2", "src/compress/bzip2/testdata/random.data.bz2", "src/compress/flate/", "src/compress/flate/deflate.go", "src/compress/flate/deflate_test.go", "src/compress/flate/deflatefast.go", "src/compress/flate/dict_decoder.go", "src/compress/flate/dict_decoder_test.go", "src/compress/flate/example_test.go", "src/compress/flate/flate_test.go", "src/compress/flate/huffman_bit_writer.go", "src/compress/flate/huffman_bit_writer_test.go", "src/compress/flate/huffman_code.go", "src/compress/flate/inflate.go", "src/compress/flate/inflate_test.go", "src/compress/flate/reader_test.go", "src/compress/flate/testdata/", "src/compress/flate/testdata/huffman-null-max.dyn.expect", "src/compress/flate/testdata/huffman-null-max.dyn.expect-noinput", "src/compress/flate/testdata/huffman-null-max.golden", "src/compress/flate/testdata/huffman-null-max.in", "src/compress/flate/testdata/huffman-null-max.wb.expect", "src/compress/flate/testdata/huffman-null-max.wb.expect-noinput", "src/compress/flate/testdata/huffman-pi.dyn.expect", "src/compress/flate/testdata/huffman-pi.dyn.expect-noinput", "src/compress/flate/testdata/huffman-pi.golden", "src/compress/flate/testdata/huffman-pi.in", "src/compress/flate/testdata/huffman-pi.wb.expect", "src/compress/flate/testdata/huffman-pi.wb.expect-noinput", "src/compress/flate/testdata/huffman-rand-1k.dyn.expect", "src/compress/flate/testdata/huffman-rand-1k.dyn.expect-noinput", "src/compress/flate/testdata/huffman-rand-1k.golden", "src/compress/flate/testdata/huffman-rand-1k.in", "src/compress/flate/testdata/huffman-rand-1k.wb.expect", "src/compress/flate/testdata/huffman-rand-1k.wb.expect-noinput", "src/compress/flate/testdata/huffman-rand-limit.dyn.expect", "src/compress/flate/testdata/huffman-rand-limit.dyn.expect-noinput", "src/compress/flate/testdata/huffman-rand-limit.golden", "src/compress/flate/testdata/huffman-rand-limit.in", "src/compress/flate/testdata/huffman-rand-limit.wb.expect", "src/compress/flate/testdata/huffman-rand-limit.wb.expect-noinput", "src/compress/flate/testdata/huffman-rand-max.golden", "src/compress/flate/testdata/huffman-rand-max.in", "src/compress/flate/testdata/huffman-shifts.dyn.expect", "src/compress/flate/testdata/huffman-shifts.dyn.expect-noinput", "src/compress/flate/testdata/huffman-shifts.golden", "src/compress/flate/testdata/huffman-shifts.in", "src/compress/flate/testdata/huffman-shifts.wb.expect", "src/compress/flate/testdata/huffman-shifts.wb.expect-noinput", "src/compress/flate/testdata/huffman-text-shift.dyn.expect", "src/compress/flate/testdata/huffman-text-shift.dyn.expect-noinput", "src/compress/flate/testdata/huffman-text-shift.golden", "src/compress/flate/testdata/huffman-text-shift.in", "src/compress/flate/testdata/huffman-text-shift.wb.expect", "src/compress/flate/testdata/huffman-text-shift.wb.expect-noinput", "src/compress/flate/testdata/huffman-text.dyn.expect", "src/compress/flate/testdata/huffman-text.dyn.expect-noinput", "src/compress/flate/testdata/huffman-text.golden", "src/compress/flate/testdata/huffman-text.in", "src/compress/flate/testdata/huffman-text.wb.expect", "src/compress/flate/testdata/huffman-text.wb.expect-noinput", "src/compress/flate/testdata/huffman-zero.dyn.expect", "src/compress/flate/testdata/huffman-zero.dyn.expect-noinput", "src/compress/flate/testdata/huffman-zero.golden", "src/compress/flate/testdata/huffman-zero.in", "src/compress/flate/testdata/huffman-zero.wb.expect", "src/compress/flate/testdata/huffman-zero.wb.expect-noinput", "src/compress/flate/testdata/null-long-match.dyn.expect-noinput", "src/compress/flate/testdata/null-long-match.wb.expect-noinput", "src/compress/flate/token.go", "src/compress/flate/writer_test.go", "src/compress/gzip/", "src/compress/gzip/example_test.go", "src/compress/gzip/gunzip.go", "src/compress/gzip/gunzip_test.go", "src/compress/gzip/gzip.go", "src/compress/gzip/gzip_test.go", "src/compress/gzip/issue14937_test.go", "src/compress/gzip/testdata/", "src/compress/gzip/testdata/issue6550.gz.base64", "src/compress/lzw/", "src/compress/lzw/reader.go", "src/compress/lzw/reader_test.go", "src/compress/lzw/writer.go", "src/compress/lzw/writer_test.go", "src/compress/testdata/", "src/compress/testdata/e.txt", "src/compress/testdata/gettysburg.txt", "src/compress/testdata/pi.txt", "src/compress/zlib/", "src/compress/zlib/example_test.go", "src/compress/zlib/reader.go", "src/compress/zlib/reader_test.go", "src/compress/zlib/writer.go", "src/compress/zlib/writer_test.go"}
+
+func loadMetacacheSample(t testing.TB) *metacacheReader {
+ b, err := os.ReadFile("testdata/metacache.s2")
+ if err != nil {
+ t.Fatal(err)
+ }
+ return newMetacacheReader(bytes.NewReader(b))
+}
+
+func loadMetacacheSampleEntries(t testing.TB) metaCacheEntriesSorted {
+ r := loadMetacacheSample(t)
+ defer r.Close()
+ entries, err := r.readN(-1, false, true, false, "")
+ if err != io.EOF {
+ t.Fatal(err)
+ }
+
+ return entries
+}
+
+func Test_metacacheReader_readNames(t *testing.T) {
+ r := loadMetacacheSample(t)
+ defer r.Close()
+ names, err := r.readNames(-1)
+ if err != io.EOF {
+ t.Fatal(err)
+ }
+ want := loadMetacacheSampleNames
+ if !reflect.DeepEqual(names, want) {
+ t.Errorf("got unexpected result: %#v", names)
+ }
+}
+
+func Test_metacacheReader_readN(t *testing.T) {
+ r := loadMetacacheSample(t)
+ defer r.Close()
+ entries, err := r.readN(-1, false, true, false, "")
+ if err != io.EOF {
+ t.Fatal(err, entries.len())
+ }
+ want := loadMetacacheSampleNames
+ for i, entry := range entries.entries() {
+ if entry.name != want[i] {
+ t.Errorf("entry %d, want %q, got %q", i, want[i], entry.name)
+ }
+ }
+ if entries.len() != len(want) {
+ t.Fatal("unexpected length:", entries.len(), "want:", len(want))
+ }
+
+ want = want[:0]
+ entries, err = r.readN(0, false, true, false, "")
+ if err != nil {
+ t.Fatal(err, entries.len())
+ }
+ if entries.len() != len(want) {
+ t.Fatal("unexpected length:", entries.len(), "want:", len(want))
+ }
+
+ // Reload.
+ r = loadMetacacheSample(t)
+ defer r.Close()
+ entries, err = r.readN(0, false, true, false, "")
+ if err != nil {
+ t.Fatal(err, entries.len())
+ }
+ if entries.len() != len(want) {
+ t.Fatal("unexpected length:", entries.len(), "want:", len(want))
+ }
+
+ entries, err = r.readN(5, false, true, false, "")
+ if err != nil {
+ t.Fatal(err, entries.len())
+ }
+ want = loadMetacacheSampleNames[:5]
+ if entries.len() != len(want) {
+ t.Fatal("unexpected length:", entries.len(), "want:", len(want))
+ }
+
+ for i, entry := range entries.entries() {
+ if entry.name != want[i] {
+ t.Errorf("entry %d, want %q, got %q", i, want[i], entry.name)
+ }
+ }
+}
+
+func Test_metacacheReader_readNDirs(t *testing.T) {
+ r := loadMetacacheSample(t)
+ defer r.Close()
+ entries, err := r.readN(-1, false, true, false, "")
+ if err != io.EOF {
+ t.Fatal(err, entries.len())
+ }
+ want := loadMetacacheSampleNames
+ var noDirs []string
+ for i, entry := range entries.entries() {
+ if entry.name != want[i] {
+ t.Errorf("entry %d, want %q, got %q", i, want[i], entry.name)
+ }
+ if !entry.isDir() {
+ noDirs = append(noDirs, entry.name)
+ }
+ }
+ if entries.len() != len(want) {
+ t.Fatal("unexpected length:", entries.len(), "want:", len(want))
+ }
+
+ want = noDirs
+ r = loadMetacacheSample(t)
+ defer r.Close()
+ entries, err = r.readN(-1, false, false, false, "")
+ if err != io.EOF {
+ t.Fatal(err, entries.len())
+ }
+ for i, entry := range entries.entries() {
+ if entry.name != want[i] {
+ t.Errorf("entry %d, want %q, got %q", i, want[i], entry.name)
+ }
+ }
+ if entries.len() != len(want) {
+ t.Fatal("unexpected length:", entries.len(), "want:", len(want))
+ }
+
+ want = want[:0]
+ entries, err = r.readN(0, false, false, false, "")
+ if err != nil {
+ t.Fatal(err, entries.len())
+ }
+ if entries.len() != len(want) {
+ t.Fatal("unexpected length:", entries.len(), "want:", len(want))
+ }
+
+ // Reload.
+ r = loadMetacacheSample(t)
+ defer r.Close()
+ entries, err = r.readN(0, false, false, false, "")
+ if err != nil {
+ t.Fatal(err, entries.len())
+ }
+ if entries.len() != len(want) {
+ t.Fatal("unexpected length:", entries.len(), "want:", len(want))
+ }
+
+ entries, err = r.readN(5, false, false, false, "")
+ if err != nil {
+ t.Fatal(err, entries.len())
+ }
+ want = noDirs[:5]
+ if entries.len() != len(want) {
+ t.Fatal("unexpected length:", entries.len(), "want:", len(want))
+ }
+
+ for i, entry := range entries.entries() {
+ if entry.name != want[i] {
+ t.Errorf("entry %d, want %q, got %q", i, want[i], entry.name)
+ }
+ }
+}
+
+func Test_metacacheReader_readNPrefix(t *testing.T) {
+ r := loadMetacacheSample(t)
+ defer r.Close()
+ entries, err := r.readN(-1, false, true, false, "src/compress/bzip2/")
+ if err != io.EOF {
+ t.Fatal(err, entries.len())
+ }
+ want := loadMetacacheSampleNames[:16]
+ for i, entry := range entries.entries() {
+ if entry.name != want[i] {
+ t.Errorf("entry %d, want %q, got %q", i, want[i], entry.name)
+ }
+ }
+ if entries.len() != len(want) {
+ t.Fatal("unexpected length:", entries.len(), "want:", len(want))
+ }
+
+ r = loadMetacacheSample(t)
+ defer r.Close()
+ entries, err = r.readN(-1, false, true, false, "src/nonexist")
+ if err != io.EOF {
+ t.Fatal(err, entries.len())
+ }
+ want = loadMetacacheSampleNames[:0]
+ if entries.len() != len(want) {
+ t.Fatal("unexpected length:", entries.len(), "want:", len(want))
+ }
+ for i, entry := range entries.entries() {
+ if entry.name != want[i] {
+ t.Errorf("entry %d, want %q, got %q", i, want[i], entry.name)
+ }
+ }
+
+ r = loadMetacacheSample(t)
+ defer r.Close()
+ entries, err = r.readN(-1, false, true, false, "src/a")
+ if err != io.EOF {
+ t.Fatal(err, entries.len())
+ }
+ want = loadMetacacheSampleNames[:0]
+ if entries.len() != len(want) {
+ t.Fatal("unexpected length:", entries.len(), "want:", len(want))
+ }
+ for i, entry := range entries.entries() {
+ if entry.name != want[i] {
+ t.Errorf("entry %d, want %q, got %q", i, want[i], entry.name)
+ }
+ }
+
+ r = loadMetacacheSample(t)
+ defer r.Close()
+ entries, err = r.readN(-1, false, true, false, "src/compress/zlib/e")
+ if err != io.EOF {
+ t.Fatal(err, entries.len())
+ }
+ want = []string{"src/compress/zlib/example_test.go"}
+ if entries.len() != len(want) {
+ t.Fatal("unexpected length:", entries.len(), "want:", len(want))
+ }
+ for i, entry := range entries.entries() {
+ if entry.name != want[i] {
+ t.Errorf("entry %d, want %q, got %q", i, want[i], entry.name)
+ }
+ }
+}
+
+func Test_metacacheReader_readFn(t *testing.T) {
+ r := loadMetacacheSample(t)
+ defer r.Close()
+ i := 0
+ err := r.readFn(func(entry metaCacheEntry) bool {
+ want := loadMetacacheSampleNames[i]
+ if entry.name != want {
+ t.Errorf("entry %d, want %q, got %q", i, want, entry.name)
+ }
+ i++
+ return true
+ })
+ if err != io.EOF {
+ t.Fatal(err)
+ }
+}
+
+func Test_metacacheReader_readAll(t *testing.T) {
+ r := loadMetacacheSample(t)
+ defer r.Close()
+ var readErr error
+ objs := make(chan metaCacheEntry, 1)
+ var wg sync.WaitGroup
+ wg.Add(1)
+ go func() {
+ readErr = r.readAll(t.Context(), objs)
+ wg.Done()
+ }()
+ want := loadMetacacheSampleNames
+ i := 0
+ for entry := range objs {
+ if entry.name != want[i] {
+ t.Errorf("entry %d, want %q, got %q", i, want[i], entry.name)
+ }
+ i++
+ }
+ wg.Wait()
+ if readErr != nil {
+ t.Fatal(readErr)
+ }
+}
+
+func Test_metacacheReader_forwardTo(t *testing.T) {
+ r := loadMetacacheSample(t)
+ defer r.Close()
+ err := r.forwardTo("src/compress/zlib/reader_test.go")
+ if err != nil {
+ t.Fatal(err)
+ }
+ names, err := r.readNames(-1)
+ if err != io.EOF {
+ t.Fatal(err)
+ }
+ want := []string{"src/compress/zlib/reader_test.go", "src/compress/zlib/writer.go", "src/compress/zlib/writer_test.go"}
+ if !reflect.DeepEqual(names, want) {
+ t.Errorf("got unexpected result: %#v", names)
+ }
+
+ // Try with prefix
+ r = loadMetacacheSample(t)
+ err = r.forwardTo("src/compress/zlib/reader_t")
+ if err != nil {
+ t.Fatal(err)
+ }
+ names, err = r.readNames(-1)
+ if err != io.EOF {
+ t.Fatal(err)
+ }
+ if !reflect.DeepEqual(names, want) {
+ t.Errorf("got unexpected result: %#v", names)
+ }
+}
+
+func Test_metacacheReader_next(t *testing.T) {
+ r := loadMetacacheSample(t)
+ defer r.Close()
+ for i, want := range loadMetacacheSampleNames {
+ gotObj, err := r.next()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if gotObj.name != want {
+ t.Errorf("entry %d, want %q, got %q", i, want, gotObj.name)
+ }
+ }
+}
+
+func Test_metacacheReader_peek(t *testing.T) {
+ r := loadMetacacheSample(t)
+ defer r.Close()
+ for i, want := range loadMetacacheSampleNames {
+ got, err := r.peek()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got.name != want {
+ t.Errorf("entry %d, want %q, got %q", i, want, got.name)
+ }
+ gotObj, err := r.next()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if gotObj.name != want {
+ t.Errorf("entry %d, want %q, got %q", i, want, gotObj.name)
+ }
+ }
+}
+
+func Test_newMetacacheStream(t *testing.T) {
+ r := loadMetacacheSample(t)
+ var buf bytes.Buffer
+ w := newMetacacheWriter(&buf, 1<<20)
+ defer w.Close()
+ err := r.readFn(func(object metaCacheEntry) bool {
+ err := w.write(object)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return true
+ })
+ r.Close()
+ if err != io.EOF {
+ t.Fatal(err)
+ }
+ err = w.Close()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ r = newMetacacheReader(&buf)
+ defer r.Close()
+ names, err := r.readNames(-1)
+ if err != io.EOF {
+ t.Fatal(err)
+ }
+ want := loadMetacacheSampleNames
+ if !reflect.DeepEqual(names, want) {
+ t.Errorf("got unexpected result: %#v", names)
+ }
+}
+
+func Test_metacacheReader_skip(t *testing.T) {
+ r := loadMetacacheSample(t)
+ defer r.Close()
+ names, err := r.readNames(5)
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := loadMetacacheSampleNames[:5]
+ if !reflect.DeepEqual(names, want) {
+ t.Errorf("got unexpected result: %#v", names)
+ }
+ err = r.skip(5)
+ if err != nil {
+ t.Fatal(err)
+ }
+ names, err = r.readNames(5)
+ if err != nil {
+ t.Fatal(err)
+ }
+ want = loadMetacacheSampleNames[10:15]
+ if !reflect.DeepEqual(names, want) {
+ t.Errorf("got unexpected result: %#v", names)
+ }
+
+ err = r.skip(len(loadMetacacheSampleNames))
+ if err != io.EOF {
+ t.Fatal(err)
+ }
+}
diff --git a/cmd/metacache-walk.go b/cmd/metacache-walk.go
new file mode 100644
index 00000000..fa949d9a
--- /dev/null
+++ b/cmd/metacache-walk.go
@@ -0,0 +1,457 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "errors"
+ "io"
+ "sort"
+ "strings"
+
+ "github.com/minio/minio/internal/grid"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/valyala/bytebufferpool"
+)
+
+//go:generate msgp -file $GOFILE
+
+// WalkDirOptions provides options for WalkDir operations.
+type WalkDirOptions struct {
+ // Bucket to scanner
+ Bucket string
+
+ // Directory inside the bucket.
+ BaseDir string
+
+ // Do a full recursive scan.
+ Recursive bool
+
+ // ReportNotFound will return errFileNotFound if all disks reports the BaseDir cannot be found.
+ ReportNotFound bool
+
+ // FilterPrefix will only return results with given prefix within folder.
+ // Should never contain a slash.
+ FilterPrefix string
+
+ // ForwardTo will forward to the given object path.
+ ForwardTo string
+
+ // Limit the number of returned objects if > 0.
+ Limit int
+
+ // DiskID contains the disk ID of the disk.
+ // Leave empty to not check disk ID.
+ DiskID string
+}
+
+// supported FS for Nlink optimization in readdir.
+const (
+ xfs = "XFS"
+ ext4 = "EXT4"
+)
+
+// WalkDir will traverse a directory and return all entries found.
+// On success a sorted meta cache stream will be returned.
+// Metadata has data stripped, if any.
+// The function tries to quit as fast as the context is canceled to avoid further drive IO
+func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writer) (err error) {
+ legacyFS := s.fsType != xfs && s.fsType != ext4
+
+ s.RLock()
+ legacy := s.formatLegacy
+ s.RUnlock()
+
+ // Verify if volume is valid and it exists.
+ volumeDir, err := s.getVolDir(opts.Bucket)
+ if err != nil {
+ return err
+ }
+
+ if !skipAccessChecks(opts.Bucket) {
+ // Stat a volume entry.
+ if err = Access(volumeDir); err != nil {
+ return convertAccessError(err, errVolumeAccessDenied)
+ }
+ }
+
+ // Use a small block size to start sending quickly
+ w := newMetacacheWriter(wr, 16<<10)
+ w.reuseBlocks = true // We are not sharing results, so reuse buffers.
+ defer w.Close()
+ out, err := w.stream()
+ if err != nil {
+ return err
+ }
+ defer xioutil.SafeClose(out)
+ var objsReturned int
+
+ objReturned := func(metadata []byte) {
+ if opts.Limit <= 0 {
+ return
+ }
+ if m, _, _ := isIndexedMetaV2(metadata); m != nil && !m.AllHidden(true) {
+ objsReturned++
+ }
+ }
+ send := func(entry metaCacheEntry) error {
+ objReturned(entry.metadata)
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case out <- entry:
+ }
+ return nil
+ }
+
+ // Fast exit track to check if we are listing an object with
+ // a trailing slash, this will avoid to list the object content.
+ if HasSuffix(opts.BaseDir, SlashSeparator) {
+ metadata, err := s.readMetadata(ctx, pathJoin(volumeDir,
+ opts.BaseDir[:len(opts.BaseDir)-1]+globalDirSuffix,
+ xlStorageFormatFile))
+ diskHealthCheckOK(ctx, err)
+ if err == nil {
+ // if baseDir is already a directory object, consider it
+ // as part of the list call, this is AWS S3 specific
+ // behavior.
+ if err := send(metaCacheEntry{
+ name: opts.BaseDir,
+ metadata: metadata,
+ }); err != nil {
+ return err
+ }
+ } else {
+ st, sterr := Lstat(pathJoin(volumeDir, opts.BaseDir, xlStorageFormatFile))
+ if sterr == nil && st.Mode().IsRegular() {
+ return errFileNotFound
+ }
+ }
+ }
+
+ prefix := opts.FilterPrefix
+ var scanDir func(path string) error
+
+ scanDir = func(current string) error {
+ if contextCanceled(ctx) {
+ return ctx.Err()
+ }
+ if opts.Limit > 0 && objsReturned >= opts.Limit {
+ return nil
+ }
+
+ // Skip forward, if requested...
+ sb := bytebufferpool.Get()
+ defer func() {
+ sb.Reset()
+ bytebufferpool.Put(sb)
+ }()
+
+ forward := ""
+ if len(opts.ForwardTo) > 0 && strings.HasPrefix(opts.ForwardTo, current) {
+ forward = strings.TrimPrefix(opts.ForwardTo, current)
+ // Trim further directories and trailing slash.
+ if idx := strings.IndexByte(forward, '/'); idx > 0 {
+ forward = forward[:idx]
+ }
+ }
+
+ if s.walkMu != nil {
+ s.walkMu.Lock()
+ }
+ entries, err := s.ListDir(ctx, "", opts.Bucket, current, -1)
+ if s.walkMu != nil {
+ s.walkMu.Unlock()
+ }
+ if err != nil {
+ // Folder could have gone away in-between
+ if err != errVolumeNotFound && err != errFileNotFound {
+ internalLogOnceIf(ctx, err, "metacache-walk-scan-dir")
+ }
+ if opts.ReportNotFound && err == errFileNotFound && current == opts.BaseDir {
+ err = errFileNotFound
+ } else {
+ err = nil
+ }
+ diskHealthCheckOK(ctx, err)
+ return err
+ }
+ diskHealthCheckOK(ctx, err)
+ if len(entries) == 0 {
+ return nil
+ }
+ dirObjects := make(map[string]struct{})
+
+ // Avoid a bunch of cleanup when joining.
+ current = strings.Trim(current, SlashSeparator)
+ for i, entry := range entries {
+ if contextCanceled(ctx) {
+ return ctx.Err()
+ }
+ if opts.Limit > 0 && objsReturned >= opts.Limit {
+ return nil
+ }
+ if len(prefix) > 0 && !strings.HasPrefix(entry, prefix) {
+ // Do not retain the file, since it doesn't
+ // match the prefix.
+ entries[i] = ""
+ continue
+ }
+ if len(forward) > 0 && entry < forward {
+ // Do not retain the file, since its
+ // lexially smaller than 'forward'
+ entries[i] = ""
+ continue
+ }
+ if hasSuffixByte(entry, SlashSeparatorChar) {
+ if strings.HasSuffix(entry, globalDirSuffixWithSlash) {
+ // Add without extension so it is sorted correctly.
+ entry = strings.TrimSuffix(entry, globalDirSuffixWithSlash) + slashSeparator
+ dirObjects[entry] = struct{}{}
+ entries[i] = entry
+ continue
+ }
+ // Trim slash, since we don't know if this is folder or object.
+ entries[i] = entries[i][:len(entry)-1]
+ continue
+ }
+ // Do not retain the file.
+ entries[i] = ""
+
+ if contextCanceled(ctx) {
+ return ctx.Err()
+ }
+ // If root was an object return it as such.
+ if HasSuffix(entry, xlStorageFormatFile) {
+ var meta metaCacheEntry
+ if s.walkReadMu != nil {
+ s.walkReadMu.Lock()
+ }
+ meta.metadata, err = s.readMetadata(ctx, pathJoinBuf(sb, volumeDir, current, entry))
+ if s.walkReadMu != nil {
+ s.walkReadMu.Unlock()
+ }
+ diskHealthCheckOK(ctx, err)
+ if err != nil {
+ // It is totally possible that xl.meta was overwritten
+ // while being concurrently listed at the same time in
+ // such scenarios the 'xl.meta' might get truncated
+ if !IsErrIgnored(err, io.EOF, io.ErrUnexpectedEOF) {
+ internalLogOnceIf(ctx, err, "metacache-walk-read-metadata")
+ }
+ continue
+ }
+ meta.name = strings.TrimSuffix(entry, xlStorageFormatFile)
+ meta.name = strings.TrimSuffix(meta.name, SlashSeparator)
+ meta.name = pathJoinBuf(sb, current, meta.name)
+ meta.name = decodeDirObject(meta.name)
+
+ return send(meta)
+ }
+ // Check legacy.
+ if HasSuffix(entry, xlStorageFormatFileV1) && legacy {
+ var meta metaCacheEntry
+ meta.metadata, err = xioutil.ReadFile(pathJoinBuf(sb, volumeDir, current, entry))
+ diskHealthCheckOK(ctx, err)
+ if err != nil {
+ if !IsErrIgnored(err, io.EOF, io.ErrUnexpectedEOF) {
+ internalLogIf(ctx, err)
+ }
+ continue
+ }
+ meta.name = strings.TrimSuffix(entry, xlStorageFormatFileV1)
+ meta.name = strings.TrimSuffix(meta.name, SlashSeparator)
+ meta.name = pathJoinBuf(sb, current, meta.name)
+
+ return send(meta)
+ }
+ // Skip all other files.
+ }
+
+ // Process in sort order.
+ sort.Strings(entries)
+ dirStack := make([]string, 0, 5)
+ prefix = "" // Remove prefix after first level as we have already filtered the list.
+ if len(forward) > 0 {
+ // Conservative forwarding. Entries may be either objects or prefixes.
+ for i, entry := range entries {
+ if entry >= forward || strings.HasPrefix(forward, entry) {
+ entries = entries[i:]
+ break
+ }
+ }
+ }
+
+ for _, entry := range entries {
+ if contextCanceled(ctx) {
+ return ctx.Err()
+ }
+ if opts.Limit > 0 && objsReturned >= opts.Limit {
+ return nil
+ }
+ if entry == "" {
+ continue
+ }
+ meta := metaCacheEntry{name: pathJoinBuf(sb, current, entry)}
+
+ // If directory entry on stack before this, pop it now.
+ for len(dirStack) > 0 && dirStack[len(dirStack)-1] < meta.name {
+ pop := dirStack[len(dirStack)-1]
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case out <- metaCacheEntry{name: pop}:
+ }
+ if opts.Recursive {
+ // Scan folder we found. Should be in correct sort order where we are.
+ err := scanDir(pop)
+ if err != nil {
+ if errors.Is(err, context.Canceled) {
+ return err
+ }
+ internalLogIf(ctx, err)
+ }
+ }
+ dirStack = dirStack[:len(dirStack)-1]
+ }
+
+ // All objects will be returned as directories, there has been no object check yet.
+ // Check it by attempting to read metadata.
+ _, isDirObj := dirObjects[entry]
+ if isDirObj {
+ meta.name = meta.name[:len(meta.name)-1] + globalDirSuffixWithSlash
+ }
+
+ if s.walkReadMu != nil {
+ s.walkReadMu.Lock()
+ }
+ meta.metadata, err = s.readMetadata(ctx, pathJoinBuf(sb, volumeDir, meta.name, xlStorageFormatFile))
+ if s.walkReadMu != nil {
+ s.walkReadMu.Unlock()
+ }
+ diskHealthCheckOK(ctx, err)
+ switch {
+ case err == nil:
+ // It was an object
+ if isDirObj {
+ meta.name = strings.TrimSuffix(meta.name, globalDirSuffixWithSlash) + slashSeparator
+ }
+ if err := send(meta); err != nil {
+ return err
+ }
+ case osIsNotExist(err), isSysErrIsDir(err):
+ if legacy {
+ meta.metadata, err = xioutil.ReadFile(pathJoinBuf(sb, volumeDir, meta.name, xlStorageFormatFileV1))
+ diskHealthCheckOK(ctx, err)
+ if err == nil {
+ // It was an object
+ if err := send(meta); err != nil {
+ return err
+ }
+ continue
+ }
+ }
+
+ // NOT an object, append to stack (with slash)
+ // If dirObject, but no metadata (which is unexpected) we skip it.
+ if !isDirObj {
+ if !isDirEmpty(pathJoinBuf(sb, volumeDir, meta.name), legacyFS) {
+ dirStack = append(dirStack, meta.name+slashSeparator)
+ }
+ }
+ case isSysErrNotDir(err):
+ // skip
+ }
+ }
+
+ // If directory entry left on stack, pop it now.
+ for len(dirStack) > 0 {
+ if opts.Limit > 0 && objsReturned >= opts.Limit {
+ return nil
+ }
+ if contextCanceled(ctx) {
+ return ctx.Err()
+ }
+ pop := dirStack[len(dirStack)-1]
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case out <- metaCacheEntry{name: pop}:
+ }
+ if opts.Recursive {
+ // Scan folder we found. Should be in correct sort order where we are.
+ internalLogIf(ctx, scanDir(pop))
+ }
+ dirStack = dirStack[:len(dirStack)-1]
+ }
+ return nil
+ }
+
+ // Stream output.
+ return scanDir(opts.BaseDir)
+}
+
+func (p *xlStorageDiskIDCheck) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writer) (err error) {
+ if err := p.checkID(opts.DiskID); err != nil {
+ return err
+ }
+ ctx, done, err := p.TrackDiskHealth(ctx, storageMetricWalkDir, opts.Bucket, opts.BaseDir)
+ if err != nil {
+ return err
+ }
+ defer done(0, &err)
+
+ return p.storage.WalkDir(ctx, opts, wr)
+}
+
+// WalkDir will traverse a directory and return all entries found.
+// On success a meta cache stream will be returned, that should be closed when done.
+func (client *storageRESTClient) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writer) error {
+ // Ensure remote has the same disk ID.
+ opts.DiskID = *client.diskID.Load()
+ b, err := opts.MarshalMsg(grid.GetByteBuffer()[:0])
+ if err != nil {
+ return toStorageErr(err)
+ }
+
+ st, err := client.gridConn.NewStream(ctx, grid.HandlerWalkDir, b)
+ if err != nil {
+ return toStorageErr(err)
+ }
+ return toStorageErr(st.Results(func(in []byte) error {
+ _, err := wr.Write(in)
+ return err
+ }))
+}
+
+// WalkDirHandler - remote caller to list files and folders in a requested directory path.
+func (s *storageRESTServer) WalkDirHandler(ctx context.Context, payload []byte, _ <-chan []byte, out chan<- []byte) (gerr *grid.RemoteErr) {
+ var opts WalkDirOptions
+ _, err := opts.UnmarshalMsg(payload)
+ if err != nil {
+ return grid.NewRemoteErr(err)
+ }
+
+ if !s.checkID(opts.DiskID) {
+ return grid.NewRemoteErr(errDiskNotFound)
+ }
+
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+ return grid.NewRemoteErr(s.getStorage().WalkDir(ctx, opts, grid.WriterToChannel(ctx, out)))
+}
diff --git a/cmd/metacache-walk_gen.go b/cmd/metacache-walk_gen.go
new file mode 100644
index 00000000..6f0624f9
--- /dev/null
+++ b/cmd/metacache-walk_gen.go
@@ -0,0 +1,285 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "github.com/tinylib/msgp/msgp"
+)
+
+// DecodeMsg implements msgp.Decodable
+func (z *WalkDirOptions) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Bucket":
+ z.Bucket, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "BaseDir":
+ z.BaseDir, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "BaseDir")
+ return
+ }
+ case "Recursive":
+ z.Recursive, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "Recursive")
+ return
+ }
+ case "ReportNotFound":
+ z.ReportNotFound, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "ReportNotFound")
+ return
+ }
+ case "FilterPrefix":
+ z.FilterPrefix, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "FilterPrefix")
+ return
+ }
+ case "ForwardTo":
+ z.ForwardTo, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "ForwardTo")
+ return
+ }
+ case "Limit":
+ z.Limit, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "Limit")
+ return
+ }
+ case "DiskID":
+ z.DiskID, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "DiskID")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *WalkDirOptions) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 8
+ // write "Bucket"
+ err = en.Append(0x88, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Bucket)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ // write "BaseDir"
+ err = en.Append(0xa7, 0x42, 0x61, 0x73, 0x65, 0x44, 0x69, 0x72)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.BaseDir)
+ if err != nil {
+ err = msgp.WrapError(err, "BaseDir")
+ return
+ }
+ // write "Recursive"
+ err = en.Append(0xa9, 0x52, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.Recursive)
+ if err != nil {
+ err = msgp.WrapError(err, "Recursive")
+ return
+ }
+ // write "ReportNotFound"
+ err = en.Append(0xae, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x4e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.ReportNotFound)
+ if err != nil {
+ err = msgp.WrapError(err, "ReportNotFound")
+ return
+ }
+ // write "FilterPrefix"
+ err = en.Append(0xac, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.FilterPrefix)
+ if err != nil {
+ err = msgp.WrapError(err, "FilterPrefix")
+ return
+ }
+ // write "ForwardTo"
+ err = en.Append(0xa9, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x54, 0x6f)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.ForwardTo)
+ if err != nil {
+ err = msgp.WrapError(err, "ForwardTo")
+ return
+ }
+ // write "Limit"
+ err = en.Append(0xa5, 0x4c, 0x69, 0x6d, 0x69, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.Limit)
+ if err != nil {
+ err = msgp.WrapError(err, "Limit")
+ return
+ }
+ // write "DiskID"
+ err = en.Append(0xa6, 0x44, 0x69, 0x73, 0x6b, 0x49, 0x44)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.DiskID)
+ if err != nil {
+ err = msgp.WrapError(err, "DiskID")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *WalkDirOptions) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 8
+ // string "Bucket"
+ o = append(o, 0x88, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ o = msgp.AppendString(o, z.Bucket)
+ // string "BaseDir"
+ o = append(o, 0xa7, 0x42, 0x61, 0x73, 0x65, 0x44, 0x69, 0x72)
+ o = msgp.AppendString(o, z.BaseDir)
+ // string "Recursive"
+ o = append(o, 0xa9, 0x52, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65)
+ o = msgp.AppendBool(o, z.Recursive)
+ // string "ReportNotFound"
+ o = append(o, 0xae, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x4e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64)
+ o = msgp.AppendBool(o, z.ReportNotFound)
+ // string "FilterPrefix"
+ o = append(o, 0xac, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78)
+ o = msgp.AppendString(o, z.FilterPrefix)
+ // string "ForwardTo"
+ o = append(o, 0xa9, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x54, 0x6f)
+ o = msgp.AppendString(o, z.ForwardTo)
+ // string "Limit"
+ o = append(o, 0xa5, 0x4c, 0x69, 0x6d, 0x69, 0x74)
+ o = msgp.AppendInt(o, z.Limit)
+ // string "DiskID"
+ o = append(o, 0xa6, 0x44, 0x69, 0x73, 0x6b, 0x49, 0x44)
+ o = msgp.AppendString(o, z.DiskID)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *WalkDirOptions) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Bucket":
+ z.Bucket, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "BaseDir":
+ z.BaseDir, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "BaseDir")
+ return
+ }
+ case "Recursive":
+ z.Recursive, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Recursive")
+ return
+ }
+ case "ReportNotFound":
+ z.ReportNotFound, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReportNotFound")
+ return
+ }
+ case "FilterPrefix":
+ z.FilterPrefix, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "FilterPrefix")
+ return
+ }
+ case "ForwardTo":
+ z.ForwardTo, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ForwardTo")
+ return
+ }
+ case "Limit":
+ z.Limit, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Limit")
+ return
+ }
+ case "DiskID":
+ z.DiskID, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "DiskID")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *WalkDirOptions) Msgsize() (s int) {
+ s = 1 + 7 + msgp.StringPrefixSize + len(z.Bucket) + 8 + msgp.StringPrefixSize + len(z.BaseDir) + 10 + msgp.BoolSize + 15 + msgp.BoolSize + 13 + msgp.StringPrefixSize + len(z.FilterPrefix) + 10 + msgp.StringPrefixSize + len(z.ForwardTo) + 6 + msgp.IntSize + 7 + msgp.StringPrefixSize + len(z.DiskID)
+ return
+}
diff --git a/cmd/metacache-walk_gen_test.go b/cmd/metacache-walk_gen_test.go
new file mode 100644
index 00000000..16edd846
--- /dev/null
+++ b/cmd/metacache-walk_gen_test.go
@@ -0,0 +1,123 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+func TestMarshalUnmarshalWalkDirOptions(t *testing.T) {
+ v := WalkDirOptions{}
+ 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 BenchmarkMarshalMsgWalkDirOptions(b *testing.B) {
+ v := WalkDirOptions{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgWalkDirOptions(b *testing.B) {
+ v := WalkDirOptions{}
+ 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 BenchmarkUnmarshalWalkDirOptions(b *testing.B) {
+ v := WalkDirOptions{}
+ 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 TestEncodeDecodeWalkDirOptions(t *testing.T) {
+ v := WalkDirOptions{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodeWalkDirOptions Msgsize() is inaccurate")
+ }
+
+ vn := WalkDirOptions{}
+ 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 BenchmarkEncodeWalkDirOptions(b *testing.B) {
+ v := WalkDirOptions{}
+ 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 BenchmarkDecodeWalkDirOptions(b *testing.B) {
+ v := WalkDirOptions{}
+ 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)
+ }
+ }
+}
diff --git a/cmd/metacache.go b/cmd/metacache.go
new file mode 100644
index 00000000..7f35d391
--- /dev/null
+++ b/cmd/metacache.go
@@ -0,0 +1,200 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "path"
+ "strings"
+ "time"
+
+ "github.com/minio/pkg/v3/console"
+)
+
+type scanStatus uint8
+
+const (
+ scanStateNone scanStatus = iota
+ scanStateStarted
+ scanStateSuccess
+ scanStateError
+
+ // Time in which the initiator of a scan must have reported back.
+ metacacheMaxRunningAge = time.Minute
+
+ // Max time between client calls before dropping an async cache listing.
+ metacacheMaxClientWait = 3 * time.Minute
+
+ // metacacheBlockSize is the number of file/directory entries to have in each block.
+ metacacheBlockSize = 5000
+
+ // metacacheSharePrefix controls whether prefixes on dirty paths are always shared.
+ // This will make `test/a` and `test/b` share listings if they are concurrent.
+ // Enabling this will make cache sharing more likely and cause less IO,
+ // but may cause additional latency to some calls.
+ metacacheSharePrefix = false
+)
+
+//go:generate msgp -file $GOFILE -unexported
+
+// metacache contains a tracked cache entry.
+type metacache struct {
+ // do not re-arrange the struct this struct has been ordered to use less
+ // space - if you do so please run https://github.com/orijtech/structslop
+ // and verify if your changes are optimal.
+ ended time.Time `msg:"end"`
+ started time.Time `msg:"st"`
+ lastHandout time.Time `msg:"lh"`
+ lastUpdate time.Time `msg:"u"`
+ bucket string `msg:"b"`
+ filter string `msg:"flt"`
+ id string `msg:"id"`
+ error string `msg:"err"`
+ root string `msg:"root"`
+ fileNotFound bool `msg:"fnf"`
+ status scanStatus `msg:"stat"`
+ recursive bool `msg:"rec"`
+ dataVersion uint8 `msg:"v"`
+}
+
+func (m *metacache) finished() bool {
+ return !m.ended.IsZero()
+}
+
+// worthKeeping indicates if the cache by itself is worth keeping.
+func (m *metacache) worthKeeping() bool {
+ if m == nil {
+ return false
+ }
+ cache := m
+ switch {
+ case !cache.finished() && time.Since(cache.lastUpdate) > metacacheMaxRunningAge:
+ // Not finished and update for metacacheMaxRunningAge, discard it.
+ return false
+ case cache.finished() && time.Since(cache.lastHandout) > 5*metacacheMaxClientWait:
+ // Keep for 15 minutes after we last saw the client.
+ // Since the cache is finished keeping it a bit longer doesn't hurt us.
+ return false
+ case cache.status == scanStateError || cache.status == scanStateNone:
+ // Remove failed listings after 5 minutes.
+ return time.Since(cache.lastUpdate) > 5*time.Minute
+ }
+ return true
+}
+
+// keepAlive will continuously update lastHandout until ctx is canceled.
+func (m metacache) keepAlive(ctx context.Context, rpc *peerRESTClient) {
+ // we intentionally operate on a copy of m, so we can update without locks.
+ t := time.NewTicker(metacacheMaxClientWait / 10)
+ defer t.Stop()
+ for {
+ select {
+ case <-ctx.Done():
+ // Request is done, stop updating.
+ return
+ case <-t.C:
+ m.lastHandout = time.Now()
+
+ if m2, err := rpc.UpdateMetacacheListing(ctx, m); err == nil {
+ if m2.status != scanStateStarted {
+ if serverDebugLog {
+ console.Debugln("returning", m.id, "due to scan state", m2.status, time.Now().Format(time.RFC3339))
+ }
+ return
+ }
+ m = m2
+ if serverDebugLog {
+ console.Debugln("refreshed", m.id, time.Now().Format(time.RFC3339))
+ }
+ } else if serverDebugLog {
+ console.Debugln("error refreshing", m.id, time.Now().Format(time.RFC3339))
+ }
+ }
+ }
+}
+
+// baseDirFromPrefix will return the base directory given an object path.
+// For example an object with name prefix/folder/object.ext will return `prefix/folder/`.
+func baseDirFromPrefix(prefix string) string {
+ b := path.Dir(prefix)
+ if b == "." || b == "./" || b == "/" {
+ b = ""
+ }
+ if !strings.Contains(prefix, slashSeparator) {
+ b = ""
+ }
+ if len(b) > 0 && !strings.HasSuffix(b, slashSeparator) {
+ b += slashSeparator
+ }
+ return b
+}
+
+// update cache with new status.
+// The updates are conditional so multiple callers can update with different states.
+func (m *metacache) update(update metacache) {
+ now := UTCNow()
+ m.lastUpdate = now
+
+ if update.lastHandout.After(m.lastHandout) {
+ m.lastHandout = update.lastUpdate
+ if m.lastHandout.After(now) {
+ m.lastHandout = now
+ }
+ }
+ if m.status == scanStateStarted && update.status == scanStateSuccess {
+ m.ended = now
+ }
+
+ if m.status == scanStateStarted && update.status != scanStateStarted {
+ m.status = update.status
+ }
+
+ if m.status == scanStateStarted && time.Since(m.lastHandout) > metacacheMaxClientWait {
+ // Drop if client hasn't been seen for 3 minutes.
+ m.status = scanStateError
+ m.error = "client not seen"
+ }
+
+ if m.error == "" && update.error != "" {
+ m.error = update.error
+ m.status = scanStateError
+ m.ended = now
+ }
+ m.fileNotFound = m.fileNotFound || update.fileNotFound
+}
+
+// delete all cache data on disks.
+func (m *metacache) delete(ctx context.Context) {
+ if m.bucket == "" || m.id == "" {
+ bugLogIf(ctx, fmt.Errorf("metacache.delete: bucket (%s) or id (%s) empty", m.bucket, m.id))
+ return
+ }
+ objAPI := newObjectLayerFn()
+ if objAPI == nil {
+ internalLogIf(ctx, errors.New("metacache.delete: no object layer"))
+ return
+ }
+ ez, ok := objAPI.(deleteAllStorager)
+ if !ok {
+ bugLogIf(ctx, errors.New("metacache.delete: expected objAPI to be 'deleteAllStorager'"))
+ return
+ }
+ ez.deleteAll(ctx, minioMetaBucket, metacachePrefixForID(m.bucket, m.id))
+}
diff --git a/cmd/metacache_gen.go b/cmd/metacache_gen.go
new file mode 100644
index 00000000..9ca1bca8
--- /dev/null
+++ b/cmd/metacache_gen.go
@@ -0,0 +1,470 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "github.com/tinylib/msgp/msgp"
+)
+
+// DecodeMsg implements msgp.Decodable
+func (z *metacache) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "end":
+ z.ended, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "ended")
+ return
+ }
+ case "st":
+ z.started, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "started")
+ return
+ }
+ case "lh":
+ z.lastHandout, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "lastHandout")
+ return
+ }
+ case "u":
+ z.lastUpdate, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "lastUpdate")
+ return
+ }
+ case "b":
+ z.bucket, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "bucket")
+ return
+ }
+ case "flt":
+ z.filter, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "filter")
+ return
+ }
+ case "id":
+ z.id, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "id")
+ return
+ }
+ case "err":
+ z.error, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "error")
+ return
+ }
+ case "root":
+ z.root, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "root")
+ return
+ }
+ case "fnf":
+ z.fileNotFound, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "fileNotFound")
+ return
+ }
+ case "stat":
+ {
+ var zb0002 uint8
+ zb0002, err = dc.ReadUint8()
+ if err != nil {
+ err = msgp.WrapError(err, "status")
+ return
+ }
+ z.status = scanStatus(zb0002)
+ }
+ case "rec":
+ z.recursive, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "recursive")
+ return
+ }
+ case "v":
+ z.dataVersion, err = dc.ReadUint8()
+ if err != nil {
+ err = msgp.WrapError(err, "dataVersion")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *metacache) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 13
+ // write "end"
+ err = en.Append(0x8d, 0xa3, 0x65, 0x6e, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.ended)
+ if err != nil {
+ err = msgp.WrapError(err, "ended")
+ return
+ }
+ // write "st"
+ err = en.Append(0xa2, 0x73, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.started)
+ if err != nil {
+ err = msgp.WrapError(err, "started")
+ return
+ }
+ // write "lh"
+ err = en.Append(0xa2, 0x6c, 0x68)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.lastHandout)
+ if err != nil {
+ err = msgp.WrapError(err, "lastHandout")
+ return
+ }
+ // write "u"
+ err = en.Append(0xa1, 0x75)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.lastUpdate)
+ if err != nil {
+ err = msgp.WrapError(err, "lastUpdate")
+ return
+ }
+ // write "b"
+ err = en.Append(0xa1, 0x62)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.bucket)
+ if err != nil {
+ err = msgp.WrapError(err, "bucket")
+ return
+ }
+ // write "flt"
+ err = en.Append(0xa3, 0x66, 0x6c, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.filter)
+ if err != nil {
+ err = msgp.WrapError(err, "filter")
+ return
+ }
+ // write "id"
+ err = en.Append(0xa2, 0x69, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.id)
+ if err != nil {
+ err = msgp.WrapError(err, "id")
+ return
+ }
+ // write "err"
+ err = en.Append(0xa3, 0x65, 0x72, 0x72)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.error)
+ if err != nil {
+ err = msgp.WrapError(err, "error")
+ return
+ }
+ // write "root"
+ err = en.Append(0xa4, 0x72, 0x6f, 0x6f, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.root)
+ if err != nil {
+ err = msgp.WrapError(err, "root")
+ return
+ }
+ // write "fnf"
+ err = en.Append(0xa3, 0x66, 0x6e, 0x66)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.fileNotFound)
+ if err != nil {
+ err = msgp.WrapError(err, "fileNotFound")
+ return
+ }
+ // write "stat"
+ err = en.Append(0xa4, 0x73, 0x74, 0x61, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint8(uint8(z.status))
+ if err != nil {
+ err = msgp.WrapError(err, "status")
+ return
+ }
+ // write "rec"
+ err = en.Append(0xa3, 0x72, 0x65, 0x63)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.recursive)
+ if err != nil {
+ err = msgp.WrapError(err, "recursive")
+ return
+ }
+ // write "v"
+ err = en.Append(0xa1, 0x76)
+ if err != nil {
+ return
+ }
+ err = en.WriteUint8(z.dataVersion)
+ if err != nil {
+ err = msgp.WrapError(err, "dataVersion")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *metacache) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 13
+ // string "end"
+ o = append(o, 0x8d, 0xa3, 0x65, 0x6e, 0x64)
+ o = msgp.AppendTime(o, z.ended)
+ // string "st"
+ o = append(o, 0xa2, 0x73, 0x74)
+ o = msgp.AppendTime(o, z.started)
+ // string "lh"
+ o = append(o, 0xa2, 0x6c, 0x68)
+ o = msgp.AppendTime(o, z.lastHandout)
+ // string "u"
+ o = append(o, 0xa1, 0x75)
+ o = msgp.AppendTime(o, z.lastUpdate)
+ // string "b"
+ o = append(o, 0xa1, 0x62)
+ o = msgp.AppendString(o, z.bucket)
+ // string "flt"
+ o = append(o, 0xa3, 0x66, 0x6c, 0x74)
+ o = msgp.AppendString(o, z.filter)
+ // string "id"
+ o = append(o, 0xa2, 0x69, 0x64)
+ o = msgp.AppendString(o, z.id)
+ // string "err"
+ o = append(o, 0xa3, 0x65, 0x72, 0x72)
+ o = msgp.AppendString(o, z.error)
+ // string "root"
+ o = append(o, 0xa4, 0x72, 0x6f, 0x6f, 0x74)
+ o = msgp.AppendString(o, z.root)
+ // string "fnf"
+ o = append(o, 0xa3, 0x66, 0x6e, 0x66)
+ o = msgp.AppendBool(o, z.fileNotFound)
+ // string "stat"
+ o = append(o, 0xa4, 0x73, 0x74, 0x61, 0x74)
+ o = msgp.AppendUint8(o, uint8(z.status))
+ // string "rec"
+ o = append(o, 0xa3, 0x72, 0x65, 0x63)
+ o = msgp.AppendBool(o, z.recursive)
+ // string "v"
+ o = append(o, 0xa1, 0x76)
+ o = msgp.AppendUint8(o, z.dataVersion)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *metacache) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "end":
+ z.ended, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ended")
+ return
+ }
+ case "st":
+ z.started, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "started")
+ return
+ }
+ case "lh":
+ z.lastHandout, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "lastHandout")
+ return
+ }
+ case "u":
+ z.lastUpdate, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "lastUpdate")
+ return
+ }
+ case "b":
+ z.bucket, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "bucket")
+ return
+ }
+ case "flt":
+ z.filter, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "filter")
+ return
+ }
+ case "id":
+ z.id, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "id")
+ return
+ }
+ case "err":
+ z.error, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "error")
+ return
+ }
+ case "root":
+ z.root, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "root")
+ return
+ }
+ case "fnf":
+ z.fileNotFound, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "fileNotFound")
+ return
+ }
+ case "stat":
+ {
+ var zb0002 uint8
+ zb0002, bts, err = msgp.ReadUint8Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "status")
+ return
+ }
+ z.status = scanStatus(zb0002)
+ }
+ case "rec":
+ z.recursive, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "recursive")
+ return
+ }
+ case "v":
+ z.dataVersion, bts, err = msgp.ReadUint8Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "dataVersion")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *metacache) Msgsize() (s int) {
+ s = 1 + 4 + msgp.TimeSize + 3 + msgp.TimeSize + 3 + msgp.TimeSize + 2 + msgp.TimeSize + 2 + msgp.StringPrefixSize + len(z.bucket) + 4 + msgp.StringPrefixSize + len(z.filter) + 3 + msgp.StringPrefixSize + len(z.id) + 4 + msgp.StringPrefixSize + len(z.error) + 5 + msgp.StringPrefixSize + len(z.root) + 4 + msgp.BoolSize + 5 + msgp.Uint8Size + 4 + msgp.BoolSize + 2 + msgp.Uint8Size
+ return
+}
+
+// DecodeMsg implements msgp.Decodable
+func (z *scanStatus) DecodeMsg(dc *msgp.Reader) (err error) {
+ {
+ var zb0001 uint8
+ zb0001, err = dc.ReadUint8()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ (*z) = scanStatus(zb0001)
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z scanStatus) EncodeMsg(en *msgp.Writer) (err error) {
+ err = en.WriteUint8(uint8(z))
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z scanStatus) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ o = msgp.AppendUint8(o, uint8(z))
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *scanStatus) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ {
+ var zb0001 uint8
+ zb0001, bts, err = msgp.ReadUint8Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ (*z) = scanStatus(zb0001)
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z scanStatus) Msgsize() (s int) {
+ s = msgp.Uint8Size
+ return
+}
diff --git a/cmd/metacache_gen_test.go b/cmd/metacache_gen_test.go
new file mode 100644
index 00000000..017d4cc3
--- /dev/null
+++ b/cmd/metacache_gen_test.go
@@ -0,0 +1,123 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+func TestMarshalUnmarshalmetacache(t *testing.T) {
+ v := metacache{}
+ 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 BenchmarkMarshalMsgmetacache(b *testing.B) {
+ v := metacache{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgmetacache(b *testing.B) {
+ v := metacache{}
+ 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 BenchmarkUnmarshalmetacache(b *testing.B) {
+ v := metacache{}
+ 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 TestEncodeDecodemetacache(t *testing.T) {
+ v := metacache{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodemetacache Msgsize() is inaccurate")
+ }
+
+ vn := metacache{}
+ 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 BenchmarkEncodemetacache(b *testing.B) {
+ v := metacache{}
+ 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 BenchmarkDecodemetacache(b *testing.B) {
+ v := metacache{}
+ 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)
+ }
+ }
+}
diff --git a/cmd/metacache_test.go b/cmd/metacache_test.go
new file mode 100644
index 00000000..84860d85
--- /dev/null
+++ b/cmd/metacache_test.go
@@ -0,0 +1,246 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "testing"
+ "time"
+)
+
+var metaCacheTestsetTimestamp = time.Now()
+
+var metaCacheTestset = []metacache{
+ 0: {
+ id: "case-1-normal",
+ bucket: "bucket",
+ root: "folder/prefix",
+ recursive: false,
+ status: scanStateSuccess,
+ fileNotFound: false,
+ error: "",
+ started: metaCacheTestsetTimestamp,
+ ended: metaCacheTestsetTimestamp.Add(time.Minute),
+ lastUpdate: metaCacheTestsetTimestamp.Add(time.Minute),
+ lastHandout: metaCacheTestsetTimestamp,
+ dataVersion: metacacheStreamVersion,
+ },
+ 1: {
+ id: "case-2-recursive",
+ bucket: "bucket",
+ root: "folder/prefix",
+ recursive: true,
+ status: scanStateSuccess,
+ fileNotFound: false,
+ error: "",
+ started: metaCacheTestsetTimestamp,
+ ended: metaCacheTestsetTimestamp.Add(time.Minute),
+ lastUpdate: metaCacheTestsetTimestamp.Add(time.Minute),
+ lastHandout: metaCacheTestsetTimestamp,
+ dataVersion: metacacheStreamVersion,
+ },
+ 2: {
+ id: "case-3-older",
+ bucket: "bucket",
+ root: "folder/prefix",
+ recursive: false,
+ status: scanStateSuccess,
+ fileNotFound: true,
+ error: "",
+ started: metaCacheTestsetTimestamp.Add(-time.Minute),
+ ended: metaCacheTestsetTimestamp,
+ lastUpdate: metaCacheTestsetTimestamp,
+ lastHandout: metaCacheTestsetTimestamp,
+ dataVersion: metacacheStreamVersion,
+ },
+ 3: {
+ id: "case-4-error",
+ bucket: "bucket",
+ root: "folder/prefix",
+ recursive: false,
+ status: scanStateError,
+ fileNotFound: false,
+ error: "an error lol",
+ started: metaCacheTestsetTimestamp.Add(-20 * time.Minute),
+ ended: metaCacheTestsetTimestamp.Add(-20 * time.Minute),
+ lastUpdate: metaCacheTestsetTimestamp.Add(-20 * time.Minute),
+ lastHandout: metaCacheTestsetTimestamp.Add(-20 * time.Minute),
+ dataVersion: metacacheStreamVersion,
+ },
+ 4: {
+ id: "case-5-noupdate",
+ bucket: "bucket",
+ root: "folder/prefix",
+ recursive: false,
+ status: scanStateStarted,
+ fileNotFound: false,
+ error: "",
+ started: metaCacheTestsetTimestamp.Add(-time.Minute),
+ ended: time.Time{},
+ lastUpdate: metaCacheTestsetTimestamp.Add(-time.Minute),
+ lastHandout: metaCacheTestsetTimestamp,
+ dataVersion: metacacheStreamVersion,
+ },
+ 5: {
+ id: "case-6-404notfound",
+ bucket: "bucket",
+ root: "folder/notfound",
+ recursive: true,
+ status: scanStateSuccess,
+ fileNotFound: true,
+ error: "",
+ started: metaCacheTestsetTimestamp,
+ ended: metaCacheTestsetTimestamp.Add(time.Minute),
+ lastUpdate: metaCacheTestsetTimestamp.Add(time.Minute),
+ lastHandout: metaCacheTestsetTimestamp,
+ dataVersion: metacacheStreamVersion,
+ },
+ 6: {
+ id: "case-7-oldcycle",
+ bucket: "bucket",
+ root: "folder/prefix",
+ recursive: true,
+ status: scanStateSuccess,
+ fileNotFound: false,
+ error: "",
+ started: metaCacheTestsetTimestamp.Add(-10 * time.Minute),
+ ended: metaCacheTestsetTimestamp.Add(-8 * time.Minute),
+ lastUpdate: metaCacheTestsetTimestamp.Add(-8 * time.Minute),
+ lastHandout: metaCacheTestsetTimestamp,
+ dataVersion: metacacheStreamVersion,
+ },
+ 7: {
+ id: "case-8-running",
+ bucket: "bucket",
+ root: "folder/running",
+ recursive: false,
+ status: scanStateStarted,
+ fileNotFound: false,
+ error: "",
+ started: metaCacheTestsetTimestamp.Add(-1 * time.Minute),
+ ended: time.Time{},
+ lastUpdate: metaCacheTestsetTimestamp.Add(-1 * time.Minute),
+ lastHandout: metaCacheTestsetTimestamp,
+ dataVersion: metacacheStreamVersion,
+ },
+ 8: {
+ id: "case-8-finished-a-week-ago",
+ bucket: "bucket",
+ root: "folder/finished",
+ recursive: false,
+ status: scanStateSuccess,
+ fileNotFound: false,
+ error: "",
+ started: metaCacheTestsetTimestamp.Add(-7 * 24 * time.Hour),
+ ended: metaCacheTestsetTimestamp.Add(-7 * 24 * time.Hour),
+ lastUpdate: metaCacheTestsetTimestamp.Add(-7 * 24 * time.Hour),
+ lastHandout: metaCacheTestsetTimestamp.Add(-7 * 24 * time.Hour),
+ dataVersion: metacacheStreamVersion,
+ },
+}
+
+func Test_baseDirFromPrefix(t *testing.T) {
+ tests := []struct {
+ name string
+ prefix string
+ want string
+ }{
+ {
+ name: "root",
+ prefix: "object.ext",
+ want: "",
+ },
+ {
+ name: "rootdotslash",
+ prefix: "./object.ext",
+ want: "",
+ },
+ {
+ name: "rootslash",
+ prefix: "/",
+ want: "",
+ },
+ {
+ name: "folder",
+ prefix: "prefix/",
+ want: "prefix/",
+ },
+ {
+ name: "folderobj",
+ prefix: "prefix/obj.ext",
+ want: "prefix/",
+ },
+ {
+ name: "folderfolderobj",
+ prefix: "prefix/prefix2/obj.ext",
+ want: "prefix/prefix2/",
+ },
+ {
+ name: "folderfolder",
+ prefix: "prefix/prefix2/",
+ want: "prefix/prefix2/",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := baseDirFromPrefix(tt.prefix); got != tt.want {
+ t.Errorf("baseDirFromPrefix() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func Test_metacache_finished(t *testing.T) {
+ wantResults := []bool{0: true, 1: true, 2: true, 3: true, 4: false, 5: true, 6: true, 7: false, 8: true}
+
+ for i, tt := range metaCacheTestset {
+ t.Run(tt.id, func(t *testing.T) {
+ var want bool
+ if i >= len(wantResults) {
+ t.Logf("no expected result for test #%d", i)
+ } else {
+ want = wantResults[i]
+ }
+
+ got := tt.finished()
+ if got != want {
+ t.Errorf("#%d: want %v, got %v", i, want, got)
+ }
+ })
+ }
+}
+
+func Test_metacache_worthKeeping(t *testing.T) {
+ // TODO: Update...
+ wantResults := []bool{0: true, 1: true, 2: true, 3: false, 4: false, 5: true, 6: true, 7: false, 8: false}
+
+ for i, tt := range metaCacheTestset {
+ t.Run(tt.id, func(t *testing.T) {
+ var want bool
+ if i >= len(wantResults) {
+ t.Logf("no expected result for test #%d", i)
+ } else {
+ want = wantResults[i]
+ }
+
+ got := tt.worthKeeping()
+ if got != want {
+ t.Errorf("#%d: want %v, got %v", i, want, got)
+ }
+ })
+ }
+}
diff --git a/cmd/metrics-realtime.go b/cmd/metrics-realtime.go
new file mode 100644
index 00000000..4a1d5573
--- /dev/null
+++ b/cmd/metrics-realtime.go
@@ -0,0 +1,231 @@
+// Copyright (c) 2015-2022 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/disk"
+ "github.com/minio/minio/internal/net"
+ c "github.com/shirou/gopsutil/v3/cpu"
+ "github.com/shirou/gopsutil/v3/load"
+)
+
+type collectMetricsOpts struct {
+ hosts map[string]struct{}
+ disks map[string]struct{}
+ jobID string
+ depID string
+}
+
+func collectLocalMetrics(types madmin.MetricType, opts collectMetricsOpts) (m madmin.RealtimeMetrics) {
+ if types == madmin.MetricsNone {
+ return m
+ }
+
+ byHostName := globalMinioAddr
+ if len(opts.hosts) > 0 {
+ server := getLocalServerProperty(globalEndpoints, &http.Request{
+ Host: globalLocalNodeName,
+ }, false)
+ if _, ok := opts.hosts[server.Endpoint]; ok {
+ byHostName = server.Endpoint
+ } else {
+ return m
+ }
+ }
+
+ if strings.HasPrefix(byHostName, ":") && !strings.HasPrefix(globalLocalNodeName, ":") {
+ byHostName = globalLocalNodeName
+ }
+
+ if types.Contains(madmin.MetricsDisk) {
+ m.ByDisk = make(map[string]madmin.DiskMetric)
+ aggr := madmin.DiskMetric{
+ CollectedAt: time.Now(),
+ }
+ for name, disk := range collectLocalDisksMetrics(opts.disks) {
+ m.ByDisk[name] = disk
+ aggr.Merge(&disk)
+ }
+ m.Aggregated.Disk = &aggr
+ }
+
+ if types.Contains(madmin.MetricsScanner) {
+ metrics := globalScannerMetrics.report()
+ m.Aggregated.Scanner = &metrics
+ }
+ if types.Contains(madmin.MetricsOS) {
+ metrics := globalOSMetrics.report()
+ m.Aggregated.OS = &metrics
+ }
+ if types.Contains(madmin.MetricsBatchJobs) {
+ m.Aggregated.BatchJobs = globalBatchJobsMetrics.report(opts.jobID)
+ }
+ if types.Contains(madmin.MetricsSiteResync) {
+ m.Aggregated.SiteResync = globalSiteResyncMetrics.report(opts.depID)
+ }
+ if types.Contains(madmin.MetricNet) {
+ m.Aggregated.Net = &madmin.NetMetrics{
+ CollectedAt: UTCNow(),
+ InterfaceName: globalInternodeInterface,
+ }
+ netStats, err := net.GetInterfaceNetStats(globalInternodeInterface)
+ if err != nil {
+ m.Errors = append(m.Errors, fmt.Sprintf("%s: %v (nicstats)", byHostName, err.Error()))
+ } else {
+ m.Aggregated.Net.NetStats = netStats
+ }
+ }
+ if types.Contains(madmin.MetricsMem) {
+ m.Aggregated.Mem = &madmin.MemMetrics{
+ CollectedAt: UTCNow(),
+ }
+ m.Aggregated.Mem.Info = madmin.GetMemInfo(GlobalContext, byHostName)
+ }
+ if types.Contains(madmin.MetricsCPU) {
+ m.Aggregated.CPU = &madmin.CPUMetrics{
+ CollectedAt: UTCNow(),
+ }
+ cm, err := c.Times(false)
+ if err != nil {
+ m.Errors = append(m.Errors, fmt.Sprintf("%s: %v (cpuTimes)", byHostName, err.Error()))
+ } else {
+ // not collecting per-cpu stats, so there will be only one element
+ if len(cm) == 1 {
+ m.Aggregated.CPU.TimesStat = &cm[0]
+ } else {
+ m.Errors = append(m.Errors, fmt.Sprintf("%s: Expected one CPU stat, got %d", byHostName, len(cm)))
+ }
+ }
+ cpuCount, err := c.Counts(true)
+ if err != nil {
+ m.Errors = append(m.Errors, fmt.Sprintf("%s: %v (cpuCount)", byHostName, err.Error()))
+ } else {
+ m.Aggregated.CPU.CPUCount = cpuCount
+ }
+
+ loadStat, err := load.Avg()
+ if err != nil {
+ m.Errors = append(m.Errors, fmt.Sprintf("%s: %v (loadStat)", byHostName, err.Error()))
+ } else {
+ m.Aggregated.CPU.LoadStat = loadStat
+ }
+ }
+ if types.Contains(madmin.MetricsRPC) {
+ gr := globalGrid.Load()
+ if gr == nil {
+ m.Errors = append(m.Errors, fmt.Sprintf("%s: Grid not initialized", byHostName))
+ } else {
+ stats := gr.ConnStats()
+ m.Aggregated.RPC = &stats
+ }
+ }
+ // Add types...
+
+ // ByHost is a shallow reference, so careful about sharing.
+ m.ByHost = map[string]madmin.Metrics{byHostName: m.Aggregated}
+ m.Hosts = append(m.Hosts, byHostName)
+
+ return m
+}
+
+func collectLocalDisksMetrics(disks map[string]struct{}) map[string]madmin.DiskMetric {
+ objLayer := newObjectLayerFn()
+ if objLayer == nil {
+ return nil
+ }
+
+ metrics := make(map[string]madmin.DiskMetric)
+ storageInfo := objLayer.LocalStorageInfo(GlobalContext, true)
+ for _, d := range storageInfo.Disks {
+ if len(disks) != 0 {
+ _, ok := disks[d.Endpoint]
+ if !ok {
+ continue
+ }
+ }
+
+ if d.State != madmin.DriveStateOk && d.State != madmin.DriveStateUnformatted {
+ metrics[d.Endpoint] = madmin.DiskMetric{NDisks: 1, Offline: 1}
+ continue
+ }
+
+ var dm madmin.DiskMetric
+ dm.NDisks = 1
+ if d.Healing {
+ dm.Healing++
+ }
+ if d.Metrics != nil {
+ dm.LifeTimeOps = make(map[string]uint64, len(d.Metrics.APICalls))
+ for k, v := range d.Metrics.APICalls {
+ if v != 0 {
+ dm.LifeTimeOps[k] = v
+ }
+ }
+ dm.LastMinute.Operations = make(map[string]madmin.TimedAction, len(d.Metrics.APICalls))
+ for k, v := range d.Metrics.LastMinute {
+ if v.Count != 0 {
+ dm.LastMinute.Operations[k] = v
+ }
+ }
+ }
+
+ st, err := disk.GetDriveStats(d.Major, d.Minor)
+ if err == nil {
+ dm.IOStats = madmin.DiskIOStats{
+ ReadIOs: st.ReadIOs,
+ ReadMerges: st.ReadMerges,
+ ReadSectors: st.ReadSectors,
+ ReadTicks: st.ReadTicks,
+ WriteIOs: st.WriteIOs,
+ WriteMerges: st.WriteMerges,
+ WriteSectors: st.WriteSectors,
+ WriteTicks: st.WriteTicks,
+ CurrentIOs: st.CurrentIOs,
+ TotalTicks: st.TotalTicks,
+ ReqTicks: st.ReqTicks,
+ DiscardIOs: st.DiscardIOs,
+ DiscardMerges: st.DiscardMerges,
+ DiscardSectors: st.DiscardSectors,
+ DiscardTicks: st.DiscardTicks,
+ FlushIOs: st.FlushIOs,
+ FlushTicks: st.FlushTicks,
+ }
+ }
+
+ metrics[d.Endpoint] = dm
+ }
+ return metrics
+}
+
+func collectRemoteMetrics(ctx context.Context, types madmin.MetricType, opts collectMetricsOpts) (m madmin.RealtimeMetrics) {
+ if !globalIsDistErasure {
+ return m
+ }
+ all := globalNotificationSys.GetMetrics(ctx, types, opts)
+ for _, remote := range all {
+ m.Merge(&remote)
+ }
+ return m
+}
diff --git a/cmd/metrics-resource.go b/cmd/metrics-resource.go
new file mode 100644
index 00000000..94c91873
--- /dev/null
+++ b/cmd/metrics-resource.go
@@ -0,0 +1,493 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "math"
+ "net/http"
+ "sync"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+const (
+ resourceMetricsCollectionInterval = time.Minute
+ resourceMetricsCacheInterval = time.Minute
+
+ // drive stats
+ totalInodes MetricName = "total_inodes"
+ readsPerSec MetricName = "reads_per_sec"
+ writesPerSec MetricName = "writes_per_sec"
+ readsKBPerSec MetricName = "reads_kb_per_sec"
+ writesKBPerSec MetricName = "writes_kb_per_sec"
+ readsAwait MetricName = "reads_await"
+ writesAwait MetricName = "writes_await"
+ percUtil MetricName = "perc_util"
+ usedInodes MetricName = "used_inodes"
+
+ // network stats
+ interfaceRxBytes MetricName = "rx_bytes"
+ interfaceRxErrors MetricName = "rx_errors"
+ interfaceTxBytes MetricName = "tx_bytes"
+ interfaceTxErrors MetricName = "tx_errors"
+
+ // cpu stats
+ cpuUser MetricName = "user"
+ cpuSystem MetricName = "system"
+ cpuIOWait MetricName = "iowait"
+ cpuIdle MetricName = "idle"
+ cpuNice MetricName = "nice"
+ cpuSteal MetricName = "steal"
+ cpuLoad1 MetricName = "load1"
+ cpuLoad5 MetricName = "load5"
+ cpuLoad15 MetricName = "load15"
+ cpuLoad1Perc MetricName = "load1_perc"
+ cpuLoad5Perc MetricName = "load5_perc"
+ cpuLoad15Perc MetricName = "load15_perc"
+)
+
+var (
+ resourceCollector *minioResourceCollector
+ // resourceMetricsMap is a map of subsystem to its metrics
+ resourceMetricsMap map[MetricSubsystem]ResourceMetrics
+ resourceMetricsMapMu sync.RWMutex
+ // resourceMetricsHelpMap maps metric name to its help string
+ resourceMetricsHelpMap map[MetricName]string
+ resourceMetricsGroups []*MetricsGroupV2
+ // initial values for drives (at the time of server startup)
+ // used for calculating avg values for drive metrics
+ latestDriveStats map[string]madmin.DiskIOStats
+ latestDriveStatsMu sync.RWMutex
+ lastDriveStatsRefresh time.Time
+)
+
+// PeerResourceMetrics represents the resource metrics
+// retrieved from a peer, along with errors if any
+type PeerResourceMetrics struct {
+ Metrics map[MetricSubsystem]ResourceMetrics
+ Errors []string
+}
+
+// ResourceMetrics is a map of unique key identifying
+// a resource metric (e.g. reads_per_sec_{node}_{drive})
+// to its data
+type ResourceMetrics map[string]ResourceMetric
+
+// ResourceMetric represents a single resource metric
+// The metrics are collected from all servers periodically
+// and stored in the resource metrics map.
+// It also maintains the count of number of times this metric
+// was collected since the server started, and the sum,
+// average and max values across the same.
+type ResourceMetric struct {
+ Name MetricName
+ Labels map[string]string
+
+ // value captured in current cycle
+ Current float64
+
+ // Used when system provides cumulative (since uptime) values
+ // helps in calculating the current value by comparing the new
+ // cumulative value with previous one
+ Cumulative float64
+
+ Max float64
+ Avg float64
+ Sum float64
+ Count uint64
+}
+
+func init() {
+ interval := fmt.Sprintf("%ds", int(resourceMetricsCollectionInterval.Seconds()))
+ resourceMetricsHelpMap = map[MetricName]string{
+ interfaceRxBytes: "Bytes received on the interface in " + interval,
+ interfaceRxErrors: "Receive errors in " + interval,
+ interfaceTxBytes: "Bytes transmitted in " + interval,
+ interfaceTxErrors: "Transmit errors in " + interval,
+ total: "Total memory on the node",
+ memUsed: "Used memory on the node",
+ memUsedPerc: "Used memory percentage on the node",
+ memFree: "Free memory on the node",
+ memShared: "Shared memory on the node",
+ memBuffers: "Buffers memory on the node",
+ memCache: "Cache memory on the node",
+ memAvailable: "Available memory on the node",
+ readsPerSec: "Reads per second on a drive",
+ writesPerSec: "Writes per second on a drive",
+ readsKBPerSec: "Kilobytes read per second on a drive",
+ writesKBPerSec: "Kilobytes written per second on a drive",
+ readsAwait: "Average time for read requests to be served on a drive",
+ writesAwait: "Average time for write requests to be served on a drive",
+ percUtil: "Percentage of time the disk was busy",
+ usedBytes: "Used bytes on a drive",
+ totalBytes: "Total bytes on a drive",
+ usedInodes: "Total inodes used on a drive",
+ totalInodes: "Total inodes on a drive",
+ cpuUser: "CPU user time",
+ cpuSystem: "CPU system time",
+ cpuIdle: "CPU idle time",
+ cpuIOWait: "CPU ioWait time",
+ cpuSteal: "CPU steal time",
+ cpuNice: "CPU nice time",
+ cpuLoad1: "CPU load average 1min",
+ cpuLoad5: "CPU load average 5min",
+ cpuLoad15: "CPU load average 15min",
+ cpuLoad1Perc: "CPU load average 1min (percentage)",
+ cpuLoad5Perc: "CPU load average 5min (percentage)",
+ cpuLoad15Perc: "CPU load average 15min (percentage)",
+ }
+ resourceMetricsGroups = []*MetricsGroupV2{
+ getResourceMetrics(),
+ }
+
+ resourceCollector = newMinioResourceCollector(resourceMetricsGroups)
+}
+
+func getResourceKey(name MetricName, labels map[string]string) string {
+ // labels are used to uniquely identify a metric
+ // e.g. reads_per_sec_{drive} inside the map
+ sfx := ""
+ for _, v := range labels {
+ if len(sfx) > 0 {
+ sfx += "_"
+ }
+ sfx += v
+ }
+
+ return string(name) + "_" + sfx
+}
+
+func updateResourceMetrics(subSys MetricSubsystem, name MetricName, val float64, labels map[string]string, isCumulative bool) {
+ resourceMetricsMapMu.Lock()
+ defer resourceMetricsMapMu.Unlock()
+ subsysMetrics, found := resourceMetricsMap[subSys]
+ if !found {
+ subsysMetrics = ResourceMetrics{}
+ }
+
+ key := getResourceKey(name, labels)
+ metric, found := subsysMetrics[key]
+ if !found {
+ metric = ResourceMetric{
+ Name: name,
+ Labels: labels,
+ }
+ }
+
+ if isCumulative {
+ metric.Current = val - metric.Cumulative
+ metric.Cumulative = val
+ } else {
+ metric.Current = val
+ }
+
+ if metric.Current > metric.Max {
+ metric.Max = val
+ }
+
+ metric.Sum += metric.Current
+ metric.Count++
+
+ metric.Avg = metric.Sum / float64(metric.Count)
+ subsysMetrics[key] = metric
+
+ resourceMetricsMap[subSys] = subsysMetrics
+}
+
+// updateDriveIOStats - Updates the drive IO stats by calculating the difference between the current and latest updated values.
+func updateDriveIOStats(currentStats madmin.DiskIOStats, latestStats madmin.DiskIOStats, labels map[string]string) {
+ sectorSize := uint64(512)
+ kib := float64(1 << 10)
+ diffInSeconds := time.Now().UTC().Sub(lastDriveStatsRefresh).Seconds()
+ if diffInSeconds == 0 {
+ // too soon to update the stats
+ return
+ }
+ diffStats := getDiffStats(latestStats, currentStats)
+
+ updateResourceMetrics(driveSubsystem, readsPerSec, float64(diffStats.ReadIOs)/diffInSeconds, labels, false)
+ readKib := float64(diffStats.ReadSectors*sectorSize) / kib
+ updateResourceMetrics(driveSubsystem, readsKBPerSec, readKib/diffInSeconds, labels, false)
+
+ updateResourceMetrics(driveSubsystem, writesPerSec, float64(diffStats.WriteIOs)/diffInSeconds, labels, false)
+ writeKib := float64(diffStats.WriteSectors*sectorSize) / kib
+ updateResourceMetrics(driveSubsystem, writesKBPerSec, writeKib/diffInSeconds, labels, false)
+
+ rdAwait := 0.0
+ if diffStats.ReadIOs > 0 {
+ rdAwait = float64(diffStats.ReadTicks) / float64(diffStats.ReadIOs)
+ }
+ updateResourceMetrics(driveSubsystem, readsAwait, rdAwait, labels, false)
+
+ wrAwait := 0.0
+ if diffStats.WriteIOs > 0 {
+ wrAwait = float64(diffStats.WriteTicks) / float64(diffStats.WriteIOs)
+ }
+ updateResourceMetrics(driveSubsystem, writesAwait, wrAwait, labels, false)
+ updateResourceMetrics(driveSubsystem, percUtil, float64(diffStats.TotalTicks)/(diffInSeconds*10), labels, false)
+}
+
+func collectDriveMetrics(m madmin.RealtimeMetrics) {
+ latestDriveStatsMu.Lock()
+ for d, dm := range m.ByDisk {
+ labels := map[string]string{"drive": d}
+ latestStats, ok := latestDriveStats[d]
+ if !ok {
+ latestDriveStats[d] = dm.IOStats
+ continue
+ }
+ updateDriveIOStats(dm.IOStats, latestStats, labels)
+ latestDriveStats[d] = dm.IOStats
+ }
+ lastDriveStatsRefresh = time.Now().UTC()
+ latestDriveStatsMu.Unlock()
+
+ globalLocalDrivesMu.RLock()
+ localDrives := cloneDrives(globalLocalDrivesMap)
+ globalLocalDrivesMu.RUnlock()
+
+ for _, d := range localDrives {
+ di, err := d.DiskInfo(GlobalContext, DiskInfoOptions{})
+ labels := map[string]string{"drive": di.Endpoint}
+ if err == nil {
+ updateResourceMetrics(driveSubsystem, usedBytes, float64(di.Used), labels, false)
+ updateResourceMetrics(driveSubsystem, totalBytes, float64(di.Total), labels, false)
+ updateResourceMetrics(driveSubsystem, usedInodes, float64(di.UsedInodes), labels, false)
+ updateResourceMetrics(driveSubsystem, totalInodes, float64(di.FreeInodes+di.UsedInodes), labels, false)
+ }
+ }
+}
+
+func collectLocalResourceMetrics() {
+ types := madmin.MetricsDisk | madmin.MetricNet | madmin.MetricsMem | madmin.MetricsCPU
+
+ m := collectLocalMetrics(types, collectMetricsOpts{})
+ for _, hm := range m.ByHost {
+ if hm.Net != nil && len(hm.Net.NetStats.Name) > 0 {
+ stats := hm.Net.NetStats
+ labels := map[string]string{"interface": stats.Name}
+ updateResourceMetrics(interfaceSubsystem, interfaceRxBytes, float64(stats.RxBytes), labels, true)
+ updateResourceMetrics(interfaceSubsystem, interfaceRxErrors, float64(stats.RxErrors), labels, true)
+ updateResourceMetrics(interfaceSubsystem, interfaceTxBytes, float64(stats.TxBytes), labels, true)
+ updateResourceMetrics(interfaceSubsystem, interfaceTxErrors, float64(stats.TxErrors), labels, true)
+ }
+ if hm.Mem != nil && len(hm.Mem.Info.Addr) > 0 {
+ labels := map[string]string{}
+ stats := hm.Mem.Info
+ updateResourceMetrics(memSubsystem, total, float64(stats.Total), labels, false)
+ updateResourceMetrics(memSubsystem, memUsed, float64(stats.Used), labels, false)
+ perc := math.Round(float64(stats.Used*100*100)/float64(stats.Total)) / 100
+ updateResourceMetrics(memSubsystem, memUsedPerc, perc, labels, false)
+ updateResourceMetrics(memSubsystem, memFree, float64(stats.Free), labels, false)
+ updateResourceMetrics(memSubsystem, memShared, float64(stats.Shared), labels, false)
+ updateResourceMetrics(memSubsystem, memBuffers, float64(stats.Buffers), labels, false)
+ updateResourceMetrics(memSubsystem, memAvailable, float64(stats.Available), labels, false)
+ updateResourceMetrics(memSubsystem, memCache, float64(stats.Cache), labels, false)
+ }
+ if hm.CPU != nil {
+ labels := map[string]string{}
+ ts := hm.CPU.TimesStat
+ if ts != nil {
+ tot := ts.User + ts.System + ts.Idle + ts.Iowait + ts.Nice + ts.Steal
+ cpuUserVal := math.Round(ts.User/tot*100*100) / 100
+ updateResourceMetrics(cpuSubsystem, cpuUser, cpuUserVal, labels, false)
+ cpuSystemVal := math.Round(ts.System/tot*100*100) / 100
+ updateResourceMetrics(cpuSubsystem, cpuSystem, cpuSystemVal, labels, false)
+ cpuIdleVal := math.Round(ts.Idle/tot*100*100) / 100
+ updateResourceMetrics(cpuSubsystem, cpuIdle, cpuIdleVal, labels, false)
+ cpuIOWaitVal := math.Round(ts.Iowait/tot*100*100) / 100
+ updateResourceMetrics(cpuSubsystem, cpuIOWait, cpuIOWaitVal, labels, false)
+ cpuNiceVal := math.Round(ts.Nice/tot*100*100) / 100
+ updateResourceMetrics(cpuSubsystem, cpuNice, cpuNiceVal, labels, false)
+ cpuStealVal := math.Round(ts.Steal/tot*100*100) / 100
+ updateResourceMetrics(cpuSubsystem, cpuSteal, cpuStealVal, labels, false)
+ }
+ ls := hm.CPU.LoadStat
+ if ls != nil {
+ updateResourceMetrics(cpuSubsystem, cpuLoad1, ls.Load1, labels, false)
+ updateResourceMetrics(cpuSubsystem, cpuLoad5, ls.Load5, labels, false)
+ updateResourceMetrics(cpuSubsystem, cpuLoad15, ls.Load15, labels, false)
+ if hm.CPU.CPUCount > 0 {
+ perc := math.Round(ls.Load1*100*100/float64(hm.CPU.CPUCount)) / 100
+ updateResourceMetrics(cpuSubsystem, cpuLoad1Perc, perc, labels, false)
+ perc = math.Round(ls.Load5*100*100/float64(hm.CPU.CPUCount)) / 100
+ updateResourceMetrics(cpuSubsystem, cpuLoad5Perc, perc, labels, false)
+ perc = math.Round(ls.Load15*100*100/float64(hm.CPU.CPUCount)) / 100
+ updateResourceMetrics(cpuSubsystem, cpuLoad15Perc, perc, labels, false)
+ }
+ }
+ }
+ break // only one host expected
+ }
+
+ collectDriveMetrics(m)
+}
+
+func initLatestValues() {
+ m := collectLocalMetrics(madmin.MetricsDisk, collectMetricsOpts{})
+ latestDriveStatsMu.Lock()
+ latestDriveStats = map[string]madmin.DiskIOStats{}
+ for d, dm := range m.ByDisk {
+ latestDriveStats[d] = dm.IOStats
+ }
+ lastDriveStatsRefresh = time.Now().UTC()
+ latestDriveStatsMu.Unlock()
+}
+
+// startResourceMetricsCollection - starts the job for collecting resource metrics
+func startResourceMetricsCollection() {
+ initLatestValues()
+
+ resourceMetricsMapMu.Lock()
+ resourceMetricsMap = map[MetricSubsystem]ResourceMetrics{}
+ resourceMetricsMapMu.Unlock()
+ metricsTimer := time.NewTimer(resourceMetricsCollectionInterval)
+ defer metricsTimer.Stop()
+
+ collectLocalResourceMetrics()
+
+ for {
+ select {
+ case <-GlobalContext.Done():
+ return
+ case <-metricsTimer.C:
+ collectLocalResourceMetrics()
+
+ // Reset the timer for next cycle.
+ metricsTimer.Reset(resourceMetricsCollectionInterval)
+ }
+ }
+}
+
+// minioResourceCollector is the Collector for resource metrics
+type minioResourceCollector struct {
+ metricsGroups []*MetricsGroupV2
+ desc *prometheus.Desc
+}
+
+// Describe sends the super-set of all possible descriptors of metrics
+func (c *minioResourceCollector) Describe(ch chan<- *prometheus.Desc) {
+ ch <- c.desc
+}
+
+// Collect is called by the Prometheus registry when collecting metrics.
+func (c *minioResourceCollector) Collect(out chan<- prometheus.Metric) {
+ var wg sync.WaitGroup
+ publish := func(in <-chan MetricV2) {
+ defer wg.Done()
+ for metric := range in {
+ labels, values := getOrderedLabelValueArrays(metric.VariableLabels)
+ collectMetric(metric, labels, values, "resource", out)
+ }
+ }
+
+ // Call peer api to fetch metrics
+ wg.Add(2)
+ go publish(ReportMetrics(GlobalContext, c.metricsGroups))
+ go publish(globalNotificationSys.GetResourceMetrics(GlobalContext))
+ wg.Wait()
+}
+
+// newMinioResourceCollector describes the collector
+// and returns reference of minio resource Collector
+// It creates the Prometheus Description which is used
+// to define Metric and help string
+func newMinioResourceCollector(metricsGroups []*MetricsGroupV2) *minioResourceCollector {
+ return &minioResourceCollector{
+ metricsGroups: metricsGroups,
+ desc: prometheus.NewDesc("minio_resource_stats", "Resource statistics exposed by MinIO server", nil, nil),
+ }
+}
+
+func prepareResourceMetrics(rm ResourceMetric, subSys MetricSubsystem, requireAvgMax bool) []MetricV2 {
+ help := resourceMetricsHelpMap[rm.Name]
+ name := rm.Name
+ metrics := make([]MetricV2, 0, 3)
+ metrics = append(metrics, MetricV2{
+ Description: getResourceMetricDescription(subSys, name, help),
+ Value: rm.Current,
+ VariableLabels: cloneMSS(rm.Labels),
+ })
+
+ if requireAvgMax {
+ avgName := MetricName(fmt.Sprintf("%s_avg", name))
+ avgHelp := fmt.Sprintf("%s (avg)", help)
+ metrics = append(metrics, MetricV2{
+ Description: getResourceMetricDescription(subSys, avgName, avgHelp),
+ Value: math.Round(rm.Avg*100) / 100,
+ VariableLabels: cloneMSS(rm.Labels),
+ })
+
+ maxName := MetricName(fmt.Sprintf("%s_max", name))
+ maxHelp := fmt.Sprintf("%s (max)", help)
+ metrics = append(metrics, MetricV2{
+ Description: getResourceMetricDescription(subSys, maxName, maxHelp),
+ Value: rm.Max,
+ VariableLabels: cloneMSS(rm.Labels),
+ })
+ }
+
+ return metrics
+}
+
+func getResourceMetricDescription(subSys MetricSubsystem, name MetricName, help string) MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: subSys,
+ Name: name,
+ Help: help,
+ Type: gaugeMetric,
+ }
+}
+
+func getResourceMetrics() *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: resourceMetricsCacheInterval,
+ }
+ mg.RegisterRead(func(ctx context.Context) []MetricV2 {
+ metrics := []MetricV2{}
+
+ subSystems := []MetricSubsystem{interfaceSubsystem, memSubsystem, driveSubsystem, cpuSubsystem}
+ resourceMetricsMapMu.RLock()
+ defer resourceMetricsMapMu.RUnlock()
+ for _, subSys := range subSystems {
+ stats, found := resourceMetricsMap[subSys]
+ if found {
+ requireAvgMax := true
+ if subSys == driveSubsystem {
+ requireAvgMax = false
+ }
+ for _, m := range stats {
+ metrics = append(metrics, prepareResourceMetrics(m, subSys, requireAvgMax)...)
+ }
+ }
+ }
+
+ return metrics
+ })
+ return mg
+}
+
+// metricsResourceHandler is the prometheus handler for resource metrics
+func metricsResourceHandler() http.Handler {
+ return metricsHTTPHandler(resourceCollector, "handler.MetricsResource")
+}
diff --git a/cmd/metrics-router.go b/cmd/metrics-router.go
new file mode 100644
index 00000000..f8b85c25
--- /dev/null
+++ b/cmd/metrics-router.go
@@ -0,0 +1,75 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "net/http"
+ "strings"
+
+ "github.com/minio/mux"
+ "github.com/minio/pkg/v3/env"
+)
+
+const (
+ prometheusMetricsPathLegacy = "/prometheus/metrics"
+ prometheusMetricsV2ClusterPath = "/v2/metrics/cluster"
+ prometheusMetricsV2BucketPath = "/v2/metrics/bucket"
+ prometheusMetricsV2NodePath = "/v2/metrics/node"
+ prometheusMetricsV2ResourcePath = "/v2/metrics/resource"
+
+ // Metrics v3 endpoints
+ metricsV3Path = "/metrics/v3"
+)
+
+// Standard env prometheus auth type
+const (
+ EnvPrometheusAuthType = "MINIO_PROMETHEUS_AUTH_TYPE"
+ EnvPrometheusOpenMetrics = "MINIO_PROMETHEUS_OPEN_METRICS"
+)
+
+type prometheusAuthType string
+
+const (
+ prometheusJWT prometheusAuthType = "jwt"
+ prometheusPublic prometheusAuthType = "public"
+)
+
+// registerMetricsRouter - add handler functions for metrics.
+func registerMetricsRouter(router *mux.Router) {
+ // metrics router
+ metricsRouter := router.NewRoute().PathPrefix(minioReservedBucketPath).Subrouter()
+ authType := prometheusAuthType(strings.ToLower(env.Get(EnvPrometheusAuthType, string(prometheusJWT))))
+
+ auth := AuthMiddleware
+ if authType == prometheusPublic {
+ auth = NoAuthMiddleware
+ }
+
+ metricsRouter.Handle(prometheusMetricsPathLegacy, auth(metricsHandler()))
+ metricsRouter.Handle(prometheusMetricsV2ClusterPath, auth(metricsServerHandler()))
+ metricsRouter.Handle(prometheusMetricsV2BucketPath, auth(metricsBucketHandler()))
+ metricsRouter.Handle(prometheusMetricsV2NodePath, auth(metricsNodeHandler()))
+ metricsRouter.Handle(prometheusMetricsV2ResourcePath, auth(metricsResourceHandler()))
+
+ // Metrics v3
+ metricsV3Server := newMetricsV3Server(auth)
+
+ // Register metrics v3 handler. It also accepts an optional query
+ // parameter `?list` - see handler for details.
+ metricsRouter.Methods(http.MethodGet).Path(metricsV3Path + "{pathComps:.*}").Handler(metricsV3Server)
+}
diff --git a/cmd/metrics-v2.go b/cmd/metrics-v2.go
new file mode 100644
index 00000000..e38750a5
--- /dev/null
+++ b/cmd/metrics-v2.go
@@ -0,0 +1,4435 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "maps"
+ "math"
+ "net/http"
+ "runtime"
+ "strconv"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/minio/kms-go/kes"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/bucket/lifecycle"
+ "github.com/minio/minio/internal/cachevalue"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/minio/internal/mcontext"
+ "github.com/minio/minio/internal/rest"
+ "github.com/prometheus/client_golang/prometheus"
+ dto "github.com/prometheus/client_model/go"
+ "github.com/prometheus/common/expfmt"
+ "github.com/prometheus/procfs"
+)
+
+//go:generate msgp -file=$GOFILE -unexported -io=false
+
+var (
+ nodeCollector *minioNodeCollector
+ clusterCollector *minioClusterCollector
+ bucketCollector *minioBucketCollector
+ peerMetricsGroups []*MetricsGroupV2
+ bucketPeerMetricsGroups []*MetricsGroupV2
+)
+
+// v2MetricsMaxBuckets enforces a bucket count limit on metrics for v2 calls.
+// If people hit this limit, they should move to v3, as certain calls explode with high bucket count.
+const v2MetricsMaxBuckets = 100
+
+func init() {
+ clusterMetricsGroups := []*MetricsGroupV2{
+ getNodeHealthMetrics(MetricsGroupOpts{dependGlobalNotificationSys: true}),
+ getClusterStorageMetrics(MetricsGroupOpts{dependGlobalObjectAPI: true}),
+ getClusterTierMetrics(MetricsGroupOpts{dependGlobalObjectAPI: true}),
+ getClusterUsageMetrics(MetricsGroupOpts{dependGlobalObjectAPI: true}),
+ getKMSMetrics(MetricsGroupOpts{dependGlobalObjectAPI: true, dependGlobalKMS: true}),
+ getClusterHealthMetrics(MetricsGroupOpts{dependGlobalObjectAPI: true}),
+ getIAMNodeMetrics(MetricsGroupOpts{dependGlobalAuthNPlugin: true, dependGlobalIAMSys: true}),
+ getReplicationSiteMetrics(MetricsGroupOpts{dependGlobalSiteReplicationSys: true}),
+ getBatchJobsMetrics(MetricsGroupOpts{dependGlobalObjectAPI: true}),
+ }
+
+ peerMetricsGroups = []*MetricsGroupV2{
+ getGoMetrics(),
+ getHTTPMetrics(MetricsGroupOpts{}),
+ getNotificationMetrics(MetricsGroupOpts{dependGlobalLambdaTargetList: true}),
+ getMinioProcMetrics(),
+ getMinioVersionMetrics(),
+ getNetworkMetrics(),
+ getS3TTFBMetric(),
+ getILMNodeMetrics(),
+ getScannerNodeMetrics(),
+ getIAMNodeMetrics(MetricsGroupOpts{dependGlobalAuthNPlugin: true, dependGlobalIAMSys: true}),
+ getKMSNodeMetrics(MetricsGroupOpts{dependGlobalObjectAPI: true, dependGlobalKMS: true}),
+ getMinioHealingMetrics(MetricsGroupOpts{dependGlobalBackgroundHealState: true}),
+ getWebhookMetrics(),
+ getTierMetrics(),
+ }
+
+ allMetricsGroups := func() (allMetrics []*MetricsGroupV2) {
+ allMetrics = append(allMetrics, clusterMetricsGroups...)
+ allMetrics = append(allMetrics, peerMetricsGroups...)
+ return allMetrics
+ }()
+
+ nodeGroups := []*MetricsGroupV2{
+ getNodeHealthMetrics(MetricsGroupOpts{dependGlobalNotificationSys: true}),
+ getHTTPMetrics(MetricsGroupOpts{}),
+ getNetworkMetrics(),
+ getMinioVersionMetrics(),
+ getS3TTFBMetric(),
+ getTierMetrics(),
+ getNotificationMetrics(MetricsGroupOpts{dependGlobalLambdaTargetList: true}),
+ getDistLockMetrics(MetricsGroupOpts{dependGlobalIsDistErasure: true, dependGlobalLockServer: true}),
+ getIAMNodeMetrics(MetricsGroupOpts{dependGlobalAuthNPlugin: true, dependGlobalIAMSys: true}),
+ getLocalStorageMetrics(MetricsGroupOpts{dependGlobalObjectAPI: true}),
+ getReplicationNodeMetrics(MetricsGroupOpts{dependGlobalObjectAPI: true, dependBucketTargetSys: true}),
+ }
+
+ bucketMetricsGroups := []*MetricsGroupV2{
+ getBucketUsageMetrics(MetricsGroupOpts{dependGlobalObjectAPI: true}),
+ getHTTPMetrics(MetricsGroupOpts{bucketOnly: true}),
+ getBucketTTFBMetric(),
+ }
+
+ bucketPeerMetricsGroups = []*MetricsGroupV2{
+ getHTTPMetrics(MetricsGroupOpts{bucketOnly: true}),
+ getBucketTTFBMetric(),
+ }
+
+ nodeCollector = newMinioCollectorNode(nodeGroups)
+ clusterCollector = newMinioClusterCollector(allMetricsGroups)
+ bucketCollector = newMinioBucketCollector(bucketMetricsGroups)
+}
+
+// MetricNamespace is top level grouping of metrics to create the metric name.
+type MetricNamespace string
+
+// MetricSubsystem is the sub grouping for metrics within a namespace.
+type MetricSubsystem string
+
+const (
+ bucketMetricNamespace MetricNamespace = "minio_bucket"
+ clusterMetricNamespace MetricNamespace = "minio_cluster"
+ healMetricNamespace MetricNamespace = "minio_heal"
+ interNodeMetricNamespace MetricNamespace = "minio_inter_node"
+ nodeMetricNamespace MetricNamespace = "minio_node"
+ minioMetricNamespace MetricNamespace = "minio"
+ s3MetricNamespace MetricNamespace = "minio_s3"
+)
+
+const (
+ cacheSubsystem MetricSubsystem = "cache"
+ capacityRawSubsystem MetricSubsystem = "capacity_raw"
+ capacityUsableSubsystem MetricSubsystem = "capacity_usable"
+ driveSubsystem MetricSubsystem = "drive"
+ interfaceSubsystem MetricSubsystem = "if"
+ memSubsystem MetricSubsystem = "mem"
+ cpuSubsystem MetricSubsystem = "cpu_avg"
+ storageClassSubsystem MetricSubsystem = "storage_class"
+ fileDescriptorSubsystem MetricSubsystem = "file_descriptor"
+ goRoutines MetricSubsystem = "go_routine"
+ ioSubsystem MetricSubsystem = "io"
+ nodesSubsystem MetricSubsystem = "nodes"
+ objectsSubsystem MetricSubsystem = "objects"
+ bucketsSubsystem MetricSubsystem = "bucket"
+ processSubsystem MetricSubsystem = "process"
+ replicationSubsystem MetricSubsystem = "replication"
+ requestsSubsystem MetricSubsystem = "requests"
+ requestsRejectedSubsystem MetricSubsystem = "requests_rejected"
+ timeSubsystem MetricSubsystem = "time"
+ ttfbSubsystem MetricSubsystem = "requests_ttfb"
+ trafficSubsystem MetricSubsystem = "traffic"
+ softwareSubsystem MetricSubsystem = "software"
+ sysCallSubsystem MetricSubsystem = "syscall"
+ usageSubsystem MetricSubsystem = "usage"
+ quotaSubsystem MetricSubsystem = "quota"
+ ilmSubsystem MetricSubsystem = "ilm"
+ tierSubsystem MetricSubsystem = "tier"
+ scannerSubsystem MetricSubsystem = "scanner"
+ iamSubsystem MetricSubsystem = "iam"
+ kmsSubsystem MetricSubsystem = "kms"
+ notifySubsystem MetricSubsystem = "notify"
+ lambdaSubsystem MetricSubsystem = "lambda"
+ auditSubsystem MetricSubsystem = "audit"
+ webhookSubsystem MetricSubsystem = "webhook"
+)
+
+// MetricName are the individual names for the metric.
+type MetricName string
+
+const (
+ authTotal MetricName = "auth_total"
+ canceledTotal MetricName = "canceled_total"
+ errorsTotal MetricName = "errors_total"
+ headerTotal MetricName = "header_total"
+ healTotal MetricName = "heal_total"
+ hitsTotal MetricName = "hits_total"
+ inflightTotal MetricName = "inflight_total"
+ invalidTotal MetricName = "invalid_total"
+ limitTotal MetricName = "limit_total"
+ missedTotal MetricName = "missed_total"
+ waitingTotal MetricName = "waiting_total"
+ incomingTotal MetricName = "incoming_total"
+ objectTotal MetricName = "object_total"
+ versionTotal MetricName = "version_total"
+ deleteMarkerTotal MetricName = "deletemarker_total"
+ offlineTotal MetricName = "offline_total"
+ onlineTotal MetricName = "online_total"
+ openTotal MetricName = "open_total"
+ readTotal MetricName = "read_total"
+ timestampTotal MetricName = "timestamp_total"
+ writeTotal MetricName = "write_total"
+ total MetricName = "total"
+ freeInodes MetricName = "free_inodes"
+
+ lastMinFailedCount MetricName = "last_minute_failed_count"
+ lastMinFailedBytes MetricName = "last_minute_failed_bytes"
+ lastHourFailedCount MetricName = "last_hour_failed_count"
+ lastHourFailedBytes MetricName = "last_hour_failed_bytes"
+ totalFailedCount MetricName = "total_failed_count"
+ totalFailedBytes MetricName = "total_failed_bytes"
+
+ currActiveWorkers MetricName = "current_active_workers"
+ avgActiveWorkers MetricName = "average_active_workers"
+ maxActiveWorkers MetricName = "max_active_workers"
+ recentBacklogCount MetricName = "recent_backlog_count"
+ currInQueueCount MetricName = "last_minute_queued_count"
+ currInQueueBytes MetricName = "last_minute_queued_bytes"
+ receivedCount MetricName = "received_count"
+ sentCount MetricName = "sent_count"
+ currTransferRate MetricName = "current_transfer_rate"
+ avgTransferRate MetricName = "average_transfer_rate"
+ maxTransferRate MetricName = "max_transfer_rate"
+ credentialErrors MetricName = "credential_errors"
+
+ currLinkLatency MetricName = "current_link_latency_ms"
+ avgLinkLatency MetricName = "average_link_latency_ms"
+ maxLinkLatency MetricName = "max_link_latency_ms"
+
+ linkOnline MetricName = "link_online"
+ linkOfflineDuration MetricName = "link_offline_duration_seconds"
+ linkDowntimeTotalDuration MetricName = "link_downtime_duration_seconds"
+
+ avgInQueueCount MetricName = "average_queued_count"
+ avgInQueueBytes MetricName = "average_queued_bytes"
+ maxInQueueCount MetricName = "max_queued_count"
+ maxInQueueBytes MetricName = "max_queued_bytes"
+ proxiedGetRequestsTotal MetricName = "proxied_get_requests_total"
+ proxiedHeadRequestsTotal MetricName = "proxied_head_requests_total"
+ proxiedPutTaggingRequestsTotal MetricName = "proxied_put_tagging_requests_total"
+ proxiedGetTaggingRequestsTotal MetricName = "proxied_get_tagging_requests_total"
+ proxiedDeleteTaggingRequestsTotal MetricName = "proxied_delete_tagging_requests_total"
+ proxiedGetRequestsFailures MetricName = "proxied_get_requests_failures"
+ proxiedHeadRequestsFailures MetricName = "proxied_head_requests_failures"
+ proxiedPutTaggingRequestFailures MetricName = "proxied_put_tagging_requests_failures"
+ proxiedGetTaggingRequestFailures MetricName = "proxied_get_tagging_requests_failures"
+ proxiedDeleteTaggingRequestFailures MetricName = "proxied_delete_tagging_requests_failures"
+
+ freeBytes MetricName = "free_bytes"
+ readBytes MetricName = "read_bytes"
+ rcharBytes MetricName = "rchar_bytes"
+ receivedBytes MetricName = "received_bytes"
+ latencyMilliSec MetricName = "latency_ms"
+ sentBytes MetricName = "sent_bytes"
+ totalBytes MetricName = "total_bytes"
+ usedBytes MetricName = "used_bytes"
+ writeBytes MetricName = "write_bytes"
+ wcharBytes MetricName = "wchar_bytes"
+
+ latencyMicroSec MetricName = "latency_us"
+ latencyNanoSec MetricName = "latency_ns"
+
+ commitInfo MetricName = "commit_info"
+ usageInfo MetricName = "usage_info"
+ versionInfo MetricName = "version_info"
+
+ sizeDistribution = "size_distribution"
+ versionDistribution = "version_distribution"
+ ttfbDistribution = "seconds_distribution"
+ ttlbDistribution = "ttlb_seconds_distribution"
+
+ lastActivityTime = "last_activity_nano_seconds"
+ startTime = "starttime_seconds"
+ upTime = "uptime_seconds"
+ memory = "resident_memory_bytes"
+ vmemory = "virtual_memory_bytes"
+ cpu = "cpu_total_seconds"
+
+ expiryMissedTasks MetricName = "expiry_missed_tasks"
+ expiryMissedFreeVersions MetricName = "expiry_missed_freeversions"
+ expiryMissedTierJournalTasks MetricName = "expiry_missed_tierjournal_tasks"
+ expiryNumWorkers MetricName = "expiry_num_workers"
+ transitionMissedTasks MetricName = "transition_missed_immediate_tasks"
+
+ transitionedBytes MetricName = "transitioned_bytes"
+ transitionedObjects MetricName = "transitioned_objects"
+ transitionedVersions MetricName = "transitioned_versions"
+
+ tierRequestsSuccess MetricName = "requests_success"
+ tierRequestsFailure MetricName = "requests_failure"
+
+ kmsOnline = "online"
+ kmsRequestsSuccess = "request_success"
+ kmsRequestsError = "request_error"
+ kmsRequestsFail = "request_failure"
+ kmsUptime = "uptime"
+
+ webhookOnline = "online"
+)
+
+const (
+ serverName = "server"
+)
+
+// MetricTypeV2 for the types of metrics supported
+type MetricTypeV2 string
+
+const (
+ gaugeMetric = "gaugeMetric"
+ counterMetric = "counterMetric"
+ histogramMetric = "histogramMetric"
+)
+
+// MetricDescription describes the metric
+type MetricDescription struct {
+ Namespace MetricNamespace `json:"MetricNamespace"`
+ Subsystem MetricSubsystem `json:"Subsystem"`
+ Name MetricName `json:"MetricName"`
+ Help string `json:"Help"`
+ Type MetricTypeV2 `json:"Type"`
+}
+
+// MetricV2 captures the details for a metric
+type MetricV2 struct {
+ Description MetricDescription `json:"Description"`
+ StaticLabels map[string]string `json:"StaticLabels"`
+ Value float64 `json:"Value"`
+ VariableLabels map[string]string `json:"VariableLabels"`
+ HistogramBucketLabel string `json:"HistogramBucketLabel"`
+ Histogram map[string]uint64 `json:"Histogram"`
+}
+
+// MetricsGroupV2 are a group of metrics that are initialized together.
+type MetricsGroupV2 struct {
+ metricsCache *cachevalue.Cache[[]MetricV2] `msg:"-"`
+ cacheInterval time.Duration
+ metricsGroupOpts MetricsGroupOpts
+}
+
+// MetricsGroupOpts are a group of metrics opts to be used to initialize the metrics group.
+type MetricsGroupOpts struct {
+ dependGlobalObjectAPI bool
+ dependGlobalAuthNPlugin bool
+ dependGlobalSiteReplicationSys bool
+ dependGlobalNotificationSys bool
+ dependGlobalKMS bool
+ bucketOnly bool
+ dependGlobalLambdaTargetList bool
+ dependGlobalIAMSys bool
+ dependGlobalLockServer bool
+ dependGlobalIsDistErasure bool
+ dependGlobalBackgroundHealState bool
+ dependBucketTargetSys bool
+}
+
+// RegisterRead register the metrics populator function to be used
+// to populate new values upon cache invalidation.
+func (g *MetricsGroupV2) RegisterRead(read func(context.Context) []MetricV2) {
+ g.metricsCache = cachevalue.NewFromFunc(g.cacheInterval,
+ cachevalue.Opts{ReturnLastGood: true},
+ func(ctx context.Context) ([]MetricV2, error) {
+ if g.metricsGroupOpts.dependGlobalObjectAPI {
+ objLayer := newObjectLayerFn()
+ // Service not initialized yet
+ if objLayer == nil {
+ return []MetricV2{}, nil
+ }
+ }
+ if g.metricsGroupOpts.dependGlobalAuthNPlugin {
+ if globalAuthNPlugin == nil {
+ return []MetricV2{}, nil
+ }
+ }
+ if g.metricsGroupOpts.dependGlobalSiteReplicationSys {
+ if !globalSiteReplicationSys.isEnabled() {
+ return []MetricV2{}, nil
+ }
+ }
+ if g.metricsGroupOpts.dependGlobalNotificationSys {
+ if globalNotificationSys == nil {
+ return []MetricV2{}, nil
+ }
+ }
+ if g.metricsGroupOpts.dependGlobalKMS {
+ if GlobalKMS == nil {
+ return []MetricV2{}, nil
+ }
+ }
+ if g.metricsGroupOpts.dependGlobalLambdaTargetList {
+ if globalLambdaTargetList == nil {
+ return []MetricV2{}, nil
+ }
+ }
+ if g.metricsGroupOpts.dependGlobalIAMSys {
+ if globalIAMSys == nil {
+ return []MetricV2{}, nil
+ }
+ }
+ if g.metricsGroupOpts.dependGlobalLockServer {
+ if globalLockServer == nil {
+ return []MetricV2{}, nil
+ }
+ }
+ if g.metricsGroupOpts.dependGlobalIsDistErasure {
+ if !globalIsDistErasure {
+ return []MetricV2{}, nil
+ }
+ }
+ if g.metricsGroupOpts.dependGlobalBackgroundHealState {
+ if globalBackgroundHealState == nil {
+ return []MetricV2{}, nil
+ }
+ }
+ if g.metricsGroupOpts.dependBucketTargetSys {
+ if globalBucketTargetSys == nil {
+ return []MetricV2{}, nil
+ }
+ }
+ return read(GlobalContext), nil
+ },
+ )
+}
+
+func (m *MetricV2) clone() MetricV2 {
+ metric := MetricV2{
+ Description: m.Description,
+ Value: m.Value,
+ HistogramBucketLabel: m.HistogramBucketLabel,
+ StaticLabels: make(map[string]string, len(m.StaticLabels)),
+ VariableLabels: make(map[string]string, len(m.VariableLabels)),
+ Histogram: make(map[string]uint64, len(m.Histogram)),
+ }
+ maps.Copy(metric.StaticLabels, m.StaticLabels)
+ maps.Copy(metric.VariableLabels, m.VariableLabels)
+ maps.Copy(metric.Histogram, m.Histogram)
+ return metric
+}
+
+// Get - returns cached value always upton the configured TTL,
+// once the TTL expires "read()" registered function is called
+// to return the new values and updated.
+func (g *MetricsGroupV2) Get() (metrics []MetricV2) {
+ m, _ := g.metricsCache.Get()
+ if len(m) == 0 {
+ return []MetricV2{}
+ }
+
+ metrics = make([]MetricV2, 0, len(m))
+ for i := range m {
+ metrics = append(metrics, m[i].clone())
+ }
+ return metrics
+}
+
+func getClusterBucketsTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: bucketsSubsystem,
+ Name: total,
+ Help: "Total number of buckets in the cluster",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterCapacityTotalBytesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: capacityRawSubsystem,
+ Name: totalBytes,
+ Help: "Total capacity online in the cluster",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterCapacityFreeBytesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: capacityRawSubsystem,
+ Name: freeBytes,
+ Help: "Total free capacity online in the cluster",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterCapacityUsageBytesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: capacityUsableSubsystem,
+ Name: totalBytes,
+ Help: "Total usable capacity online in the cluster",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterCapacityUsageFreeBytesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: capacityUsableSubsystem,
+ Name: freeBytes,
+ Help: "Total free usable capacity online in the cluster",
+ Type: gaugeMetric,
+ }
+}
+
+func getNodeDriveAPILatencyMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: driveSubsystem,
+ Name: latencyMicroSec,
+ Help: "Average last minute latency in µs for drive API storage operations",
+ Type: gaugeMetric,
+ }
+}
+
+func getNodeDriveUsedBytesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: driveSubsystem,
+ Name: usedBytes,
+ Help: "Total storage used on a drive",
+ Type: gaugeMetric,
+ }
+}
+
+func getNodeDriveTimeoutErrorsMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: driveSubsystem,
+ Name: "errors_timeout",
+ Help: "Total number of drive timeout errors since server uptime",
+ Type: counterMetric,
+ }
+}
+
+func getNodeDriveIOErrorsMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: driveSubsystem,
+ Name: "errors_ioerror",
+ Help: "Total number of drive I/O errors since server uptime",
+ Type: counterMetric,
+ }
+}
+
+func getNodeDriveAvailabilityErrorsMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: driveSubsystem,
+ Name: "errors_availability",
+ Help: "Total number of drive I/O errors, timeouts since server uptime",
+ Type: counterMetric,
+ }
+}
+
+func getNodeDriveWaitingIOMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: driveSubsystem,
+ Name: "io_waiting",
+ Help: "Total number I/O operations waiting on drive",
+ Type: counterMetric,
+ }
+}
+
+func getNodeDriveFreeBytesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: driveSubsystem,
+ Name: freeBytes,
+ Help: "Total storage available on a drive",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterDrivesOfflineTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: driveSubsystem,
+ Name: offlineTotal,
+ Help: "Total drives offline in this cluster",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterDrivesOnlineTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: driveSubsystem,
+ Name: onlineTotal,
+ Help: "Total drives online in this cluster",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterDrivesTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: driveSubsystem,
+ Name: total,
+ Help: "Total drives in this cluster",
+ Type: gaugeMetric,
+ }
+}
+
+func getNodeDrivesOfflineTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: driveSubsystem,
+ Name: offlineTotal,
+ Help: "Total drives offline in this node",
+ Type: gaugeMetric,
+ }
+}
+
+func getNodeDrivesOnlineTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: driveSubsystem,
+ Name: onlineTotal,
+ Help: "Total drives online in this node",
+ Type: gaugeMetric,
+ }
+}
+
+func getNodeDrivesTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: driveSubsystem,
+ Name: total,
+ Help: "Total drives in this node",
+ Type: gaugeMetric,
+ }
+}
+
+func getNodeStandardParityMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: storageClassSubsystem,
+ Name: "standard_parity",
+ Help: "standard storage class parity",
+ Type: gaugeMetric,
+ }
+}
+
+func getNodeRRSParityMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: storageClassSubsystem,
+ Name: "rrs_parity",
+ Help: "reduced redundancy storage class parity",
+ Type: gaugeMetric,
+ }
+}
+
+func getNodeDrivesFreeInodesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: driveSubsystem,
+ Name: freeInodes,
+ Help: "Free inodes on a drive",
+ Type: gaugeMetric,
+ }
+}
+
+func getNodeDriveTotalBytesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: driveSubsystem,
+ Name: totalBytes,
+ Help: "Total storage on a drive",
+ Type: gaugeMetric,
+ }
+}
+
+func getUsageLastScanActivityMD() MetricDescription {
+ return MetricDescription{
+ Namespace: minioMetricNamespace,
+ Subsystem: usageSubsystem,
+ Name: lastActivityTime,
+ Help: "Time elapsed (in nano seconds) since last scan activity",
+ Type: gaugeMetric,
+ }
+}
+
+func getBucketUsageLastScanActivityMD() MetricDescription {
+ return MetricDescription{
+ Namespace: bucketMetricNamespace,
+ Subsystem: usageSubsystem,
+ Name: lastActivityTime,
+ Help: "Time elapsed (in nano seconds) since last scan activity",
+ Type: gaugeMetric,
+ }
+}
+
+func getBucketUsageQuotaTotalBytesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: bucketMetricNamespace,
+ Subsystem: quotaSubsystem,
+ Name: totalBytes,
+ Help: "Total bucket quota size in bytes",
+ Type: gaugeMetric,
+ }
+}
+
+func getBucketTrafficReceivedBytes() MetricDescription {
+ return MetricDescription{
+ Namespace: bucketMetricNamespace,
+ Subsystem: trafficSubsystem,
+ Name: receivedBytes,
+ Help: "Total number of S3 bytes received for this bucket",
+ Type: gaugeMetric,
+ }
+}
+
+func getBucketTrafficSentBytes() MetricDescription {
+ return MetricDescription{
+ Namespace: bucketMetricNamespace,
+ Subsystem: trafficSubsystem,
+ Name: sentBytes,
+ Help: "Total number of S3 bytes sent for this bucket",
+ Type: gaugeMetric,
+ }
+}
+
+func getBucketUsageTotalBytesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: bucketMetricNamespace,
+ Subsystem: usageSubsystem,
+ Name: totalBytes,
+ Help: "Total bucket size in bytes",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterUsageTotalBytesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: usageSubsystem,
+ Name: totalBytes,
+ Help: "Total cluster usage in bytes",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterUsageObjectsTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: usageSubsystem,
+ Name: objectTotal,
+ Help: "Total number of objects in a cluster",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterUsageVersionsTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: usageSubsystem,
+ Name: versionTotal,
+ Help: "Total number of versions (includes delete marker) in a cluster",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterUsageDeleteMarkersTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: usageSubsystem,
+ Name: deleteMarkerTotal,
+ Help: "Total number of delete markers in a cluster",
+ Type: gaugeMetric,
+ }
+}
+
+func getBucketUsageObjectsTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: bucketMetricNamespace,
+ Subsystem: usageSubsystem,
+ Name: objectTotal,
+ Help: "Total number of objects",
+ Type: gaugeMetric,
+ }
+}
+
+func getBucketUsageVersionsTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: bucketMetricNamespace,
+ Subsystem: usageSubsystem,
+ Name: versionTotal,
+ Help: "Total number of versions (includes delete marker)",
+ Type: gaugeMetric,
+ }
+}
+
+func getBucketUsageDeleteMarkersTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: bucketMetricNamespace,
+ Subsystem: usageSubsystem,
+ Name: deleteMarkerTotal,
+ Help: "Total number of delete markers",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterObjectDistributionMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: objectsSubsystem,
+ Name: sizeDistribution,
+ Help: "Distribution of object sizes across a cluster",
+ Type: histogramMetric,
+ }
+}
+
+func getClusterObjectVersionsMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: objectsSubsystem,
+ Name: versionDistribution,
+ Help: "Distribution of object versions across a cluster",
+ Type: histogramMetric,
+ }
+}
+
+func getClusterRepLinkLatencyCurrMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: replicationSubsystem,
+ Name: currLinkLatency,
+ Help: "Replication current link latency in milliseconds",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterRepLinkOnlineMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: replicationSubsystem,
+ Name: linkOnline,
+ Help: "Reports whether replication link is online (1) or offline(0)",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterRepLinkCurrOfflineDurationMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: replicationSubsystem,
+ Name: linkOfflineDuration,
+ Help: "Duration of replication link being offline in seconds since last offline event",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterRepLinkTotalOfflineDurationMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: replicationSubsystem,
+ Name: linkDowntimeTotalDuration,
+ Help: "Total downtime of replication link in seconds since server uptime",
+ Type: gaugeMetric,
+ }
+}
+
+func getBucketRepLatencyMD() MetricDescription {
+ return MetricDescription{
+ Namespace: bucketMetricNamespace,
+ Subsystem: replicationSubsystem,
+ Name: latencyMilliSec,
+ Help: "Replication latency in milliseconds",
+ Type: histogramMetric,
+ }
+}
+
+func getRepFailedBytesLastMinuteMD(namespace MetricNamespace) MetricDescription {
+ return MetricDescription{
+ Namespace: namespace,
+ Subsystem: replicationSubsystem,
+ Name: lastMinFailedBytes,
+ Help: "Total number of bytes failed at least once to replicate in the last full minute",
+ Type: gaugeMetric,
+ }
+}
+
+func getRepFailedOperationsLastMinuteMD(namespace MetricNamespace) MetricDescription {
+ return MetricDescription{
+ Namespace: namespace,
+ Subsystem: replicationSubsystem,
+ Name: lastMinFailedCount,
+ Help: "Total number of objects which failed replication in the last full minute",
+ Type: gaugeMetric,
+ }
+}
+
+func getRepFailedBytesLastHourMD(namespace MetricNamespace) MetricDescription {
+ return MetricDescription{
+ Namespace: namespace,
+ Subsystem: replicationSubsystem,
+ Name: lastHourFailedBytes,
+ Help: "Total number of bytes failed at least once to replicate in the last hour",
+ Type: gaugeMetric,
+ }
+}
+
+func getRepFailedOperationsLastHourMD(namespace MetricNamespace) MetricDescription {
+ return MetricDescription{
+ Namespace: namespace,
+ Subsystem: replicationSubsystem,
+ Name: lastHourFailedCount,
+ Help: "Total number of objects which failed replication in the last hour",
+ Type: gaugeMetric,
+ }
+}
+
+func getRepFailedBytesTotalMD(namespace MetricNamespace) MetricDescription {
+ return MetricDescription{
+ Namespace: namespace,
+ Subsystem: replicationSubsystem,
+ Name: totalFailedBytes,
+ Help: "Total number of bytes failed at least once to replicate since server uptime",
+ Type: counterMetric,
+ }
+}
+
+func getRepFailedOperationsTotalMD(namespace MetricNamespace) MetricDescription {
+ return MetricDescription{
+ Namespace: namespace,
+ Subsystem: replicationSubsystem,
+ Name: totalFailedCount,
+ Help: "Total number of objects which failed replication since server uptime",
+ Type: counterMetric,
+ }
+}
+
+func getRepSentBytesMD(namespace MetricNamespace) MetricDescription {
+ return MetricDescription{
+ Namespace: namespace,
+ Subsystem: replicationSubsystem,
+ Name: sentBytes,
+ Help: "Total number of bytes replicated to the target",
+ Type: counterMetric,
+ }
+}
+
+func getRepSentOperationsMD(namespace MetricNamespace) MetricDescription {
+ return MetricDescription{
+ Namespace: namespace,
+ Subsystem: replicationSubsystem,
+ Name: sentCount,
+ Help: "Total number of objects replicated to the target",
+ Type: gaugeMetric,
+ }
+}
+
+func getRepReceivedBytesMD(namespace MetricNamespace) MetricDescription {
+ helpText := "Total number of bytes replicated to this bucket from another source bucket"
+ if namespace == clusterMetricNamespace {
+ helpText = "Total number of bytes replicated to this cluster from site replication peer"
+ }
+ return MetricDescription{
+ Namespace: namespace,
+ Subsystem: replicationSubsystem,
+ Name: receivedBytes,
+ Help: helpText,
+ Type: counterMetric,
+ }
+}
+
+func getRepReceivedOperationsMD(namespace MetricNamespace) MetricDescription {
+ help := "Total number of objects received by this cluster"
+ if namespace == bucketMetricNamespace {
+ help = "Total number of objects received by this bucket from another source bucket"
+ }
+ return MetricDescription{
+ Namespace: namespace,
+ Subsystem: replicationSubsystem,
+ Name: receivedCount,
+ Help: help,
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterReplMRFFailedOperationsMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: replicationSubsystem,
+ Name: recentBacklogCount,
+ Help: "Total number of objects seen in replication backlog in the last 5 minutes",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterRepCredentialErrorsMD(namespace MetricNamespace) MetricDescription {
+ return MetricDescription{
+ Namespace: namespace,
+ Subsystem: replicationSubsystem,
+ Name: credentialErrors,
+ Help: "Total number of replication credential errors since server uptime",
+ Type: counterMetric,
+ }
+}
+
+func getClusterReplCurrQueuedOperationsMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: replicationSubsystem,
+ Name: currInQueueCount,
+ Help: "Total number of objects queued for replication in the last full minute",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterReplCurrQueuedBytesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: replicationSubsystem,
+ Name: currInQueueBytes,
+ Help: "Total number of bytes queued for replication in the last full minute",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterReplActiveWorkersCountMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: replicationSubsystem,
+ Name: currActiveWorkers,
+ Help: "Total number of active replication workers",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterReplAvgActiveWorkersCountMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: replicationSubsystem,
+ Name: avgActiveWorkers,
+ Help: "Average number of active replication workers",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterReplMaxActiveWorkersCountMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: replicationSubsystem,
+ Name: maxActiveWorkers,
+ Help: "Maximum number of active replication workers seen since server uptime",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterReplCurrentTransferRateMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: replicationSubsystem,
+ Name: currTransferRate,
+ Help: "Current replication transfer rate in bytes/sec",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterRepLinkLatencyMaxMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: replicationSubsystem,
+ Name: maxLinkLatency,
+ Help: "Maximum replication link latency in milliseconds seen since server uptime",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterRepLinkLatencyAvgMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: replicationSubsystem,
+ Name: avgLinkLatency,
+ Help: "Average replication link latency in milliseconds",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterReplAvgQueuedOperationsMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: replicationSubsystem,
+ Name: avgInQueueCount,
+ Help: "Average number of objects queued for replication since server uptime",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterReplAvgQueuedBytesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: replicationSubsystem,
+ Name: avgInQueueBytes,
+ Help: "Average number of bytes queued for replication since server uptime",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterReplMaxQueuedOperationsMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: replicationSubsystem,
+ Name: maxInQueueCount,
+ Help: "Maximum number of objects queued for replication since server uptime",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterReplMaxQueuedBytesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: replicationSubsystem,
+ Name: maxInQueueBytes,
+ Help: "Maximum number of bytes queued for replication since server uptime",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterReplAvgTransferRateMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: replicationSubsystem,
+ Name: avgTransferRate,
+ Help: "Average replication transfer rate in bytes/sec",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterReplMaxTransferRateMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: replicationSubsystem,
+ Name: maxTransferRate,
+ Help: "Maximum replication transfer rate in bytes/sec seen since server uptime",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterReplProxiedGetOperationsMD(ns MetricNamespace) MetricDescription {
+ return MetricDescription{
+ Namespace: ns,
+ Subsystem: replicationSubsystem,
+ Name: proxiedGetRequestsTotal,
+ Help: "Number of GET requests proxied to replication target",
+ Type: counterMetric,
+ }
+}
+
+func getClusterReplProxiedHeadOperationsMD(ns MetricNamespace) MetricDescription {
+ return MetricDescription{
+ Namespace: ns,
+ Subsystem: replicationSubsystem,
+ Name: proxiedHeadRequestsTotal,
+ Help: "Number of HEAD requests proxied to replication target",
+ Type: counterMetric,
+ }
+}
+
+func getClusterReplProxiedPutTaggingOperationsMD(ns MetricNamespace) MetricDescription {
+ return MetricDescription{
+ Namespace: ns,
+ Subsystem: replicationSubsystem,
+ Name: proxiedPutTaggingRequestsTotal,
+ Help: "Number of PUT tagging requests proxied to replication target",
+ Type: counterMetric,
+ }
+}
+
+func getClusterReplProxiedGetTaggingOperationsMD(ns MetricNamespace) MetricDescription {
+ return MetricDescription{
+ Namespace: ns,
+ Subsystem: replicationSubsystem,
+ Name: proxiedGetTaggingRequestsTotal,
+ Help: "Number of GET tagging requests proxied to replication target",
+ Type: counterMetric,
+ }
+}
+
+func getClusterReplProxiedRmvTaggingOperationsMD(ns MetricNamespace) MetricDescription {
+ return MetricDescription{
+ Namespace: ns,
+ Subsystem: replicationSubsystem,
+ Name: proxiedDeleteTaggingRequestsTotal,
+ Help: "Number of DELETE tagging requests proxied to replication target",
+ Type: counterMetric,
+ }
+}
+
+func getClusterReplProxiedGetFailedOperationsMD(ns MetricNamespace) MetricDescription {
+ return MetricDescription{
+ Namespace: ns,
+ Subsystem: replicationSubsystem,
+ Name: proxiedGetRequestsFailures,
+ Help: "Number of failures in GET requests proxied to replication target",
+ Type: counterMetric,
+ }
+}
+
+func getClusterReplProxiedHeadFailedOperationsMD(ns MetricNamespace) MetricDescription {
+ return MetricDescription{
+ Namespace: ns,
+ Subsystem: replicationSubsystem,
+ Name: proxiedHeadRequestsFailures,
+ Help: "Number of failures in HEAD requests proxied to replication target",
+ Type: counterMetric,
+ }
+}
+
+func getClusterReplProxiedPutTaggingFailedOperationsMD(ns MetricNamespace) MetricDescription {
+ return MetricDescription{
+ Namespace: ns,
+ Subsystem: replicationSubsystem,
+ Name: proxiedPutTaggingRequestFailures,
+ Help: "Number of failures in PUT tagging proxy requests to replication target",
+ Type: counterMetric,
+ }
+}
+
+func getClusterReplProxiedGetTaggingFailedOperationsMD(ns MetricNamespace) MetricDescription {
+ return MetricDescription{
+ Namespace: ns,
+ Subsystem: replicationSubsystem,
+ Name: proxiedGetTaggingRequestFailures,
+ Help: "Number of failures in GET tagging proxy requests to replication target",
+ Type: counterMetric,
+ }
+}
+
+func getClusterReplProxiedRmvTaggingFailedOperationsMD(ns MetricNamespace) MetricDescription {
+ return MetricDescription{
+ Namespace: ns,
+ Subsystem: replicationSubsystem,
+ Name: proxiedDeleteTaggingRequestFailures,
+ Help: "Number of failures in DELETE tagging proxy requests to replication target",
+ Type: counterMetric,
+ }
+}
+
+func getBucketObjectDistributionMD() MetricDescription {
+ return MetricDescription{
+ Namespace: bucketMetricNamespace,
+ Subsystem: objectsSubsystem,
+ Name: sizeDistribution,
+ Help: "Distribution of object sizes in the bucket, includes label for the bucket name",
+ Type: histogramMetric,
+ }
+}
+
+func getBucketObjectVersionsMD() MetricDescription {
+ return MetricDescription{
+ Namespace: bucketMetricNamespace,
+ Subsystem: objectsSubsystem,
+ Name: versionDistribution,
+ Help: "Distribution of object sizes in the bucket, includes label for the bucket name",
+ Type: histogramMetric,
+ }
+}
+
+func getInternodeFailedRequests() MetricDescription {
+ return MetricDescription{
+ Namespace: interNodeMetricNamespace,
+ Subsystem: trafficSubsystem,
+ Name: errorsTotal,
+ Help: "Total number of failed internode calls",
+ Type: counterMetric,
+ }
+}
+
+func getInternodeTCPDialTimeout() MetricDescription {
+ return MetricDescription{
+ Namespace: interNodeMetricNamespace,
+ Subsystem: trafficSubsystem,
+ Name: "dial_errors",
+ Help: "Total number of internode TCP dial timeouts and errors",
+ Type: counterMetric,
+ }
+}
+
+func getInternodeTCPAvgDuration() MetricDescription {
+ return MetricDescription{
+ Namespace: interNodeMetricNamespace,
+ Subsystem: trafficSubsystem,
+ Name: "dial_avg_time",
+ Help: "Average time of internodes TCP dial calls",
+ Type: gaugeMetric,
+ }
+}
+
+func getInterNodeSentBytesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: interNodeMetricNamespace,
+ Subsystem: trafficSubsystem,
+ Name: sentBytes,
+ Help: "Total number of bytes sent to the other peer nodes",
+ Type: counterMetric,
+ }
+}
+
+func getInterNodeReceivedBytesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: interNodeMetricNamespace,
+ Subsystem: trafficSubsystem,
+ Name: receivedBytes,
+ Help: "Total number of bytes received from other peer nodes",
+ Type: counterMetric,
+ }
+}
+
+func getS3SentBytesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: s3MetricNamespace,
+ Subsystem: trafficSubsystem,
+ Name: sentBytes,
+ Help: "Total number of s3 bytes sent",
+ Type: counterMetric,
+ }
+}
+
+func getS3ReceivedBytesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: s3MetricNamespace,
+ Subsystem: trafficSubsystem,
+ Name: receivedBytes,
+ Help: "Total number of s3 bytes received",
+ Type: counterMetric,
+ }
+}
+
+func getS3RequestsInFlightMD() MetricDescription {
+ return MetricDescription{
+ Namespace: s3MetricNamespace,
+ Subsystem: requestsSubsystem,
+ Name: inflightTotal,
+ Help: "Total number of S3 requests currently in flight",
+ Type: gaugeMetric,
+ }
+}
+
+func getS3RequestsInQueueMD() MetricDescription {
+ return MetricDescription{
+ Namespace: s3MetricNamespace,
+ Subsystem: requestsSubsystem,
+ Name: waitingTotal,
+ Help: "Total number of S3 requests in the waiting queue",
+ Type: gaugeMetric,
+ }
+}
+
+func getIncomingS3RequestsMD() MetricDescription {
+ return MetricDescription{
+ Namespace: s3MetricNamespace,
+ Subsystem: requestsSubsystem,
+ Name: incomingTotal,
+ Help: "Total number of incoming S3 requests",
+ Type: gaugeMetric,
+ }
+}
+
+func getS3RequestsTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: s3MetricNamespace,
+ Subsystem: requestsSubsystem,
+ Name: total,
+ Help: "Total number of S3 requests",
+ Type: counterMetric,
+ }
+}
+
+func getS3RequestsErrorsMD() MetricDescription {
+ return MetricDescription{
+ Namespace: s3MetricNamespace,
+ Subsystem: requestsSubsystem,
+ Name: errorsTotal,
+ Help: "Total number of S3 requests with (4xx and 5xx) errors",
+ Type: counterMetric,
+ }
+}
+
+func getS3Requests4xxErrorsMD() MetricDescription {
+ return MetricDescription{
+ Namespace: s3MetricNamespace,
+ Subsystem: requestsSubsystem,
+ Name: "4xx_" + errorsTotal,
+ Help: "Total number of S3 requests with (4xx) errors",
+ Type: counterMetric,
+ }
+}
+
+func getS3Requests5xxErrorsMD() MetricDescription {
+ return MetricDescription{
+ Namespace: s3MetricNamespace,
+ Subsystem: requestsSubsystem,
+ Name: "5xx_" + errorsTotal,
+ Help: "Total number of S3 requests with (5xx) errors",
+ Type: counterMetric,
+ }
+}
+
+func getS3RequestsCanceledMD() MetricDescription {
+ return MetricDescription{
+ Namespace: s3MetricNamespace,
+ Subsystem: requestsSubsystem,
+ Name: canceledTotal,
+ Help: "Total number of S3 requests that were canceled by the client",
+ Type: counterMetric,
+ }
+}
+
+func getS3RejectedAuthRequestsTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: s3MetricNamespace,
+ Subsystem: requestsRejectedSubsystem,
+ Name: authTotal,
+ Help: "Total number of S3 requests rejected for auth failure",
+ Type: counterMetric,
+ }
+}
+
+func getS3RejectedHeaderRequestsTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: s3MetricNamespace,
+ Subsystem: requestsRejectedSubsystem,
+ Name: headerTotal,
+ Help: "Total number of S3 requests rejected for invalid header",
+ Type: counterMetric,
+ }
+}
+
+func getS3RejectedTimestampRequestsTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: s3MetricNamespace,
+ Subsystem: requestsRejectedSubsystem,
+ Name: timestampTotal,
+ Help: "Total number of S3 requests rejected for invalid timestamp",
+ Type: counterMetric,
+ }
+}
+
+func getS3RejectedInvalidRequestsTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: s3MetricNamespace,
+ Subsystem: requestsRejectedSubsystem,
+ Name: invalidTotal,
+ Help: "Total number of invalid S3 requests",
+ Type: counterMetric,
+ }
+}
+
+func getHealObjectsTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: healMetricNamespace,
+ Subsystem: objectsSubsystem,
+ Name: total,
+ Help: "Objects scanned since server uptime",
+ Type: counterMetric,
+ }
+}
+
+func getHealObjectsHealTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: healMetricNamespace,
+ Subsystem: objectsSubsystem,
+ Name: healTotal,
+ Help: "Objects healed since server uptime",
+ Type: counterMetric,
+ }
+}
+
+func getHealObjectsFailTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: healMetricNamespace,
+ Subsystem: objectsSubsystem,
+ Name: errorsTotal,
+ Help: "Objects with healing failed since server uptime",
+ Type: counterMetric,
+ }
+}
+
+func getHealLastActivityTimeMD() MetricDescription {
+ return MetricDescription{
+ Namespace: healMetricNamespace,
+ Subsystem: timeSubsystem,
+ Name: lastActivityTime,
+ Help: "Time elapsed (in nano seconds) since last self healing activity",
+ Type: gaugeMetric,
+ }
+}
+
+func getNodeOnlineTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: nodesSubsystem,
+ Name: onlineTotal,
+ Help: "Total number of MinIO nodes online",
+ Type: gaugeMetric,
+ }
+}
+
+func getNodeOfflineTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: nodesSubsystem,
+ Name: offlineTotal,
+ Help: "Total number of MinIO nodes offline",
+ Type: gaugeMetric,
+ }
+}
+
+func getMinIOVersionMD() MetricDescription {
+ return MetricDescription{
+ Namespace: minioMetricNamespace,
+ Subsystem: softwareSubsystem,
+ Name: versionInfo,
+ Help: "MinIO Release tag for the server",
+ Type: gaugeMetric,
+ }
+}
+
+func getMinIOCommitMD() MetricDescription {
+ return MetricDescription{
+ Namespace: minioMetricNamespace,
+ Subsystem: softwareSubsystem,
+ Name: commitInfo,
+ Help: "Git commit hash for the MinIO release",
+ Type: gaugeMetric,
+ }
+}
+
+func getS3TTFBDistributionMD() MetricDescription {
+ return MetricDescription{
+ Namespace: s3MetricNamespace,
+ Subsystem: ttfbSubsystem,
+ Name: ttfbDistribution,
+ Help: "Distribution of time to first byte across API calls",
+ Type: gaugeMetric,
+ }
+}
+
+func getBucketTTFBDistributionMD() MetricDescription {
+ return MetricDescription{
+ Namespace: bucketMetricNamespace,
+ Subsystem: ttfbSubsystem,
+ Name: ttfbDistribution,
+ Help: "Distribution of time to first byte across API calls per bucket",
+ Type: gaugeMetric,
+ }
+}
+
+func getMinioFDOpenMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: fileDescriptorSubsystem,
+ Name: openTotal,
+ Help: "Total number of open file descriptors by the MinIO Server process",
+ Type: gaugeMetric,
+ }
+}
+
+func getMinioFDLimitMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: fileDescriptorSubsystem,
+ Name: limitTotal,
+ Help: "Limit on total number of open file descriptors for the MinIO Server process",
+ Type: gaugeMetric,
+ }
+}
+
+func getMinioProcessIOWriteBytesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: ioSubsystem,
+ Name: writeBytes,
+ Help: "Total bytes written by the process to the underlying storage system, /proc/[pid]/io write_bytes",
+ Type: counterMetric,
+ }
+}
+
+func getMinioProcessIOReadBytesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: ioSubsystem,
+ Name: readBytes,
+ Help: "Total bytes read by the process from the underlying storage system, /proc/[pid]/io read_bytes",
+ Type: counterMetric,
+ }
+}
+
+func getMinioProcessIOWriteCachedBytesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: ioSubsystem,
+ Name: wcharBytes,
+ Help: "Total bytes written by the process to the underlying storage system including page cache, /proc/[pid]/io wchar",
+ Type: counterMetric,
+ }
+}
+
+func getMinioProcessIOReadCachedBytesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: ioSubsystem,
+ Name: rcharBytes,
+ Help: "Total bytes read by the process from the underlying storage system including cache, /proc/[pid]/io rchar",
+ Type: counterMetric,
+ }
+}
+
+func getMinIOProcessSysCallRMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: sysCallSubsystem,
+ Name: readTotal,
+ Help: "Total read SysCalls to the kernel. /proc/[pid]/io syscr",
+ Type: counterMetric,
+ }
+}
+
+func getMinIOProcessSysCallWMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: sysCallSubsystem,
+ Name: writeTotal,
+ Help: "Total write SysCalls to the kernel. /proc/[pid]/io syscw",
+ Type: counterMetric,
+ }
+}
+
+func getMinIOGORoutineCountMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: goRoutines,
+ Name: total,
+ Help: "Total number of go routines running",
+ Type: gaugeMetric,
+ }
+}
+
+func getMinIOProcessStartTimeMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: processSubsystem,
+ Name: startTime,
+ Help: "Start time for MinIO process per node, time in seconds since Unix epoc",
+ Type: gaugeMetric,
+ }
+}
+
+func getMinIOProcessUptimeMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: processSubsystem,
+ Name: upTime,
+ Help: "Uptime for MinIO process per node in seconds",
+ Type: gaugeMetric,
+ }
+}
+
+func getMinIOProcessResidentMemory() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: processSubsystem,
+ Name: memory,
+ Help: "Resident memory size in bytes",
+ Type: gaugeMetric,
+ }
+}
+
+func getMinIOProcessVirtualMemory() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: processSubsystem,
+ Name: memory,
+ Help: "Virtual memory size in bytes",
+ Type: gaugeMetric,
+ }
+}
+
+func getMinIOProcessCPUTime() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: processSubsystem,
+ Name: cpu,
+ Help: "Total user and system CPU time spent in seconds",
+ Type: counterMetric,
+ }
+}
+
+func getMinioProcMetrics() *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 10 * time.Second,
+ }
+ mg.RegisterRead(func(ctx context.Context) (metrics []MetricV2) {
+ if runtime.GOOS == globalWindowsOSName || runtime.GOOS == globalMacOSName {
+ return nil
+ }
+
+ p, err := procfs.Self()
+ if err != nil {
+ internalLogOnceIf(ctx, err, string(nodeMetricNamespace))
+ return metrics
+ }
+
+ openFDs, _ := p.FileDescriptorsLen()
+ l, _ := p.Limits()
+ io, _ := p.IO()
+ stat, _ := p.Stat()
+ startTime, _ := stat.StartTime()
+
+ metrics = make([]MetricV2, 0, 20)
+
+ if openFDs > 0 {
+ metrics = append(metrics,
+ MetricV2{
+ Description: getMinioFDOpenMD(),
+ Value: float64(openFDs),
+ },
+ )
+ }
+
+ if l.OpenFiles > 0 {
+ metrics = append(metrics,
+ MetricV2{
+ Description: getMinioFDLimitMD(),
+ Value: float64(l.OpenFiles),
+ })
+ }
+
+ if io.SyscR > 0 {
+ metrics = append(metrics,
+ MetricV2{
+ Description: getMinIOProcessSysCallRMD(),
+ Value: float64(io.SyscR),
+ })
+ }
+
+ if io.SyscW > 0 {
+ metrics = append(metrics,
+ MetricV2{
+ Description: getMinIOProcessSysCallWMD(),
+ Value: float64(io.SyscW),
+ })
+ }
+
+ if io.ReadBytes > 0 {
+ metrics = append(metrics,
+ MetricV2{
+ Description: getMinioProcessIOReadBytesMD(),
+ Value: float64(io.ReadBytes),
+ })
+ }
+
+ if io.WriteBytes > 0 {
+ metrics = append(metrics,
+ MetricV2{
+ Description: getMinioProcessIOWriteBytesMD(),
+ Value: float64(io.WriteBytes),
+ })
+ }
+
+ if io.RChar > 0 {
+ metrics = append(metrics,
+ MetricV2{
+ Description: getMinioProcessIOReadCachedBytesMD(),
+ Value: float64(io.RChar),
+ })
+ }
+
+ if io.WChar > 0 {
+ metrics = append(metrics,
+ MetricV2{
+ Description: getMinioProcessIOWriteCachedBytesMD(),
+ Value: float64(io.WChar),
+ })
+ }
+
+ if startTime > 0 {
+ metrics = append(metrics,
+ MetricV2{
+ Description: getMinIOProcessStartTimeMD(),
+ Value: startTime,
+ })
+ }
+
+ if !globalBootTime.IsZero() {
+ metrics = append(metrics,
+ MetricV2{
+ Description: getMinIOProcessUptimeMD(),
+ Value: time.Since(globalBootTime).Seconds(),
+ })
+ }
+
+ if stat.ResidentMemory() > 0 {
+ metrics = append(metrics,
+ MetricV2{
+ Description: getMinIOProcessResidentMemory(),
+ Value: float64(stat.ResidentMemory()),
+ })
+ }
+
+ if stat.VirtualMemory() > 0 {
+ metrics = append(metrics,
+ MetricV2{
+ Description: getMinIOProcessVirtualMemory(),
+ Value: float64(stat.VirtualMemory()),
+ })
+ }
+
+ if stat.CPUTime() > 0 {
+ metrics = append(metrics,
+ MetricV2{
+ Description: getMinIOProcessCPUTime(),
+ Value: stat.CPUTime(),
+ })
+ }
+ return metrics
+ })
+ return mg
+}
+
+func getGoMetrics() *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 10 * time.Second,
+ }
+ mg.RegisterRead(func(ctx context.Context) (metrics []MetricV2) {
+ metrics = append(metrics, MetricV2{
+ Description: getMinIOGORoutineCountMD(),
+ Value: float64(runtime.NumGoroutine()),
+ })
+ return metrics
+ })
+ return mg
+}
+
+// getHistogramMetrics fetches histogram metrics and returns it in a []Metric
+// Note: Typically used in MetricGroup.RegisterRead
+//
+// The toLowerAPILabels parameter is added for compatibility,
+// if set, it lowercases the `api` label values.
+func getHistogramMetrics(hist *prometheus.HistogramVec, desc MetricDescription, toLowerAPILabels, limitBuckets bool) []MetricV2 {
+ ch := make(chan prometheus.Metric)
+ go func() {
+ defer xioutil.SafeClose(ch)
+ // Collects prometheus metrics from hist and sends it over ch
+ hist.Collect(ch)
+ }()
+
+ // Converts metrics received into internal []Metric type
+ var metrics []MetricV2
+ buckets := make(map[string][]MetricV2, v2MetricsMaxBuckets)
+ for promMetric := range ch {
+ dtoMetric := &dto.Metric{}
+ err := promMetric.Write(dtoMetric)
+ if err != nil {
+ // Log error and continue to receive other metric
+ // values
+ bugLogIf(GlobalContext, err)
+ continue
+ }
+
+ h := dtoMetric.GetHistogram()
+ for _, b := range h.Bucket {
+ labels := make(map[string]string)
+ for _, lp := range dtoMetric.GetLabel() {
+ if *lp.Name == "api" && toLowerAPILabels {
+ labels[*lp.Name] = strings.ToLower(*lp.Value)
+ } else {
+ labels[*lp.Name] = *lp.Value
+ }
+ }
+ labels["le"] = fmt.Sprintf("%.3f", *b.UpperBound)
+ metric := MetricV2{
+ Description: desc,
+ VariableLabels: labels,
+ Value: float64(b.GetCumulativeCount()),
+ }
+ if limitBuckets && labels["bucket"] != "" {
+ buckets[labels["bucket"]] = append(buckets[labels["bucket"]], metric)
+ } else {
+ metrics = append(metrics, metric)
+ }
+ }
+ // add metrics with +Inf label
+ labels1 := make(map[string]string)
+ for _, lp := range dtoMetric.GetLabel() {
+ if *lp.Name == "api" && toLowerAPILabels {
+ labels1[*lp.Name] = strings.ToLower(*lp.Value)
+ } else {
+ labels1[*lp.Name] = *lp.Value
+ }
+ }
+ labels1["le"] = fmt.Sprintf("%.3f", math.Inf(+1))
+
+ metric := MetricV2{
+ Description: desc,
+ VariableLabels: labels1,
+ Value: float64(dtoMetric.Histogram.GetSampleCount()),
+ }
+ if limitBuckets && labels1["bucket"] != "" {
+ buckets[labels1["bucket"]] = append(buckets[labels1["bucket"]], metric)
+ } else {
+ metrics = append(metrics, metric)
+ }
+ }
+
+ // Limit bucket metrics...
+ if limitBuckets {
+ bucketNames := mapKeysSorted(buckets)
+ bucketNames = bucketNames[:min(len(buckets), v2MetricsMaxBuckets)]
+ for _, b := range bucketNames {
+ metrics = append(metrics, buckets[b]...)
+ }
+ }
+ return metrics
+}
+
+func getBucketTTFBMetric() *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 10 * time.Second,
+ }
+ mg.RegisterRead(func(ctx context.Context) []MetricV2 {
+ return getHistogramMetrics(bucketHTTPRequestsDuration,
+ getBucketTTFBDistributionMD(), true, true)
+ })
+ return mg
+}
+
+func getS3TTFBMetric() *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 10 * time.Second,
+ }
+ mg.RegisterRead(func(ctx context.Context) []MetricV2 {
+ return getHistogramMetrics(httpRequestsDuration,
+ getS3TTFBDistributionMD(), true, true)
+ })
+ return mg
+}
+
+func getTierMetrics() *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 10 * time.Second,
+ }
+ mg.RegisterRead(func(ctx context.Context) []MetricV2 {
+ return globalTierMetrics.Report()
+ })
+ return mg
+}
+
+func getTransitionPendingTasksMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: ilmSubsystem,
+ Name: transitionPendingTasks,
+ Help: "Number of pending ILM transition tasks in the queue",
+ Type: gaugeMetric,
+ }
+}
+
+func getTransitionActiveTasksMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: ilmSubsystem,
+ Name: transitionActiveTasks,
+ Help: "Number of active ILM transition tasks",
+ Type: gaugeMetric,
+ }
+}
+
+func getTransitionMissedTasksMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: ilmSubsystem,
+ Name: transitionMissedTasks,
+ Help: "Number of missed immediate ILM transition tasks",
+ Type: gaugeMetric,
+ }
+}
+
+func getExpiryPendingTasksMD() MetricDescription {
+ return MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: ilmSubsystem,
+ Name: expiryPendingTasks,
+ Help: "Number of pending ILM expiry tasks in the queue",
+ Type: gaugeMetric,
+ }
+}
+
+func getBucketS3RequestsInFlightMD() MetricDescription {
+ return MetricDescription{
+ Namespace: bucketMetricNamespace,
+ Subsystem: requestsSubsystem,
+ Name: inflightTotal,
+ Help: "Total number of S3 requests currently in flight on a bucket",
+ Type: gaugeMetric,
+ }
+}
+
+func getBucketS3RequestsTotalMD() MetricDescription {
+ return MetricDescription{
+ Namespace: bucketMetricNamespace,
+ Subsystem: requestsSubsystem,
+ Name: total,
+ Help: "Total number of S3 requests on a bucket",
+ Type: counterMetric,
+ }
+}
+
+func getBucketS3Requests4xxErrorsMD() MetricDescription {
+ return MetricDescription{
+ Namespace: bucketMetricNamespace,
+ Subsystem: requestsSubsystem,
+ Name: "4xx_" + errorsTotal,
+ Help: "Total number of S3 requests with (4xx) errors on a bucket",
+ Type: counterMetric,
+ }
+}
+
+func getBucketS3Requests5xxErrorsMD() MetricDescription {
+ return MetricDescription{
+ Namespace: bucketMetricNamespace,
+ Subsystem: requestsSubsystem,
+ Name: "5xx_" + errorsTotal,
+ Help: "Total number of S3 requests with (5xx) errors on a bucket",
+ Type: counterMetric,
+ }
+}
+
+func getBucketS3RequestsCanceledMD() MetricDescription {
+ return MetricDescription{
+ Namespace: bucketMetricNamespace,
+ Subsystem: requestsSubsystem,
+ Name: canceledTotal,
+ Help: "Total number of S3 requests that were canceled from the client while processing on a bucket",
+ Type: counterMetric,
+ }
+}
+
+func getILMNodeMetrics() *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 10 * time.Second,
+ }
+ mg.RegisterRead(func(_ context.Context) []MetricV2 {
+ expPendingTasks := MetricV2{
+ Description: getExpiryPendingTasksMD(),
+ }
+ expMissedTasks := MetricV2{
+ Description: MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: ilmSubsystem,
+ Name: expiryMissedTasks,
+ Help: "Number of object version expiry missed due to busy system",
+ Type: counterMetric,
+ },
+ }
+ expMissedFreeVersions := MetricV2{
+ Description: MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: ilmSubsystem,
+ Name: expiryMissedFreeVersions,
+ Help: "Number of free versions expiry missed due to busy system",
+ Type: counterMetric,
+ },
+ }
+ expMissedTierJournalTasks := MetricV2{
+ Description: MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: ilmSubsystem,
+ Name: expiryMissedTierJournalTasks,
+ Help: "Number of tier journal entries cleanup missed due to busy system",
+ Type: counterMetric,
+ },
+ }
+ expNumWorkers := MetricV2{
+ Description: MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: ilmSubsystem,
+ Name: expiryNumWorkers,
+ Help: "Number of workers expiring object versions currently",
+ Type: gaugeMetric,
+ },
+ }
+ trPendingTasks := MetricV2{
+ Description: getTransitionPendingTasksMD(),
+ }
+ trActiveTasks := MetricV2{
+ Description: getTransitionActiveTasksMD(),
+ }
+ trMissedTasks := MetricV2{
+ Description: getTransitionMissedTasksMD(),
+ }
+ if globalExpiryState != nil {
+ expPendingTasks.Value = float64(globalExpiryState.PendingTasks())
+ expMissedTasks.Value = float64(globalExpiryState.stats.MissedTasks())
+ expMissedFreeVersions.Value = float64(globalExpiryState.stats.MissedFreeVersTasks())
+ expMissedTierJournalTasks.Value = float64(globalExpiryState.stats.MissedTierJournalTasks())
+ expNumWorkers.Value = float64(globalExpiryState.stats.NumWorkers())
+ }
+ if globalTransitionState != nil {
+ trPendingTasks.Value = float64(globalTransitionState.PendingTasks())
+ trActiveTasks.Value = float64(globalTransitionState.ActiveTasks())
+ trMissedTasks.Value = float64(globalTransitionState.MissedImmediateTasks())
+ }
+ return []MetricV2{
+ expPendingTasks,
+ expMissedTasks,
+ expMissedFreeVersions,
+ expMissedTierJournalTasks,
+ expNumWorkers,
+ trPendingTasks,
+ trActiveTasks,
+ trMissedTasks,
+ }
+ })
+ return mg
+}
+
+func getScannerNodeMetrics() *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 10 * time.Second,
+ }
+ mg.RegisterRead(func(_ context.Context) []MetricV2 {
+ metrics := []MetricV2{
+ {
+ Description: MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: scannerSubsystem,
+ Name: "objects_scanned",
+ Help: "Total number of unique objects scanned since server uptime",
+ Type: counterMetric,
+ },
+ Value: float64(globalScannerMetrics.lifetime(scannerMetricScanObject)),
+ },
+ {
+ Description: MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: scannerSubsystem,
+ Name: "versions_scanned",
+ Help: "Total number of object versions scanned since server uptime",
+ Type: counterMetric,
+ },
+ Value: float64(globalScannerMetrics.lifetime(scannerMetricApplyVersion)),
+ },
+ {
+ Description: MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: scannerSubsystem,
+ Name: "directories_scanned",
+ Help: "Total number of directories scanned since server uptime",
+ Type: counterMetric,
+ },
+ Value: float64(globalScannerMetrics.lifetime(scannerMetricScanFolder)),
+ },
+ {
+ Description: MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: scannerSubsystem,
+ Name: "bucket_scans_started",
+ Help: "Total number of bucket scans started since server uptime",
+ Type: counterMetric,
+ },
+ Value: float64(globalScannerMetrics.lifetime(scannerMetricScanBucketDrive) + uint64(globalScannerMetrics.activeDrives())),
+ },
+ {
+ Description: MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: scannerSubsystem,
+ Name: "bucket_scans_finished",
+ Help: "Total number of bucket scans finished since server uptime",
+ Type: counterMetric,
+ },
+ Value: float64(globalScannerMetrics.lifetime(scannerMetricScanBucketDrive)),
+ },
+ {
+ Description: MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: ilmSubsystem,
+ Name: "versions_scanned",
+ Help: "Total number of object versions checked for ilm actions since server uptime",
+ Type: counterMetric,
+ },
+ Value: float64(globalScannerMetrics.lifetime(scannerMetricILM)),
+ },
+ }
+ for i := range globalScannerMetrics.actions {
+ action := lifecycle.Action(i)
+ v := globalScannerMetrics.lifetimeActions(action)
+ if v == 0 {
+ continue
+ }
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: ilmSubsystem,
+ Name: MetricName("action_count_" + toSnake(action.String())),
+ Help: "Total action outcome of lifecycle checks since server uptime",
+ Type: counterMetric,
+ },
+ Value: float64(v),
+ })
+ }
+ return metrics
+ })
+ return mg
+}
+
+func getIAMNodeMetrics(opts MetricsGroupOpts) *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 10 * time.Second,
+ metricsGroupOpts: opts,
+ }
+ mg.RegisterRead(func(_ context.Context) (metrics []MetricV2) {
+ lastSyncTime := atomic.LoadUint64(&globalIAMSys.LastRefreshTimeUnixNano)
+ var sinceLastSyncMillis uint64
+ if lastSyncTime != 0 {
+ sinceLastSyncMillis = (uint64(time.Now().UnixNano()) - lastSyncTime) / uint64(time.Millisecond)
+ }
+
+ pluginAuthNMetrics := globalAuthNPlugin.Metrics()
+ metrics = []MetricV2{
+ {
+ Description: MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: iamSubsystem,
+ Name: "last_sync_duration_millis",
+ Help: "Last successful IAM data sync duration in milliseconds",
+ Type: gaugeMetric,
+ },
+ Value: float64(atomic.LoadUint64(&globalIAMSys.LastRefreshDurationMilliseconds)),
+ },
+ {
+ Description: MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: iamSubsystem,
+ Name: "since_last_sync_millis",
+ Help: "Time (in milliseconds) since last successful IAM data sync",
+ Type: gaugeMetric,
+ },
+ Value: float64(sinceLastSyncMillis),
+ },
+ {
+ Description: MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: iamSubsystem,
+ Name: "sync_successes",
+ Help: "Number of successful IAM data syncs since server uptime",
+ Type: counterMetric,
+ },
+ Value: float64(atomic.LoadUint64(&globalIAMSys.TotalRefreshSuccesses)),
+ },
+ {
+ Description: MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: iamSubsystem,
+ Name: "sync_failures",
+ Help: "Number of failed IAM data syncs since server uptime",
+ Type: counterMetric,
+ },
+ Value: float64(atomic.LoadUint64(&globalIAMSys.TotalRefreshFailures)),
+ },
+ {
+ Description: MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: iamSubsystem,
+ Name: "plugin_authn_service_last_succ_seconds",
+ Help: "When plugin authentication is configured, returns time (in seconds) since the last successful request to the service",
+ Type: gaugeMetric,
+ },
+ Value: pluginAuthNMetrics.LastReachableSecs,
+ },
+ {
+ Description: MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: iamSubsystem,
+ Name: "plugin_authn_service_last_fail_seconds",
+ Help: "When plugin authentication is configured, returns time (in seconds) since the last failed request to the service",
+ Type: gaugeMetric,
+ },
+ Value: pluginAuthNMetrics.LastUnreachableSecs,
+ },
+ {
+ Description: MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: iamSubsystem,
+ Name: "plugin_authn_service_total_requests_minute",
+ Help: "When plugin authentication is configured, returns total requests count in the last full minute",
+ Type: gaugeMetric,
+ },
+ Value: float64(pluginAuthNMetrics.TotalRequests),
+ },
+ {
+ Description: MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: iamSubsystem,
+ Name: "plugin_authn_service_failed_requests_minute",
+ Help: "When plugin authentication is configured, returns failed requests count in the last full minute",
+ Type: gaugeMetric,
+ },
+ Value: float64(pluginAuthNMetrics.FailedRequests),
+ },
+ {
+ Description: MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: iamSubsystem,
+ Name: "plugin_authn_service_succ_avg_rtt_ms_minute",
+ Help: "When plugin authentication is configured, returns average round-trip-time of successful requests in the last full minute",
+ Type: gaugeMetric,
+ },
+ Value: pluginAuthNMetrics.AvgSuccRTTMs,
+ },
+ {
+ Description: MetricDescription{
+ Namespace: nodeMetricNamespace,
+ Subsystem: iamSubsystem,
+ Name: "plugin_authn_service_succ_max_rtt_ms_minute",
+ Help: "When plugin authentication is configured, returns maximum round-trip-time of successful requests in the last full minute",
+ Type: gaugeMetric,
+ },
+ Value: pluginAuthNMetrics.MaxSuccRTTMs,
+ },
+ }
+
+ return metrics
+ })
+ return mg
+}
+
+// replication metrics for each node - published to the cluster endpoint with nodename as label
+func getReplicationNodeMetrics(opts MetricsGroupOpts) *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 1 * time.Minute,
+ metricsGroupOpts: opts,
+ }
+ const (
+ Online = 1
+ Offline = 0
+ )
+
+ mg.RegisterRead(func(_ context.Context) []MetricV2 {
+ var ml []MetricV2
+ // common operational metrics for bucket replication and site replication - published
+ // at cluster level
+ if rStats := globalReplicationStats.Load(); rStats != nil {
+ qs := rStats.getNodeQueueStatsSummary()
+ activeWorkersCount := MetricV2{
+ Description: getClusterReplActiveWorkersCountMD(),
+ }
+ avgActiveWorkersCount := MetricV2{
+ Description: getClusterReplAvgActiveWorkersCountMD(),
+ }
+ maxActiveWorkersCount := MetricV2{
+ Description: getClusterReplMaxActiveWorkersCountMD(),
+ }
+ currInQueueCount := MetricV2{
+ Description: getClusterReplCurrQueuedOperationsMD(),
+ }
+ currInQueueBytes := MetricV2{
+ Description: getClusterReplCurrQueuedBytesMD(),
+ }
+
+ currTransferRate := MetricV2{
+ Description: getClusterReplCurrentTransferRateMD(),
+ }
+ avgQueueCount := MetricV2{
+ Description: getClusterReplAvgQueuedOperationsMD(),
+ }
+ avgQueueBytes := MetricV2{
+ Description: getClusterReplAvgQueuedBytesMD(),
+ }
+ maxQueueCount := MetricV2{
+ Description: getClusterReplMaxQueuedOperationsMD(),
+ }
+ maxQueueBytes := MetricV2{
+ Description: getClusterReplMaxQueuedBytesMD(),
+ }
+ avgTransferRate := MetricV2{
+ Description: getClusterReplAvgTransferRateMD(),
+ }
+ maxTransferRate := MetricV2{
+ Description: getClusterReplMaxTransferRateMD(),
+ }
+ mrfCount := MetricV2{
+ Description: getClusterReplMRFFailedOperationsMD(),
+ Value: float64(qs.MRFStats.LastFailedCount),
+ }
+
+ if qs.QStats.Avg.Count > 0 || qs.QStats.Curr.Count > 0 {
+ qt := qs.QStats
+ currInQueueBytes.Value = qt.Curr.Bytes
+ currInQueueCount.Value = qt.Curr.Count
+ avgQueueBytes.Value = qt.Avg.Bytes
+ avgQueueCount.Value = qt.Avg.Count
+ maxQueueBytes.Value = qt.Max.Bytes
+ maxQueueCount.Value = qt.Max.Count
+ }
+ activeWorkersCount.Value = float64(qs.ActiveWorkers.Curr)
+ avgActiveWorkersCount.Value = float64(qs.ActiveWorkers.Avg)
+ maxActiveWorkersCount.Value = float64(qs.ActiveWorkers.Max)
+
+ if len(qs.XferStats) > 0 {
+ tots := qs.XferStats[Total]
+ currTransferRate.Value = tots.Curr
+ avgTransferRate.Value = tots.Avg
+ maxTransferRate.Value = tots.Peak
+ }
+ ml = []MetricV2{
+ activeWorkersCount,
+ avgActiveWorkersCount,
+ maxActiveWorkersCount,
+ currInQueueCount,
+ currInQueueBytes,
+ avgQueueCount,
+ avgQueueBytes,
+ maxQueueCount,
+ maxQueueBytes,
+ currTransferRate,
+ avgTransferRate,
+ maxTransferRate,
+ mrfCount,
+ }
+ }
+ for ep, health := range globalBucketTargetSys.healthStats() {
+ // link latency current
+ m := MetricV2{
+ Description: getClusterRepLinkLatencyCurrMD(),
+ VariableLabels: map[string]string{
+ "endpoint": ep,
+ },
+ }
+ m.Value = float64(health.latency.curr / time.Millisecond)
+ ml = append(ml, m)
+
+ // link latency average
+ m = MetricV2{
+ Description: getClusterRepLinkLatencyAvgMD(),
+ VariableLabels: map[string]string{
+ "endpoint": ep,
+ },
+ }
+ m.Value = float64(health.latency.avg / time.Millisecond)
+ ml = append(ml, m)
+
+ // link latency max
+ m = MetricV2{
+ Description: getClusterRepLinkLatencyMaxMD(),
+ VariableLabels: map[string]string{
+ "endpoint": ep,
+ },
+ }
+ m.Value = float64(health.latency.peak / time.Millisecond)
+ ml = append(ml, m)
+
+ linkOnline := MetricV2{
+ Description: getClusterRepLinkOnlineMD(),
+ VariableLabels: map[string]string{
+ "endpoint": ep,
+ },
+ }
+ online := Offline
+ if health.Online {
+ online = Online
+ }
+ linkOnline.Value = float64(online)
+ ml = append(ml, linkOnline)
+ offlineDuration := MetricV2{
+ Description: getClusterRepLinkCurrOfflineDurationMD(),
+ VariableLabels: map[string]string{
+ "endpoint": ep,
+ },
+ }
+ currDowntime := time.Duration(0)
+ if !health.Online && !health.lastOnline.IsZero() {
+ currDowntime = UTCNow().Sub(health.lastOnline)
+ }
+ offlineDuration.Value = float64(currDowntime / time.Second)
+ ml = append(ml, offlineDuration)
+
+ downtimeDuration := MetricV2{
+ Description: getClusterRepLinkTotalOfflineDurationMD(),
+ VariableLabels: map[string]string{
+ "endpoint": ep,
+ },
+ }
+ dwntime := max(health.offlineDuration, currDowntime)
+ downtimeDuration.Value = float64(dwntime / time.Second)
+ ml = append(ml, downtimeDuration)
+ }
+ return ml
+ })
+ return mg
+}
+
+// replication metrics for site replication
+func getReplicationSiteMetrics(opts MetricsGroupOpts) *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 1 * time.Minute,
+ metricsGroupOpts: opts,
+ }
+ mg.RegisterRead(func(_ context.Context) []MetricV2 {
+ ml := []MetricV2{}
+
+ // metrics pertinent to site replication - overall roll up.
+ if globalSiteReplicationSys.isEnabled() {
+ m, err := globalSiteReplicationSys.getSiteMetrics(GlobalContext)
+ if err != nil {
+ metricsLogIf(GlobalContext, err)
+ return ml
+ }
+ ml = append(ml, MetricV2{
+ Description: getRepReceivedBytesMD(clusterMetricNamespace),
+ Value: float64(m.ReplicaSize),
+ })
+ ml = append(ml, MetricV2{
+ Description: getRepReceivedOperationsMD(clusterMetricNamespace),
+ Value: float64(m.ReplicaCount),
+ })
+
+ for _, stat := range m.Metrics {
+ ml = append(ml, MetricV2{
+ Description: getRepFailedBytesLastMinuteMD(clusterMetricNamespace),
+ Value: float64(stat.Failed.LastMinute.Bytes),
+ VariableLabels: map[string]string{"endpoint": stat.Endpoint},
+ })
+ ml = append(ml, MetricV2{
+ Description: getRepFailedOperationsLastMinuteMD(clusterMetricNamespace),
+ Value: stat.Failed.LastMinute.Count,
+ VariableLabels: map[string]string{"endpoint": stat.Endpoint},
+ })
+ ml = append(ml, MetricV2{
+ Description: getRepFailedBytesLastHourMD(clusterMetricNamespace),
+ Value: float64(stat.Failed.LastHour.Bytes),
+ VariableLabels: map[string]string{"endpoint": stat.Endpoint},
+ })
+ ml = append(ml, MetricV2{
+ Description: getRepFailedOperationsLastHourMD(clusterMetricNamespace),
+ Value: stat.Failed.LastHour.Count,
+ VariableLabels: map[string]string{"endpoint": stat.Endpoint},
+ })
+ ml = append(ml, MetricV2{
+ Description: getRepFailedBytesTotalMD(clusterMetricNamespace),
+ Value: float64(stat.Failed.Totals.Bytes),
+ VariableLabels: map[string]string{"endpoint": stat.Endpoint},
+ })
+ ml = append(ml, MetricV2{
+ Description: getRepFailedOperationsTotalMD(clusterMetricNamespace),
+ Value: stat.Failed.Totals.Count,
+ VariableLabels: map[string]string{"endpoint": stat.Endpoint},
+ })
+
+ ml = append(ml, MetricV2{
+ Description: getRepSentBytesMD(clusterMetricNamespace),
+ Value: float64(stat.ReplicatedSize),
+ VariableLabels: map[string]string{"endpoint": stat.Endpoint},
+ })
+ ml = append(ml, MetricV2{
+ Description: getRepSentOperationsMD(clusterMetricNamespace),
+ Value: float64(stat.ReplicatedCount),
+ VariableLabels: map[string]string{"endpoint": stat.Endpoint},
+ })
+
+ if c, ok := stat.Failed.ErrCounts["AccessDenied"]; ok {
+ ml = append(ml, MetricV2{
+ Description: getClusterRepCredentialErrorsMD(clusterMetricNamespace),
+ Value: float64(c),
+ VariableLabels: map[string]string{"endpoint": stat.Endpoint},
+ })
+ }
+ }
+ ml = append(ml, MetricV2{
+ Description: getClusterReplProxiedGetOperationsMD(clusterMetricNamespace),
+ Value: float64(m.Proxied.GetTotal),
+ })
+ ml = append(ml, MetricV2{
+ Description: getClusterReplProxiedHeadOperationsMD(clusterMetricNamespace),
+ Value: float64(m.Proxied.HeadTotal),
+ })
+ ml = append(ml, MetricV2{
+ Description: getClusterReplProxiedPutTaggingOperationsMD(clusterMetricNamespace),
+ Value: float64(m.Proxied.PutTagTotal),
+ })
+ ml = append(ml, MetricV2{
+ Description: getClusterReplProxiedGetTaggingOperationsMD(clusterMetricNamespace),
+ Value: float64(m.Proxied.GetTagTotal),
+ })
+ ml = append(ml, MetricV2{
+ Description: getClusterReplProxiedRmvTaggingOperationsMD(clusterMetricNamespace),
+ Value: float64(m.Proxied.RmvTagTotal),
+ })
+ ml = append(ml, MetricV2{
+ Description: getClusterReplProxiedGetFailedOperationsMD(clusterMetricNamespace),
+ Value: float64(m.Proxied.GetFailedTotal),
+ })
+ ml = append(ml, MetricV2{
+ Description: getClusterReplProxiedHeadFailedOperationsMD(clusterMetricNamespace),
+ Value: float64(m.Proxied.HeadFailedTotal),
+ })
+ ml = append(ml, MetricV2{
+ Description: getClusterReplProxiedPutTaggingFailedOperationsMD(clusterMetricNamespace),
+ Value: float64(m.Proxied.PutTagFailedTotal),
+ })
+ ml = append(ml, MetricV2{
+ Description: getClusterReplProxiedGetTaggingFailedOperationsMD(clusterMetricNamespace),
+ Value: float64(m.Proxied.GetTagFailedTotal),
+ })
+ ml = append(ml, MetricV2{
+ Description: getClusterReplProxiedRmvTaggingFailedOperationsMD(clusterMetricNamespace),
+ Value: float64(m.Proxied.RmvTagFailedTotal),
+ })
+ }
+
+ return ml
+ })
+ return mg
+}
+
+func getMinioVersionMetrics() *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 10 * time.Second,
+ }
+ mg.RegisterRead(func(_ context.Context) (metrics []MetricV2) {
+ metrics = append(metrics, MetricV2{
+ Description: getMinIOCommitMD(),
+ VariableLabels: map[string]string{"commit": CommitID},
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getMinIOVersionMD(),
+ VariableLabels: map[string]string{"version": Version},
+ })
+ return metrics
+ })
+ return mg
+}
+
+func getNodeHealthMetrics(opts MetricsGroupOpts) *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 1 * time.Minute,
+ metricsGroupOpts: opts,
+ }
+ mg.RegisterRead(func(_ context.Context) (metrics []MetricV2) {
+ metrics = make([]MetricV2, 0, 16)
+ nodesUp, nodesDown := globalNotificationSys.GetPeerOnlineCount()
+ metrics = append(metrics, MetricV2{
+ Description: getNodeOnlineTotalMD(),
+ Value: float64(nodesUp),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getNodeOfflineTotalMD(),
+ Value: float64(nodesDown),
+ })
+ return metrics
+ })
+ return mg
+}
+
+func getMinioHealingMetrics(opts MetricsGroupOpts) *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 10 * time.Second,
+ metricsGroupOpts: opts,
+ }
+ mg.RegisterRead(func(_ context.Context) (metrics []MetricV2) {
+ bgSeq, exists := globalBackgroundHealState.getHealSequenceByToken(bgHealingUUID)
+ if !exists {
+ return metrics
+ }
+
+ if bgSeq.lastHealActivity.IsZero() {
+ return metrics
+ }
+
+ metrics = make([]MetricV2, 0, 5)
+ metrics = append(metrics, MetricV2{
+ Description: getHealLastActivityTimeMD(),
+ Value: float64(time.Since(bgSeq.lastHealActivity)),
+ })
+ metrics = append(metrics, getObjectsScanned(bgSeq)...)
+ metrics = append(metrics, getHealedItems(bgSeq)...)
+ metrics = append(metrics, getFailedItems(bgSeq)...)
+ return metrics
+ })
+ return mg
+}
+
+func getFailedItems(seq *healSequence) (m []MetricV2) {
+ items := seq.getHealFailedItemsMap()
+ m = make([]MetricV2, 0, len(items))
+ for k, v := range items {
+ m = append(m, MetricV2{
+ Description: getHealObjectsFailTotalMD(),
+ VariableLabels: map[string]string{"type": string(k)},
+ Value: float64(v),
+ })
+ }
+ return m
+}
+
+func getHealedItems(seq *healSequence) (m []MetricV2) {
+ items := seq.getHealedItemsMap()
+ m = make([]MetricV2, 0, len(items))
+ for k, v := range items {
+ m = append(m, MetricV2{
+ Description: getHealObjectsHealTotalMD(),
+ VariableLabels: map[string]string{"type": string(k)},
+ Value: float64(v),
+ })
+ }
+ return m
+}
+
+func getObjectsScanned(seq *healSequence) (m []MetricV2) {
+ items := seq.getScannedItemsMap()
+ m = make([]MetricV2, 0, len(items))
+ for k, v := range items {
+ m = append(m, MetricV2{
+ Description: getHealObjectsTotalMD(),
+ VariableLabels: map[string]string{"type": string(k)},
+ Value: float64(v),
+ })
+ }
+ return m
+}
+
+func getDistLockMetrics(opts MetricsGroupOpts) *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 1 * time.Second,
+ metricsGroupOpts: opts,
+ }
+ mg.RegisterRead(func(ctx context.Context) []MetricV2 {
+ if !globalIsDistErasure {
+ return []MetricV2{}
+ }
+
+ st := globalLockServer.stats()
+
+ metrics := make([]MetricV2, 0, 3)
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: minioNamespace,
+ Subsystem: "locks",
+ Name: "total",
+ Help: "Number of current locks on this peer",
+ Type: gaugeMetric,
+ },
+ Value: float64(st.Total),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: minioNamespace,
+ Subsystem: "locks",
+ Name: "write_total",
+ Help: "Number of current WRITE locks on this peer",
+ Type: gaugeMetric,
+ },
+ Value: float64(st.Writes),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: minioNamespace,
+ Subsystem: "locks",
+ Name: "read_total",
+ Help: "Number of current READ locks on this peer",
+ Type: gaugeMetric,
+ },
+ Value: float64(st.Reads),
+ })
+ return metrics
+ })
+ return mg
+}
+
+func getNotificationMetrics(opts MetricsGroupOpts) *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 10 * time.Second,
+ metricsGroupOpts: opts,
+ }
+ mg.RegisterRead(func(ctx context.Context) []MetricV2 {
+ metrics := make([]MetricV2, 0, 3)
+
+ if globalEventNotifier != nil {
+ nstats := globalEventNotifier.targetList.Stats()
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: minioNamespace,
+ Subsystem: notifySubsystem,
+ Name: "current_send_in_progress",
+ Help: "Number of concurrent async Send calls active to all targets (deprecated, please use 'minio_notify_target_current_send_in_progress' instead)",
+ Type: gaugeMetric,
+ },
+ Value: float64(nstats.CurrentSendCalls),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: minioNamespace,
+ Subsystem: notifySubsystem,
+ Name: "events_skipped_total",
+ Help: "Events that were skipped to be sent to the targets due to the in-memory queue being full",
+ Type: counterMetric,
+ },
+ Value: float64(nstats.EventsSkipped),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: minioNamespace,
+ Subsystem: notifySubsystem,
+ Name: "events_errors_total",
+ Help: "Events that were failed to be sent to the targets (deprecated, please use 'minio_notify_target_failed_events' instead)",
+ Type: counterMetric,
+ },
+ Value: float64(nstats.EventsErrorsTotal),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: minioNamespace,
+ Subsystem: notifySubsystem,
+ Name: "events_sent_total",
+ Help: "Total number of events sent to the targets (deprecated, please use 'minio_notify_target_total_events' instead)",
+ Type: counterMetric,
+ },
+ Value: float64(nstats.TotalEvents),
+ })
+ for id, st := range nstats.TargetStats {
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: minioNamespace,
+ Subsystem: notifySubsystem,
+ Name: "target_total_events",
+ Help: "Total number of events sent (or) queued to the target",
+ Type: counterMetric,
+ },
+ VariableLabels: map[string]string{"target_id": id.ID, "target_name": id.Name},
+ Value: float64(st.TotalEvents),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: minioNamespace,
+ Subsystem: notifySubsystem,
+ Name: "target_failed_events",
+ Help: "Number of events failed to be sent (or) queued to the target",
+ Type: counterMetric,
+ },
+ VariableLabels: map[string]string{"target_id": id.ID, "target_name": id.Name},
+ Value: float64(st.FailedEvents),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: minioNamespace,
+ Subsystem: notifySubsystem,
+ Name: "target_current_send_in_progress",
+ Help: "Number of concurrent async Send calls active to the target",
+ Type: gaugeMetric,
+ },
+ VariableLabels: map[string]string{"target_id": id.ID, "target_name": id.Name},
+ Value: float64(st.CurrentSendCalls),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: minioNamespace,
+ Subsystem: notifySubsystem,
+ Name: "target_queue_length",
+ Help: "Number of events currently staged in the queue_dir configured for the target",
+ Type: gaugeMetric,
+ },
+ VariableLabels: map[string]string{"target_id": id.ID, "target_name": id.Name},
+ Value: float64(st.CurrentQueue),
+ })
+ }
+ }
+
+ lstats := globalLambdaTargetList.Stats()
+ for _, st := range lstats.TargetStats {
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: minioNamespace,
+ Subsystem: lambdaSubsystem,
+ Name: "active_requests",
+ Help: "Number of in progress requests",
+ },
+ VariableLabels: map[string]string{"target_id": st.ID.ID, "target_name": st.ID.Name},
+ Value: float64(st.ActiveRequests),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: minioNamespace,
+ Subsystem: lambdaSubsystem,
+ Name: "total_requests",
+ Help: "Total number of requests sent since start",
+ Type: counterMetric,
+ },
+ VariableLabels: map[string]string{"target_id": st.ID.ID, "target_name": st.ID.Name},
+ Value: float64(st.TotalRequests),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: minioNamespace,
+ Subsystem: lambdaSubsystem,
+ Name: "failed_requests",
+ Help: "Total number of requests that failed to send since start",
+ Type: counterMetric,
+ },
+ VariableLabels: map[string]string{"target_id": st.ID.ID, "target_name": st.ID.Name},
+ Value: float64(st.FailedRequests),
+ })
+ }
+
+ // Audit and system:
+ audit := logger.CurrentStats()
+ for id, st := range audit {
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: minioNamespace,
+ Subsystem: auditSubsystem,
+ Name: "target_queue_length",
+ Help: "Number of unsent messages in queue for target",
+ Type: gaugeMetric,
+ },
+ VariableLabels: map[string]string{"target_id": id},
+ Value: float64(st.QueueLength),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: minioNamespace,
+ Subsystem: auditSubsystem,
+ Name: "total_messages",
+ Help: "Total number of messages sent since start",
+ Type: counterMetric,
+ },
+ VariableLabels: map[string]string{"target_id": id},
+ Value: float64(st.TotalMessages),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: minioNamespace,
+ Subsystem: auditSubsystem,
+ Name: "failed_messages",
+ Help: "Total number of messages that failed to send since start",
+ Type: counterMetric,
+ },
+ VariableLabels: map[string]string{"target_id": id},
+ Value: float64(st.FailedMessages),
+ })
+ }
+ return metrics
+ })
+ return mg
+}
+
+func getHTTPMetrics(opts MetricsGroupOpts) *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 10 * time.Second,
+ metricsGroupOpts: opts,
+ }
+ mg.RegisterRead(func(ctx context.Context) (metrics []MetricV2) {
+ if !mg.metricsGroupOpts.bucketOnly {
+ httpStats := globalHTTPStats.toServerHTTPStats(true)
+ metrics = make([]MetricV2, 0, 3+
+ len(httpStats.CurrentS3Requests.APIStats)+
+ len(httpStats.TotalS3Requests.APIStats)+
+ len(httpStats.TotalS3Errors.APIStats)+
+ len(httpStats.TotalS35xxErrors.APIStats)+
+ len(httpStats.TotalS34xxErrors.APIStats))
+ metrics = append(metrics, MetricV2{
+ Description: getS3RejectedAuthRequestsTotalMD(),
+ Value: float64(httpStats.TotalS3RejectedAuth),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getS3RejectedTimestampRequestsTotalMD(),
+ Value: float64(httpStats.TotalS3RejectedTime),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getS3RejectedHeaderRequestsTotalMD(),
+ Value: float64(httpStats.TotalS3RejectedHeader),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getS3RejectedInvalidRequestsTotalMD(),
+ Value: float64(httpStats.TotalS3RejectedInvalid),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getS3RequestsInQueueMD(),
+ Value: float64(httpStats.S3RequestsInQueue),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getIncomingS3RequestsMD(),
+ Value: float64(httpStats.S3RequestsIncoming),
+ })
+
+ for api, value := range httpStats.CurrentS3Requests.APIStats {
+ metrics = append(metrics, MetricV2{
+ Description: getS3RequestsInFlightMD(),
+ Value: float64(value),
+ VariableLabels: map[string]string{"api": api},
+ })
+ }
+ for api, value := range httpStats.TotalS3Requests.APIStats {
+ metrics = append(metrics, MetricV2{
+ Description: getS3RequestsTotalMD(),
+ Value: float64(value),
+ VariableLabels: map[string]string{"api": api},
+ })
+ }
+ for api, value := range httpStats.TotalS3Errors.APIStats {
+ metrics = append(metrics, MetricV2{
+ Description: getS3RequestsErrorsMD(),
+ Value: float64(value),
+ VariableLabels: map[string]string{"api": api},
+ })
+ }
+ for api, value := range httpStats.TotalS35xxErrors.APIStats {
+ metrics = append(metrics, MetricV2{
+ Description: getS3Requests5xxErrorsMD(),
+ Value: float64(value),
+ VariableLabels: map[string]string{"api": api},
+ })
+ }
+ for api, value := range httpStats.TotalS34xxErrors.APIStats {
+ metrics = append(metrics, MetricV2{
+ Description: getS3Requests4xxErrorsMD(),
+ Value: float64(value),
+ VariableLabels: map[string]string{"api": api},
+ })
+ }
+ for api, value := range httpStats.TotalS3Canceled.APIStats {
+ metrics = append(metrics, MetricV2{
+ Description: getS3RequestsCanceledMD(),
+ Value: float64(value),
+ VariableLabels: map[string]string{"api": api},
+ })
+ }
+ return metrics
+ }
+
+ // If we have too many, limit them
+ bConnStats := globalBucketConnStats.getS3InOutBytes()
+ buckets := mapKeysSorted(bConnStats)
+ buckets = buckets[:min(v2MetricsMaxBuckets, len(buckets))]
+
+ for _, bucket := range buckets {
+ inOut := bConnStats[bucket]
+ recvBytes := inOut.In
+ if recvBytes > 0 {
+ metrics = append(metrics, MetricV2{
+ Description: getBucketTrafficReceivedBytes(),
+ Value: float64(recvBytes),
+ VariableLabels: map[string]string{"bucket": bucket},
+ })
+ }
+ sentBytes := inOut.Out
+ if sentBytes > 0 {
+ metrics = append(metrics, MetricV2{
+ Description: getBucketTrafficSentBytes(),
+ Value: float64(sentBytes),
+ VariableLabels: map[string]string{"bucket": bucket},
+ })
+ }
+
+ httpStats := globalBucketHTTPStats.load(bucket)
+ for k, v := range httpStats.currentS3Requests.Load(true) {
+ metrics = append(metrics, MetricV2{
+ Description: getBucketS3RequestsInFlightMD(),
+ Value: float64(v),
+ VariableLabels: map[string]string{"bucket": bucket, "api": k},
+ })
+ }
+
+ for k, v := range httpStats.totalS3Requests.Load(true) {
+ metrics = append(metrics, MetricV2{
+ Description: getBucketS3RequestsTotalMD(),
+ Value: float64(v),
+ VariableLabels: map[string]string{"bucket": bucket, "api": k},
+ })
+ }
+
+ for k, v := range httpStats.totalS3Canceled.Load(true) {
+ metrics = append(metrics, MetricV2{
+ Description: getBucketS3RequestsCanceledMD(),
+ Value: float64(v),
+ VariableLabels: map[string]string{"bucket": bucket, "api": k},
+ })
+ }
+
+ for k, v := range httpStats.totalS34xxErrors.Load(true) {
+ metrics = append(metrics, MetricV2{
+ Description: getBucketS3Requests4xxErrorsMD(),
+ Value: float64(v),
+ VariableLabels: map[string]string{"bucket": bucket, "api": k},
+ })
+ }
+
+ for k, v := range httpStats.totalS35xxErrors.Load(true) {
+ metrics = append(metrics, MetricV2{
+ Description: getBucketS3Requests5xxErrorsMD(),
+ Value: float64(v),
+ VariableLabels: map[string]string{"bucket": bucket, "api": k},
+ })
+ }
+ }
+
+ return metrics
+ })
+ return mg
+}
+
+func getNetworkMetrics() *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 10 * time.Second,
+ }
+ mg.RegisterRead(func(ctx context.Context) (metrics []MetricV2) {
+ metrics = make([]MetricV2, 0, 10)
+ connStats := globalConnStats.toServerConnStats()
+ rpcStats := rest.GetRPCStats()
+ if globalIsDistErasure {
+ metrics = append(metrics, MetricV2{
+ Description: getInternodeFailedRequests(),
+ Value: float64(rpcStats.Errs),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getInternodeTCPDialTimeout(),
+ Value: float64(rpcStats.DialErrs),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getInternodeTCPAvgDuration(),
+ Value: float64(rpcStats.DialAvgDuration),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getInterNodeSentBytesMD(),
+ Value: float64(connStats.internodeOutputBytes),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getInterNodeReceivedBytesMD(),
+ Value: float64(connStats.internodeInputBytes),
+ })
+ }
+ metrics = append(metrics, MetricV2{
+ Description: getS3SentBytesMD(),
+ Value: float64(connStats.s3OutputBytes),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getS3ReceivedBytesMD(),
+ Value: float64(connStats.s3InputBytes),
+ })
+ return metrics
+ })
+ return mg
+}
+
+func getClusterUsageMetrics(opts MetricsGroupOpts) *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 1 * time.Minute,
+ metricsGroupOpts: opts,
+ }
+ mg.RegisterRead(func(ctx context.Context) (metrics []MetricV2) {
+ objLayer := newObjectLayerFn()
+ if objLayer == nil {
+ return metrics
+ }
+
+ metrics = make([]MetricV2, 0, 50)
+ dataUsageInfo, err := loadDataUsageFromBackend(ctx, objLayer)
+ if err != nil {
+ metricsLogIf(ctx, err)
+ return metrics
+ }
+
+ // data usage has not captured any data yet.
+ if dataUsageInfo.LastUpdate.IsZero() {
+ return metrics
+ }
+
+ metrics = append(metrics, MetricV2{
+ Description: getUsageLastScanActivityMD(),
+ Value: float64(time.Since(dataUsageInfo.LastUpdate)),
+ })
+
+ var (
+ clusterSize uint64
+ clusterBuckets uint64
+ clusterObjectsCount uint64
+ clusterVersionsCount uint64
+ clusterDeleteMarkersCount uint64
+ )
+
+ clusterObjectSizesHistogram := map[string]uint64{}
+ clusterVersionsHistogram := map[string]uint64{}
+ for _, usage := range dataUsageInfo.BucketsUsage {
+ clusterBuckets++
+ clusterSize += usage.Size
+ clusterObjectsCount += usage.ObjectsCount
+ clusterVersionsCount += usage.VersionsCount
+ clusterDeleteMarkersCount += usage.DeleteMarkersCount
+ for k, v := range usage.ObjectSizesHistogram {
+ v1, ok := clusterObjectSizesHistogram[k]
+ if !ok {
+ clusterObjectSizesHistogram[k] = v
+ } else {
+ v1 += v
+ clusterObjectSizesHistogram[k] = v1
+ }
+ }
+ for k, v := range usage.ObjectVersionsHistogram {
+ v1, ok := clusterVersionsHistogram[k]
+ if !ok {
+ clusterVersionsHistogram[k] = v
+ } else {
+ v1 += v
+ clusterVersionsHistogram[k] = v1
+ }
+ }
+ }
+
+ metrics = append(metrics, MetricV2{
+ Description: getClusterUsageTotalBytesMD(),
+ Value: float64(clusterSize),
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getClusterUsageObjectsTotalMD(),
+ Value: float64(clusterObjectsCount),
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getClusterUsageVersionsTotalMD(),
+ Value: float64(clusterVersionsCount),
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getClusterUsageDeleteMarkersTotalMD(),
+ Value: float64(clusterDeleteMarkersCount),
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getClusterObjectDistributionMD(),
+ Histogram: clusterObjectSizesHistogram,
+ HistogramBucketLabel: "range",
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getClusterObjectVersionsMD(),
+ Histogram: clusterVersionsHistogram,
+ HistogramBucketLabel: "range",
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getClusterBucketsTotalMD(),
+ Value: float64(clusterBuckets),
+ })
+
+ return metrics
+ })
+ return mg
+}
+
+func getBucketUsageMetrics(opts MetricsGroupOpts) *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 1 * time.Minute,
+ metricsGroupOpts: opts,
+ }
+ mg.RegisterRead(func(ctx context.Context) (metrics []MetricV2) {
+ objLayer := newObjectLayerFn()
+
+ metrics = make([]MetricV2, 0, 50)
+ dataUsageInfo, err := loadDataUsageFromBackend(ctx, objLayer)
+ if err != nil {
+ metricsLogIf(ctx, err)
+ return metrics
+ }
+
+ // data usage has not captured any data yet.
+ if dataUsageInfo.LastUpdate.IsZero() {
+ return metrics
+ }
+
+ metrics = append(metrics, MetricV2{
+ Description: getBucketUsageLastScanActivityMD(),
+ Value: float64(time.Since(dataUsageInfo.LastUpdate)),
+ })
+
+ var bucketReplStats map[string]BucketStats
+ if !globalSiteReplicationSys.isEnabled() {
+ bucketReplStats = globalReplicationStats.Load().getAllLatest(dataUsageInfo.BucketsUsage)
+ }
+ buckets := mapKeysSorted(dataUsageInfo.BucketsUsage)
+ if len(buckets) > v2MetricsMaxBuckets {
+ buckets = buckets[:v2MetricsMaxBuckets]
+ }
+ for _, bucket := range buckets {
+ usage := dataUsageInfo.BucketsUsage[bucket]
+ quota, _ := globalBucketQuotaSys.Get(ctx, bucket)
+
+ metrics = append(metrics, MetricV2{
+ Description: getBucketUsageTotalBytesMD(),
+ Value: float64(usage.Size),
+ VariableLabels: map[string]string{"bucket": bucket},
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getBucketUsageObjectsTotalMD(),
+ Value: float64(usage.ObjectsCount),
+ VariableLabels: map[string]string{"bucket": bucket},
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getBucketUsageVersionsTotalMD(),
+ Value: float64(usage.VersionsCount),
+ VariableLabels: map[string]string{"bucket": bucket},
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getBucketUsageDeleteMarkersTotalMD(),
+ Value: float64(usage.DeleteMarkersCount),
+ VariableLabels: map[string]string{"bucket": bucket},
+ })
+
+ if quota != nil && quota.Quota > 0 {
+ metrics = append(metrics, MetricV2{
+ Description: getBucketUsageQuotaTotalBytesMD(),
+ Value: float64(quota.Quota),
+ VariableLabels: map[string]string{"bucket": bucket},
+ })
+ }
+ if !globalSiteReplicationSys.isEnabled() {
+ var stats BucketReplicationStats
+ s, ok := bucketReplStats[bucket]
+ if ok {
+ stats = s.ReplicationStats
+ metrics = append(metrics, MetricV2{
+ Description: getRepReceivedBytesMD(bucketMetricNamespace),
+ Value: float64(stats.ReplicaSize),
+ VariableLabels: map[string]string{"bucket": bucket},
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getRepReceivedOperationsMD(bucketMetricNamespace),
+ Value: float64(stats.ReplicaCount),
+ VariableLabels: map[string]string{"bucket": bucket},
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getClusterReplProxiedGetOperationsMD(bucketMetricNamespace),
+ Value: float64(s.ProxyStats.GetTotal),
+ VariableLabels: map[string]string{"bucket": bucket},
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getClusterReplProxiedHeadOperationsMD(bucketMetricNamespace),
+ Value: float64(s.ProxyStats.HeadTotal),
+ VariableLabels: map[string]string{"bucket": bucket},
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getClusterReplProxiedPutTaggingOperationsMD(bucketMetricNamespace),
+ Value: float64(s.ProxyStats.PutTagTotal),
+ VariableLabels: map[string]string{"bucket": bucket},
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getClusterReplProxiedGetTaggingOperationsMD(bucketMetricNamespace),
+ Value: float64(s.ProxyStats.GetTagTotal),
+ VariableLabels: map[string]string{"bucket": bucket},
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getClusterReplProxiedRmvTaggingOperationsMD(bucketMetricNamespace),
+ Value: float64(s.ProxyStats.RmvTagTotal),
+ VariableLabels: map[string]string{"bucket": bucket},
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getClusterReplProxiedGetFailedOperationsMD(bucketMetricNamespace),
+ Value: float64(s.ProxyStats.GetFailedTotal),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getClusterReplProxiedHeadFailedOperationsMD(bucketMetricNamespace),
+ Value: float64(s.ProxyStats.HeadFailedTotal),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getClusterReplProxiedPutTaggingFailedOperationsMD(bucketMetricNamespace),
+ Value: float64(s.ProxyStats.PutTagFailedTotal),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getClusterReplProxiedGetTaggingFailedOperationsMD(bucketMetricNamespace),
+ Value: float64(s.ProxyStats.GetTagFailedTotal),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getClusterReplProxiedRmvTaggingFailedOperationsMD(bucketMetricNamespace),
+ Value: float64(s.ProxyStats.RmvTagFailedTotal),
+ })
+ }
+ if stats.hasReplicationUsage() {
+ for arn, stat := range stats.Stats {
+ metrics = append(metrics, MetricV2{
+ Description: getRepFailedBytesLastMinuteMD(bucketMetricNamespace),
+ Value: float64(stat.Failed.LastMinute.Bytes),
+ VariableLabels: map[string]string{"bucket": bucket, "targetArn": arn},
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getRepFailedOperationsLastMinuteMD(bucketMetricNamespace),
+ Value: stat.Failed.LastMinute.Count,
+ VariableLabels: map[string]string{"bucket": bucket, "targetArn": arn},
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getRepFailedBytesLastHourMD(bucketMetricNamespace),
+ Value: float64(stat.Failed.LastHour.Bytes),
+ VariableLabels: map[string]string{"bucket": bucket, "targetArn": arn},
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getRepFailedOperationsLastHourMD(bucketMetricNamespace),
+ Value: stat.Failed.LastHour.Count,
+ VariableLabels: map[string]string{"bucket": bucket, "targetArn": arn},
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getRepFailedBytesTotalMD(bucketMetricNamespace),
+ Value: float64(stat.Failed.Totals.Bytes),
+ VariableLabels: map[string]string{"bucket": bucket, "targetArn": arn},
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getRepFailedOperationsTotalMD(bucketMetricNamespace),
+ Value: stat.Failed.Totals.Count,
+ VariableLabels: map[string]string{"bucket": bucket, "targetArn": arn},
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getRepSentBytesMD(bucketMetricNamespace),
+ Value: float64(stat.ReplicatedSize),
+ VariableLabels: map[string]string{"bucket": bucket, "targetArn": arn},
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getRepSentOperationsMD(bucketMetricNamespace),
+ Value: float64(stat.ReplicatedCount),
+ VariableLabels: map[string]string{"bucket": bucket, "targetArn": arn},
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getBucketRepLatencyMD(),
+ HistogramBucketLabel: "range",
+ Histogram: stat.Latency.getUploadLatency(),
+ VariableLabels: map[string]string{"bucket": bucket, "operation": "upload", "targetArn": arn},
+ })
+ if c, ok := stat.Failed.ErrCounts["AccessDenied"]; ok {
+ metrics = append(metrics, MetricV2{
+ Description: getClusterRepCredentialErrorsMD(bucketMetricNamespace),
+ Value: float64(c),
+ VariableLabels: map[string]string{"bucket": bucket, "targetArn": arn},
+ })
+ }
+ }
+ }
+ }
+ metrics = append(metrics, MetricV2{
+ Description: getBucketObjectDistributionMD(),
+ Histogram: usage.ObjectSizesHistogram,
+ HistogramBucketLabel: "range",
+ VariableLabels: map[string]string{"bucket": bucket},
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getBucketObjectVersionsMD(),
+ Histogram: usage.ObjectVersionsHistogram,
+ HistogramBucketLabel: "range",
+ VariableLabels: map[string]string{"bucket": bucket},
+ })
+ }
+ return metrics
+ })
+ return mg
+}
+
+func getClusterTransitionedBytesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: ilmSubsystem,
+ Name: transitionedBytes,
+ Help: "Total bytes transitioned to a tier",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterTransitionedObjectsMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: ilmSubsystem,
+ Name: transitionedObjects,
+ Help: "Total number of objects transitioned to a tier",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterTransitionedVersionsMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: ilmSubsystem,
+ Name: transitionedVersions,
+ Help: "Total number of versions transitioned to a tier",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterTierMetrics(opts MetricsGroupOpts) *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 1 * time.Minute,
+ metricsGroupOpts: opts,
+ }
+ mg.RegisterRead(func(ctx context.Context) (metrics []MetricV2) {
+ objLayer := newObjectLayerFn()
+
+ if globalTierConfigMgr.Empty() {
+ return metrics
+ }
+
+ dui, err := loadDataUsageFromBackend(ctx, objLayer)
+ if err != nil {
+ metricsLogIf(ctx, err)
+ return metrics
+ }
+ // data usage has not captured any tier stats yet.
+ if dui.TierStats == nil {
+ return metrics
+ }
+
+ return dui.tierMetrics()
+ })
+ return mg
+}
+
+func getLocalStorageMetrics(opts MetricsGroupOpts) *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 1 * time.Minute,
+ metricsGroupOpts: opts,
+ }
+ mg.RegisterRead(func(ctx context.Context) (metrics []MetricV2) {
+ objLayer := newObjectLayerFn()
+
+ metrics = make([]MetricV2, 0, 50)
+ storageInfo := objLayer.LocalStorageInfo(ctx, true)
+ onlineDrives, offlineDrives := getOnlineOfflineDisksStats(storageInfo.Disks)
+ totalDrives := onlineDrives.Merge(offlineDrives)
+
+ for _, disk := range storageInfo.Disks {
+ metrics = append(metrics, MetricV2{
+ Description: getNodeDriveUsedBytesMD(),
+ Value: float64(disk.UsedSpace),
+ VariableLabels: map[string]string{"drive": disk.DrivePath},
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getNodeDriveFreeBytesMD(),
+ Value: float64(disk.AvailableSpace),
+ VariableLabels: map[string]string{"drive": disk.DrivePath},
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getNodeDriveTotalBytesMD(),
+ Value: float64(disk.TotalSpace),
+ VariableLabels: map[string]string{"drive": disk.DrivePath},
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getNodeDrivesFreeInodesMD(),
+ Value: float64(disk.FreeInodes),
+ VariableLabels: map[string]string{"drive": disk.DrivePath},
+ })
+
+ if disk.Metrics != nil {
+ metrics = append(metrics, MetricV2{
+ Description: getNodeDriveTimeoutErrorsMD(),
+ Value: float64(disk.Metrics.TotalErrorsTimeout),
+ VariableLabels: map[string]string{"drive": disk.DrivePath},
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getNodeDriveIOErrorsMD(),
+ Value: float64(disk.Metrics.TotalErrorsAvailability - disk.Metrics.TotalErrorsTimeout),
+ VariableLabels: map[string]string{"drive": disk.DrivePath},
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getNodeDriveAvailabilityErrorsMD(),
+ Value: float64(disk.Metrics.TotalErrorsAvailability),
+ VariableLabels: map[string]string{"drive": disk.DrivePath},
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getNodeDriveWaitingIOMD(),
+ Value: float64(disk.Metrics.TotalWaiting),
+ VariableLabels: map[string]string{"drive": disk.DrivePath},
+ })
+
+ for apiName, latency := range disk.Metrics.LastMinute {
+ metrics = append(metrics, MetricV2{
+ Description: getNodeDriveAPILatencyMD(),
+ Value: float64(latency.Avg().Microseconds()),
+ VariableLabels: map[string]string{"drive": disk.DrivePath, "api": "storage." + apiName},
+ })
+ }
+ }
+ }
+
+ metrics = append(metrics, MetricV2{
+ Description: getNodeDrivesOfflineTotalMD(),
+ Value: float64(offlineDrives.Sum()),
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getNodeDrivesOnlineTotalMD(),
+ Value: float64(onlineDrives.Sum()),
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getNodeDrivesTotalMD(),
+ Value: float64(totalDrives.Sum()),
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getNodeStandardParityMD(),
+ Value: float64(storageInfo.Backend.StandardSCParity),
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getNodeRRSParityMD(),
+ Value: float64(storageInfo.Backend.RRSCParity),
+ })
+
+ return metrics
+ })
+ return mg
+}
+
+func getClusterWriteQuorumMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: "write",
+ Name: "quorum",
+ Help: "Maximum write quorum across all pools and sets",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterHealthStatusMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: "health",
+ Name: "status",
+ Help: "Get current cluster health status",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterErasureSetHealthStatusMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: "health",
+ Name: "erasure_set_status",
+ Help: "Get current health status for this erasure set",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterErasureSetReadQuorumMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: "health",
+ Name: "erasure_set_read_quorum",
+ Help: "Get the read quorum for this erasure set",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterErasureSetWriteQuorumMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: "health",
+ Name: "erasure_set_write_quorum",
+ Help: "Get the write quorum for this erasure set",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterErasureSetOnlineDrivesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: "health",
+ Name: "erasure_set_online_drives",
+ Help: "Get the count of the online drives in this erasure set",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterErasureSetHealingDrivesMD() MetricDescription {
+ return MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: "health",
+ Name: "erasure_set_healing_drives",
+ Help: "Get the count of healing drives of this erasure set",
+ Type: gaugeMetric,
+ }
+}
+
+func getClusterHealthMetrics(opts MetricsGroupOpts) *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 10 * time.Second,
+ metricsGroupOpts: opts,
+ }
+ mg.RegisterRead(func(ctx context.Context) (metrics []MetricV2) {
+ objLayer := newObjectLayerFn()
+
+ opts := HealthOptions{}
+ result := objLayer.Health(ctx, opts)
+
+ metrics = make([]MetricV2, 0, 2+4*len(result.ESHealth))
+
+ metrics = append(metrics, MetricV2{
+ Description: getClusterWriteQuorumMD(),
+ Value: float64(result.WriteQuorum),
+ })
+
+ health := 1
+ if !result.Healthy {
+ health = 0
+ }
+
+ metrics = append(metrics, MetricV2{
+ Description: getClusterHealthStatusMD(),
+ Value: float64(health),
+ })
+
+ for _, h := range result.ESHealth {
+ labels := map[string]string{
+ "pool": strconv.Itoa(h.PoolID),
+ "set": strconv.Itoa(h.SetID),
+ }
+ metrics = append(metrics, MetricV2{
+ Description: getClusterErasureSetReadQuorumMD(),
+ VariableLabels: labels,
+ Value: float64(h.ReadQuorum),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getClusterErasureSetWriteQuorumMD(),
+ VariableLabels: labels,
+ Value: float64(h.WriteQuorum),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getClusterErasureSetOnlineDrivesMD(),
+ VariableLabels: labels,
+ Value: float64(h.HealthyDrives),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: getClusterErasureSetHealingDrivesMD(),
+ VariableLabels: labels,
+ Value: float64(h.HealingDrives),
+ })
+
+ health := 1
+ if !h.Healthy {
+ health = 0
+ }
+
+ metrics = append(metrics, MetricV2{
+ Description: getClusterErasureSetHealthStatusMD(),
+ VariableLabels: labels,
+ Value: float64(health),
+ })
+ }
+
+ return metrics
+ })
+
+ return mg
+}
+
+func getBatchJobsMetrics(opts MetricsGroupOpts) *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 10 * time.Second,
+ metricsGroupOpts: opts,
+ }
+
+ mg.RegisterRead(func(ctx context.Context) (metrics []MetricV2) {
+ var m madmin.RealtimeMetrics
+ mLocal := collectLocalMetrics(madmin.MetricsBatchJobs, collectMetricsOpts{})
+ m.Merge(&mLocal)
+
+ mRemote := collectRemoteMetrics(ctx, madmin.MetricsBatchJobs, collectMetricsOpts{})
+ m.Merge(&mRemote)
+
+ if m.Aggregated.BatchJobs == nil {
+ return metrics
+ }
+
+ for _, mj := range m.Aggregated.BatchJobs.Jobs {
+ jtype := toSnake(mj.JobType)
+ var objects, objectsFailed float64
+ var bucket string
+ switch madmin.BatchJobType(mj.JobType) {
+ case madmin.BatchJobReplicate:
+ objects = float64(mj.Replicate.Objects)
+ objectsFailed = float64(mj.Replicate.ObjectsFailed)
+ bucket = mj.Replicate.Bucket
+ case madmin.BatchJobKeyRotate:
+ objects = float64(mj.KeyRotate.Objects)
+ objectsFailed = float64(mj.KeyRotate.ObjectsFailed)
+ bucket = mj.KeyRotate.Bucket
+ case madmin.BatchJobExpire:
+ objects = float64(mj.Expired.Objects)
+ objectsFailed = float64(mj.Expired.ObjectsFailed)
+ bucket = mj.Expired.Bucket
+ }
+ metrics = append(metrics,
+ MetricV2{
+ Description: MetricDescription{
+ Namespace: bucketMetricNamespace,
+ Subsystem: "batch",
+ Name: MetricName(jtype + "_objects"),
+ Help: "Get successfully completed batch job " + jtype + "objects",
+ Type: counterMetric,
+ },
+ Value: objects,
+ VariableLabels: map[string]string{"bucket": bucket, "jobId": mj.JobID},
+ },
+ MetricV2{
+ Description: MetricDescription{
+ Namespace: bucketMetricNamespace,
+ Subsystem: "batch",
+ Name: MetricName(jtype + "_objects_failed"),
+ Help: "Get failed batch job " + jtype + "objects",
+ Type: counterMetric,
+ },
+ Value: objectsFailed,
+ VariableLabels: map[string]string{"bucket": bucket, "jobId": mj.JobID},
+ },
+ )
+ }
+ return metrics
+ })
+ return mg
+}
+
+func getClusterStorageMetrics(opts MetricsGroupOpts) *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 1 * time.Minute,
+ metricsGroupOpts: opts,
+ }
+ mg.RegisterRead(func(ctx context.Context) (metrics []MetricV2) {
+ objLayer := newObjectLayerFn()
+
+ // Fetch disk space info, ignore errors
+ metrics = make([]MetricV2, 0, 10)
+ storageInfo := objLayer.StorageInfo(ctx, true)
+ onlineDrives, offlineDrives := getOnlineOfflineDisksStats(storageInfo.Disks)
+ totalDrives := onlineDrives.Merge(offlineDrives)
+
+ metrics = append(metrics, MetricV2{
+ Description: getClusterCapacityTotalBytesMD(),
+ Value: float64(GetTotalCapacity(storageInfo.Disks)),
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getClusterCapacityFreeBytesMD(),
+ Value: float64(GetTotalCapacityFree(storageInfo.Disks)),
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getClusterCapacityUsageBytesMD(),
+ Value: float64(GetTotalUsableCapacity(storageInfo.Disks, storageInfo)),
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getClusterCapacityUsageFreeBytesMD(),
+ Value: float64(GetTotalUsableCapacityFree(storageInfo.Disks, storageInfo)),
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getClusterDrivesOfflineTotalMD(),
+ Value: float64(offlineDrives.Sum()),
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getClusterDrivesOnlineTotalMD(),
+ Value: float64(onlineDrives.Sum()),
+ })
+
+ metrics = append(metrics, MetricV2{
+ Description: getClusterDrivesTotalMD(),
+ Value: float64(totalDrives.Sum()),
+ })
+ return metrics
+ })
+ return mg
+}
+
+func getKMSNodeMetrics(opts MetricsGroupOpts) *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 10 * time.Second,
+ metricsGroupOpts: opts,
+ }
+
+ mg.RegisterRead(func(ctx context.Context) (metrics []MetricV2) {
+ const (
+ Online = 1
+ Offline = 0
+ )
+ desc := MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: kmsSubsystem,
+ Name: kmsOnline,
+ Help: "Reports whether the KMS is online (1) or offline (0)",
+ Type: gaugeMetric,
+ }
+ _, err := GlobalKMS.Metrics(ctx)
+ if _, ok := kes.IsConnError(err); ok {
+ return []MetricV2{{
+ Description: desc,
+ Value: float64(Offline),
+ }}
+ }
+ return []MetricV2{{
+ Description: desc,
+ Value: float64(Online),
+ }}
+ })
+ return mg
+}
+
+func getWebhookMetrics() *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 10 * time.Second,
+ }
+ mg.RegisterRead(func(ctx context.Context) []MetricV2 {
+ tgts := append(logger.SystemTargets(), logger.AuditTargets()...)
+ metrics := make([]MetricV2, 0, len(tgts)*4)
+ for _, t := range tgts {
+ isOnline := 0
+ if t.IsOnline(ctx) {
+ isOnline = 1
+ }
+ labels := map[string]string{
+ "name": t.String(),
+ "endpoint": t.Endpoint(),
+ }
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: webhookSubsystem,
+ Name: webhookOnline,
+ Help: "Is the webhook online?",
+ Type: gaugeMetric,
+ },
+ VariableLabels: labels,
+ Value: float64(isOnline),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: webhookSubsystem,
+ Name: webhookQueueLength,
+ Help: "Webhook queue length",
+ Type: gaugeMetric,
+ },
+ VariableLabels: labels,
+ Value: float64(t.Stats().QueueLength),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: webhookSubsystem,
+ Name: webhookTotalMessages,
+ Help: "Total number of messages sent to this target",
+ Type: counterMetric,
+ },
+ VariableLabels: labels,
+ Value: float64(t.Stats().TotalMessages),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: webhookSubsystem,
+ Name: webhookFailedMessages,
+ Help: "Number of messages that failed to send",
+ Type: counterMetric,
+ },
+ VariableLabels: labels,
+ Value: float64(t.Stats().FailedMessages),
+ })
+ }
+
+ return metrics
+ })
+ return mg
+}
+
+func getKMSMetrics(opts MetricsGroupOpts) *MetricsGroupV2 {
+ mg := &MetricsGroupV2{
+ cacheInterval: 10 * time.Second,
+ metricsGroupOpts: opts,
+ }
+
+ mg.RegisterRead(func(ctx context.Context) []MetricV2 {
+ metrics := make([]MetricV2, 0, 4)
+ metric, err := GlobalKMS.Metrics(ctx)
+ if err != nil {
+ return metrics
+ }
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: kmsSubsystem,
+ Name: kmsRequestsSuccess,
+ Help: "Number of KMS requests that succeeded",
+ Type: counterMetric,
+ },
+ Value: float64(metric.ReqOK),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: kmsSubsystem,
+ Name: kmsRequestsError,
+ Help: "Number of KMS requests that failed due to some error. (HTTP 4xx status code)",
+ Type: counterMetric,
+ },
+ Value: float64(metric.ReqErr),
+ })
+ metrics = append(metrics, MetricV2{
+ Description: MetricDescription{
+ Namespace: clusterMetricNamespace,
+ Subsystem: kmsSubsystem,
+ Name: kmsRequestsFail,
+ Help: "Number of KMS requests that failed due to some internal failure. (HTTP 5xx status code)",
+ Type: counterMetric,
+ },
+ Value: float64(metric.ReqFail),
+ })
+ return metrics
+ })
+ return mg
+}
+
+func collectMetric(metric MetricV2, labels []string, values []string, metricName string, out chan<- prometheus.Metric) {
+ if metric.Description.Type == histogramMetric {
+ if metric.Histogram == nil {
+ return
+ }
+ for k, v := range metric.Histogram {
+ pmetric, err := prometheus.NewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(string(metric.Description.Namespace),
+ string(metric.Description.Subsystem),
+ string(metric.Description.Name)),
+ metric.Description.Help,
+ append(labels, metric.HistogramBucketLabel),
+ metric.StaticLabels,
+ ),
+ prometheus.GaugeValue,
+ float64(v),
+ append(values, k)...)
+ if err != nil {
+ // Enable for debugging
+ if serverDebugLog {
+ bugLogIf(GlobalContext, fmt.Errorf("unable to validate prometheus metric (%w) %v+%v", err, values, metric.Histogram))
+ }
+ } else {
+ out <- pmetric
+ }
+ }
+ return
+ }
+ metricType := prometheus.GaugeValue
+ if metric.Description.Type == counterMetric {
+ metricType = prometheus.CounterValue
+ }
+ pmetric, err := prometheus.NewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(string(metric.Description.Namespace),
+ string(metric.Description.Subsystem),
+ string(metric.Description.Name)),
+ metric.Description.Help,
+ labels,
+ metric.StaticLabels,
+ ),
+ metricType,
+ metric.Value,
+ values...)
+ if err != nil {
+ // Enable for debugging
+ if serverDebugLog {
+ bugLogIf(GlobalContext, fmt.Errorf("unable to validate prometheus metric (%w) %v", err, values))
+ }
+ } else {
+ out <- pmetric
+ }
+}
+
+//msgp:ignore minioBucketCollector
+type minioBucketCollector struct {
+ metricsGroups []*MetricsGroupV2
+ desc *prometheus.Desc
+}
+
+func newMinioBucketCollector(metricsGroups []*MetricsGroupV2) *minioBucketCollector {
+ return &minioBucketCollector{
+ metricsGroups: metricsGroups,
+ desc: prometheus.NewDesc("minio_bucket_stats", "Statistics exposed by MinIO server cluster wide per bucket", nil, nil),
+ }
+}
+
+// Describe sends the super-set of all possible descriptors of metrics
+func (c *minioBucketCollector) Describe(ch chan<- *prometheus.Desc) {
+ ch <- c.desc
+}
+
+// Collect is called by the Prometheus registry when collecting metrics.
+func (c *minioBucketCollector) Collect(out chan<- prometheus.Metric) {
+ var wg sync.WaitGroup
+ publish := func(in <-chan MetricV2) {
+ defer wg.Done()
+ for metric := range in {
+ labels, values := getOrderedLabelValueArrays(metric.VariableLabels)
+ collectMetric(metric, labels, values, "bucket", out)
+ }
+ }
+
+ // Call peer api to fetch metrics
+ wg.Add(2)
+ go publish(ReportMetrics(GlobalContext, c.metricsGroups))
+ go publish(globalNotificationSys.GetBucketMetrics(GlobalContext))
+ wg.Wait()
+}
+
+//msgp:ignore minioClusterCollector
+type minioClusterCollector struct {
+ metricsGroups []*MetricsGroupV2
+ desc *prometheus.Desc
+}
+
+func newMinioClusterCollector(metricsGroups []*MetricsGroupV2) *minioClusterCollector {
+ return &minioClusterCollector{
+ metricsGroups: metricsGroups,
+ desc: prometheus.NewDesc("minio_stats", "Statistics exposed by MinIO server per cluster", nil, nil),
+ }
+}
+
+// Describe sends the super-set of all possible descriptors of metrics
+func (c *minioClusterCollector) Describe(ch chan<- *prometheus.Desc) {
+ ch <- c.desc
+}
+
+// Collect is called by the Prometheus registry when collecting metrics.
+func (c *minioClusterCollector) Collect(out chan<- prometheus.Metric) {
+ var wg sync.WaitGroup
+ publish := func(in <-chan MetricV2) {
+ defer wg.Done()
+ for metric := range in {
+ labels, values := getOrderedLabelValueArrays(metric.VariableLabels)
+ collectMetric(metric, labels, values, "cluster", out)
+ }
+ }
+
+ // Call peer api to fetch metrics
+ wg.Add(2)
+ go publish(ReportMetrics(GlobalContext, c.metricsGroups))
+ go publish(globalNotificationSys.GetClusterMetrics(GlobalContext))
+ wg.Wait()
+}
+
+// ReportMetrics reports serialized metrics to the channel passed for the metrics generated.
+func ReportMetrics(ctx context.Context, metricsGroups []*MetricsGroupV2) <-chan MetricV2 {
+ ch := make(chan MetricV2)
+ go func() {
+ defer xioutil.SafeClose(ch)
+ populateAndPublish(metricsGroups, func(m MetricV2) bool {
+ if m.VariableLabels == nil {
+ m.VariableLabels = make(map[string]string)
+ }
+ m.VariableLabels[serverName] = globalLocalNodeName
+ for {
+ select {
+ case ch <- m:
+ return true
+ case <-ctx.Done():
+ return false
+ }
+ }
+ })
+ }()
+ return ch
+}
+
+// minioNodeCollector is the Custom Collector
+//
+//msgp:ignore minioNodeCollector
+type minioNodeCollector struct {
+ metricsGroups []*MetricsGroupV2
+ desc *prometheus.Desc
+}
+
+// Describe sends the super-set of all possible descriptors of metrics
+func (c *minioNodeCollector) Describe(ch chan<- *prometheus.Desc) {
+ ch <- c.desc
+}
+
+// populateAndPublish populates and then publishes the metrics generated by the generator function.
+func populateAndPublish(metricsGroups []*MetricsGroupV2, publish func(m MetricV2) bool) {
+ for _, mg := range metricsGroups {
+ if mg == nil {
+ continue
+ }
+ for _, metric := range mg.Get() {
+ if !publish(metric) {
+ return
+ }
+ }
+ }
+}
+
+// Collect is called by the Prometheus registry when collecting metrics.
+func (c *minioNodeCollector) Collect(ch chan<- prometheus.Metric) {
+ // Expose MinIO's version information
+ minioVersionInfo.WithLabelValues(Version, CommitID).Set(1.0)
+
+ populateAndPublish(c.metricsGroups, func(metric MetricV2) bool {
+ labels, values := getOrderedLabelValueArrays(metric.VariableLabels)
+ values = append(values, globalLocalNodeName)
+ labels = append(labels, serverName)
+
+ if metric.Description.Type == histogramMetric {
+ if metric.Histogram == nil {
+ return true
+ }
+ for k, v := range metric.Histogram {
+ labels = append(labels, metric.HistogramBucketLabel)
+ values = append(values, k)
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(string(metric.Description.Namespace),
+ string(metric.Description.Subsystem),
+ string(metric.Description.Name)),
+ metric.Description.Help,
+ labels,
+ metric.StaticLabels,
+ ),
+ prometheus.GaugeValue,
+ float64(v),
+ values...)
+ }
+ return true
+ }
+
+ metricType := prometheus.GaugeValue
+ if metric.Description.Type == counterMetric {
+ metricType = prometheus.CounterValue
+ }
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(string(metric.Description.Namespace),
+ string(metric.Description.Subsystem),
+ string(metric.Description.Name)),
+ metric.Description.Help,
+ labels,
+ metric.StaticLabels,
+ ),
+ metricType,
+ metric.Value,
+ values...)
+ return true
+ })
+}
+
+func getOrderedLabelValueArrays(labelsWithValue map[string]string) (labels, values []string) {
+ labels = make([]string, 0, len(labelsWithValue))
+ values = make([]string, 0, len(labelsWithValue))
+ for l, v := range labelsWithValue {
+ labels = append(labels, l)
+ values = append(values, v)
+ }
+ return labels, values
+}
+
+// newMinioCollectorNode describes the collector
+// and returns reference of minioCollector for version 2
+// It creates the Prometheus Description which is used
+// to define Metric and help string
+func newMinioCollectorNode(metricsGroups []*MetricsGroupV2) *minioNodeCollector {
+ return &minioNodeCollector{
+ metricsGroups: metricsGroups,
+ desc: prometheus.NewDesc("minio_stats", "Statistics exposed by MinIO server per node", nil, nil),
+ }
+}
+
+func metricsHTTPHandler(c prometheus.Collector, funcName string) http.Handler {
+ registry := prometheus.NewRegistry()
+
+ // Report all other metrics
+ logger.CriticalIf(GlobalContext, registry.Register(c))
+
+ // DefaultGatherers include golang metrics and process metrics.
+ gatherers := prometheus.Gatherers{
+ registry,
+ }
+
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
+ if ok {
+ tc.FuncName = funcName
+ tc.ResponseRecorder.LogErrBody = true
+ }
+
+ mfs, err := gatherers.Gather()
+ if err != nil && len(mfs) == 0 {
+ writeErrorResponseJSON(r.Context(), w, toAdminAPIErr(r.Context(), err), r.URL)
+ return
+ }
+
+ contentType := expfmt.Negotiate(r.Header)
+ w.Header().Set("Content-Type", string(contentType))
+
+ enc := expfmt.NewEncoder(w, contentType)
+ for _, mf := range mfs {
+ if err := enc.Encode(mf); err != nil {
+ // client may disconnect for any reasons
+ // we do not have to log this.
+ return
+ }
+ }
+ if closer, ok := enc.(expfmt.Closer); ok {
+ closer.Close()
+ }
+ })
+}
+
+func metricsBucketHandler() http.Handler {
+ return metricsHTTPHandler(bucketCollector, "handler.MetricsBucket")
+}
+
+func metricsServerHandler() http.Handler {
+ registry := prometheus.NewRegistry()
+
+ // Report all other metrics
+ logger.CriticalIf(GlobalContext, registry.Register(clusterCollector))
+
+ // DefaultGatherers include golang metrics and process metrics.
+ gatherers := prometheus.Gatherers{
+ registry,
+ }
+
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
+ if ok {
+ tc.FuncName = "handler.MetricsCluster"
+ tc.ResponseRecorder.LogErrBody = true
+ }
+
+ mfs, err := gatherers.Gather()
+ if err != nil && len(mfs) == 0 {
+ writeErrorResponseJSON(r.Context(), w, toAdminAPIErr(r.Context(), err), r.URL)
+ return
+ }
+
+ contentType := expfmt.Negotiate(r.Header)
+ w.Header().Set("Content-Type", string(contentType))
+
+ enc := expfmt.NewEncoder(w, contentType)
+ for _, mf := range mfs {
+ if err := enc.Encode(mf); err != nil {
+ // client may disconnect for any reasons
+ // we do not have to log this.
+ return
+ }
+ }
+ if closer, ok := enc.(expfmt.Closer); ok {
+ closer.Close()
+ }
+ })
+}
+
+func metricsNodeHandler() http.Handler {
+ registry := prometheus.NewRegistry()
+
+ logger.CriticalIf(GlobalContext, registry.Register(nodeCollector))
+ if err := registry.Register(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{
+ Namespace: minioNamespace,
+ ReportErrors: true,
+ })); err != nil {
+ logger.CriticalIf(GlobalContext, err)
+ }
+ if err := registry.Register(prometheus.NewGoCollector()); err != nil {
+ logger.CriticalIf(GlobalContext, err)
+ }
+ gatherers := prometheus.Gatherers{
+ registry,
+ }
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
+ if ok {
+ tc.FuncName = "handler.MetricsNode"
+ tc.ResponseRecorder.LogErrBody = true
+ }
+
+ mfs, err := gatherers.Gather()
+ if err != nil {
+ if len(mfs) == 0 {
+ writeErrorResponseJSON(r.Context(), w, toAdminAPIErr(r.Context(), err), r.URL)
+ return
+ }
+ }
+
+ contentType := expfmt.Negotiate(r.Header)
+ w.Header().Set("Content-Type", string(contentType))
+
+ enc := expfmt.NewEncoder(w, contentType)
+ for _, mf := range mfs {
+ if err := enc.Encode(mf); err != nil {
+ metricsLogIf(r.Context(), err)
+ return
+ }
+ }
+ if closer, ok := enc.(expfmt.Closer); ok {
+ closer.Close()
+ }
+ })
+}
+
+func toSnake(camel string) (snake string) {
+ var b strings.Builder
+ l := len(camel)
+ for i, v := range camel {
+ // A is 65, a is 97
+ if v >= 'a' {
+ b.WriteRune(v)
+ continue
+ }
+ // v is capital letter here
+ // disregard first letter
+ // add underscore if last letter is capital letter
+ // add underscore when previous letter is lowercase
+ // add underscore when next letter is lowercase
+ if (i != 0 || i == l-1) && ((i > 0 && rune(camel[i-1]) >= 'a') ||
+ (i < l-1 && rune(camel[i+1]) >= 'a')) {
+ b.WriteRune('_')
+ }
+ b.WriteRune(v + 'a' - 'A')
+ }
+ return b.String()
+}
diff --git a/cmd/metrics-v2_gen.go b/cmd/metrics-v2_gen.go
new file mode 100644
index 00000000..9b13c4b8
--- /dev/null
+++ b/cmd/metrics-v2_gen.go
@@ -0,0 +1,638 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "github.com/tinylib/msgp/msgp"
+)
+
+// MarshalMsg implements msgp.Marshaler
+func (z *MetricDescription) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 5
+ // string "Namespace"
+ o = append(o, 0x85, 0xa9, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65)
+ o = msgp.AppendString(o, string(z.Namespace))
+ // string "Subsystem"
+ o = append(o, 0xa9, 0x53, 0x75, 0x62, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d)
+ o = msgp.AppendString(o, string(z.Subsystem))
+ // string "Name"
+ o = append(o, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
+ o = msgp.AppendString(o, string(z.Name))
+ // string "Help"
+ o = append(o, 0xa4, 0x48, 0x65, 0x6c, 0x70)
+ o = msgp.AppendString(o, z.Help)
+ // string "Type"
+ o = append(o, 0xa4, 0x54, 0x79, 0x70, 0x65)
+ o = msgp.AppendString(o, string(z.Type))
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *MetricDescription) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Namespace":
+ {
+ var zb0002 string
+ zb0002, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Namespace")
+ return
+ }
+ z.Namespace = MetricNamespace(zb0002)
+ }
+ case "Subsystem":
+ {
+ var zb0003 string
+ zb0003, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Subsystem")
+ return
+ }
+ z.Subsystem = MetricSubsystem(zb0003)
+ }
+ case "Name":
+ {
+ var zb0004 string
+ zb0004, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Name")
+ return
+ }
+ z.Name = MetricName(zb0004)
+ }
+ case "Help":
+ z.Help, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Help")
+ return
+ }
+ case "Type":
+ {
+ var zb0005 string
+ zb0005, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Type")
+ return
+ }
+ z.Type = MetricTypeV2(zb0005)
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *MetricDescription) Msgsize() (s int) {
+ s = 1 + 10 + msgp.StringPrefixSize + len(string(z.Namespace)) + 10 + msgp.StringPrefixSize + len(string(z.Subsystem)) + 5 + msgp.StringPrefixSize + len(string(z.Name)) + 5 + msgp.StringPrefixSize + len(z.Help) + 5 + msgp.StringPrefixSize + len(string(z.Type))
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z MetricName) 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 *MetricName) 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) = MetricName(zb0001)
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z MetricName) Msgsize() (s int) {
+ s = msgp.StringPrefixSize + len(string(z))
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z MetricNamespace) 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 *MetricNamespace) 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) = MetricNamespace(zb0001)
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z MetricNamespace) Msgsize() (s int) {
+ s = msgp.StringPrefixSize + len(string(z))
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z MetricSubsystem) 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 *MetricSubsystem) 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) = MetricSubsystem(zb0001)
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z MetricSubsystem) Msgsize() (s int) {
+ s = msgp.StringPrefixSize + len(string(z))
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z MetricTypeV2) 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 *MetricTypeV2) 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) = MetricTypeV2(zb0001)
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z MetricTypeV2) Msgsize() (s int) {
+ s = msgp.StringPrefixSize + len(string(z))
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *MetricV2) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 6
+ // string "Description"
+ o = append(o, 0x86, 0xab, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e)
+ o, err = z.Description.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Description")
+ return
+ }
+ // string "StaticLabels"
+ o = append(o, 0xac, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73)
+ o = msgp.AppendMapHeader(o, uint32(len(z.StaticLabels)))
+ for za0001, za0002 := range z.StaticLabels {
+ o = msgp.AppendString(o, za0001)
+ o = msgp.AppendString(o, za0002)
+ }
+ // string "Value"
+ o = append(o, 0xa5, 0x56, 0x61, 0x6c, 0x75, 0x65)
+ o = msgp.AppendFloat64(o, z.Value)
+ // string "VariableLabels"
+ o = append(o, 0xae, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73)
+ o = msgp.AppendMapHeader(o, uint32(len(z.VariableLabels)))
+ for za0003, za0004 := range z.VariableLabels {
+ o = msgp.AppendString(o, za0003)
+ o = msgp.AppendString(o, za0004)
+ }
+ // string "HistogramBucketLabel"
+ o = append(o, 0xb4, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x67, 0x72, 0x61, 0x6d, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x4c, 0x61, 0x62, 0x65, 0x6c)
+ o = msgp.AppendString(o, z.HistogramBucketLabel)
+ // string "Histogram"
+ o = append(o, 0xa9, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x67, 0x72, 0x61, 0x6d)
+ o = msgp.AppendMapHeader(o, uint32(len(z.Histogram)))
+ for za0005, za0006 := range z.Histogram {
+ o = msgp.AppendString(o, za0005)
+ o = msgp.AppendUint64(o, za0006)
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *MetricV2) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Description":
+ bts, err = z.Description.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Description")
+ return
+ }
+ case "StaticLabels":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "StaticLabels")
+ return
+ }
+ if z.StaticLabels == nil {
+ z.StaticLabels = make(map[string]string, zb0002)
+ } else if len(z.StaticLabels) > 0 {
+ clear(z.StaticLabels)
+ }
+ for zb0002 > 0 {
+ var za0002 string
+ zb0002--
+ var za0001 string
+ za0001, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "StaticLabels")
+ return
+ }
+ za0002, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "StaticLabels", za0001)
+ return
+ }
+ z.StaticLabels[za0001] = za0002
+ }
+ case "Value":
+ z.Value, bts, err = msgp.ReadFloat64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Value")
+ return
+ }
+ case "VariableLabels":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "VariableLabels")
+ return
+ }
+ if z.VariableLabels == nil {
+ z.VariableLabels = make(map[string]string, zb0003)
+ } else if len(z.VariableLabels) > 0 {
+ clear(z.VariableLabels)
+ }
+ for zb0003 > 0 {
+ var za0004 string
+ zb0003--
+ var za0003 string
+ za0003, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "VariableLabels")
+ return
+ }
+ za0004, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "VariableLabels", za0003)
+ return
+ }
+ z.VariableLabels[za0003] = za0004
+ }
+ case "HistogramBucketLabel":
+ z.HistogramBucketLabel, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "HistogramBucketLabel")
+ return
+ }
+ case "Histogram":
+ var zb0004 uint32
+ zb0004, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Histogram")
+ return
+ }
+ if z.Histogram == nil {
+ z.Histogram = make(map[string]uint64, zb0004)
+ } else if len(z.Histogram) > 0 {
+ clear(z.Histogram)
+ }
+ for zb0004 > 0 {
+ var za0006 uint64
+ zb0004--
+ var za0005 string
+ za0005, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Histogram")
+ return
+ }
+ za0006, bts, err = msgp.ReadUint64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Histogram", za0005)
+ return
+ }
+ z.Histogram[za0005] = za0006
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *MetricV2) Msgsize() (s int) {
+ s = 1 + 12 + z.Description.Msgsize() + 13 + msgp.MapHeaderSize
+ if z.StaticLabels != nil {
+ for za0001, za0002 := range z.StaticLabels {
+ _ = za0002
+ s += msgp.StringPrefixSize + len(za0001) + msgp.StringPrefixSize + len(za0002)
+ }
+ }
+ s += 6 + msgp.Float64Size + 15 + msgp.MapHeaderSize
+ if z.VariableLabels != nil {
+ for za0003, za0004 := range z.VariableLabels {
+ _ = za0004
+ s += msgp.StringPrefixSize + len(za0003) + msgp.StringPrefixSize + len(za0004)
+ }
+ }
+ s += 21 + msgp.StringPrefixSize + len(z.HistogramBucketLabel) + 10 + msgp.MapHeaderSize
+ if z.Histogram != nil {
+ for za0005, za0006 := range z.Histogram {
+ _ = za0006
+ s += msgp.StringPrefixSize + len(za0005) + msgp.Uint64Size
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *MetricsGroupOpts) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 12
+ // string "dependGlobalObjectAPI"
+ o = append(o, 0x8c, 0xb5, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x41, 0x50, 0x49)
+ o = msgp.AppendBool(o, z.dependGlobalObjectAPI)
+ // string "dependGlobalAuthNPlugin"
+ o = append(o, 0xb7, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x4e, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e)
+ o = msgp.AppendBool(o, z.dependGlobalAuthNPlugin)
+ // string "dependGlobalSiteReplicationSys"
+ o = append(o, 0xbe, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x79, 0x73)
+ o = msgp.AppendBool(o, z.dependGlobalSiteReplicationSys)
+ // string "dependGlobalNotificationSys"
+ o = append(o, 0xbb, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x79, 0x73)
+ o = msgp.AppendBool(o, z.dependGlobalNotificationSys)
+ // string "dependGlobalKMS"
+ o = append(o, 0xaf, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4b, 0x4d, 0x53)
+ o = msgp.AppendBool(o, z.dependGlobalKMS)
+ // string "bucketOnly"
+ o = append(o, 0xaa, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x79)
+ o = msgp.AppendBool(o, z.bucketOnly)
+ // string "dependGlobalLambdaTargetList"
+ o = append(o, 0xbc, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4c, 0x61, 0x6d, 0x62, 0x64, 0x61, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x4c, 0x69, 0x73, 0x74)
+ o = msgp.AppendBool(o, z.dependGlobalLambdaTargetList)
+ // string "dependGlobalIAMSys"
+ o = append(o, 0xb2, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x49, 0x41, 0x4d, 0x53, 0x79, 0x73)
+ o = msgp.AppendBool(o, z.dependGlobalIAMSys)
+ // string "dependGlobalLockServer"
+ o = append(o, 0xb6, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4c, 0x6f, 0x63, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72)
+ o = msgp.AppendBool(o, z.dependGlobalLockServer)
+ // string "dependGlobalIsDistErasure"
+ o = append(o, 0xb9, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x49, 0x73, 0x44, 0x69, 0x73, 0x74, 0x45, 0x72, 0x61, 0x73, 0x75, 0x72, 0x65)
+ o = msgp.AppendBool(o, z.dependGlobalIsDistErasure)
+ // string "dependGlobalBackgroundHealState"
+ o = append(o, 0xbf, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x42, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x65, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65)
+ o = msgp.AppendBool(o, z.dependGlobalBackgroundHealState)
+ // string "dependBucketTargetSys"
+ o = append(o, 0xb5, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x79, 0x73)
+ o = msgp.AppendBool(o, z.dependBucketTargetSys)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *MetricsGroupOpts) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "dependGlobalObjectAPI":
+ z.dependGlobalObjectAPI, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "dependGlobalObjectAPI")
+ return
+ }
+ case "dependGlobalAuthNPlugin":
+ z.dependGlobalAuthNPlugin, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "dependGlobalAuthNPlugin")
+ return
+ }
+ case "dependGlobalSiteReplicationSys":
+ z.dependGlobalSiteReplicationSys, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "dependGlobalSiteReplicationSys")
+ return
+ }
+ case "dependGlobalNotificationSys":
+ z.dependGlobalNotificationSys, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "dependGlobalNotificationSys")
+ return
+ }
+ case "dependGlobalKMS":
+ z.dependGlobalKMS, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "dependGlobalKMS")
+ return
+ }
+ case "bucketOnly":
+ z.bucketOnly, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "bucketOnly")
+ return
+ }
+ case "dependGlobalLambdaTargetList":
+ z.dependGlobalLambdaTargetList, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "dependGlobalLambdaTargetList")
+ return
+ }
+ case "dependGlobalIAMSys":
+ z.dependGlobalIAMSys, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "dependGlobalIAMSys")
+ return
+ }
+ case "dependGlobalLockServer":
+ z.dependGlobalLockServer, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "dependGlobalLockServer")
+ return
+ }
+ case "dependGlobalIsDistErasure":
+ z.dependGlobalIsDistErasure, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "dependGlobalIsDistErasure")
+ return
+ }
+ case "dependGlobalBackgroundHealState":
+ z.dependGlobalBackgroundHealState, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "dependGlobalBackgroundHealState")
+ return
+ }
+ case "dependBucketTargetSys":
+ z.dependBucketTargetSys, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "dependBucketTargetSys")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *MetricsGroupOpts) Msgsize() (s int) {
+ s = 1 + 22 + msgp.BoolSize + 24 + msgp.BoolSize + 31 + msgp.BoolSize + 28 + msgp.BoolSize + 16 + msgp.BoolSize + 11 + msgp.BoolSize + 29 + msgp.BoolSize + 19 + msgp.BoolSize + 23 + msgp.BoolSize + 26 + msgp.BoolSize + 32 + msgp.BoolSize + 22 + msgp.BoolSize
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *MetricsGroupV2) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 2
+ // string "cacheInterval"
+ o = append(o, 0x82, 0xad, 0x63, 0x61, 0x63, 0x68, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c)
+ o = msgp.AppendDuration(o, z.cacheInterval)
+ // string "metricsGroupOpts"
+ o = append(o, 0xb0, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x4f, 0x70, 0x74, 0x73)
+ o, err = z.metricsGroupOpts.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "metricsGroupOpts")
+ return
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *MetricsGroupV2) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "cacheInterval":
+ z.cacheInterval, bts, err = msgp.ReadDurationBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "cacheInterval")
+ return
+ }
+ case "metricsGroupOpts":
+ bts, err = z.metricsGroupOpts.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "metricsGroupOpts")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *MetricsGroupV2) Msgsize() (s int) {
+ s = 1 + 14 + msgp.DurationSize + 17 + z.metricsGroupOpts.Msgsize()
+ return
+}
diff --git a/cmd/metrics-v2_gen_test.go b/cmd/metrics-v2_gen_test.go
new file mode 100644
index 00000000..026c16d7
--- /dev/null
+++ b/cmd/metrics-v2_gen_test.go
@@ -0,0 +1,241 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "testing"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+func TestMarshalUnmarshalMetricDescription(t *testing.T) {
+ v := MetricDescription{}
+ 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 BenchmarkMarshalMsgMetricDescription(b *testing.B) {
+ v := MetricDescription{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgMetricDescription(b *testing.B) {
+ v := MetricDescription{}
+ 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 BenchmarkUnmarshalMetricDescription(b *testing.B) {
+ v := MetricDescription{}
+ 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 TestMarshalUnmarshalMetricV2(t *testing.T) {
+ v := MetricV2{}
+ 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 BenchmarkMarshalMsgMetricV2(b *testing.B) {
+ v := MetricV2{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgMetricV2(b *testing.B) {
+ v := MetricV2{}
+ 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 BenchmarkUnmarshalMetricV2(b *testing.B) {
+ v := MetricV2{}
+ 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 TestMarshalUnmarshalMetricsGroupOpts(t *testing.T) {
+ v := MetricsGroupOpts{}
+ 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 BenchmarkMarshalMsgMetricsGroupOpts(b *testing.B) {
+ v := MetricsGroupOpts{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgMetricsGroupOpts(b *testing.B) {
+ v := MetricsGroupOpts{}
+ 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 BenchmarkUnmarshalMetricsGroupOpts(b *testing.B) {
+ v := MetricsGroupOpts{}
+ 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 TestMarshalUnmarshalMetricsGroupV2(t *testing.T) {
+ v := MetricsGroupV2{}
+ 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 BenchmarkMarshalMsgMetricsGroupV2(b *testing.B) {
+ v := MetricsGroupV2{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgMetricsGroupV2(b *testing.B) {
+ v := MetricsGroupV2{}
+ 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 BenchmarkUnmarshalMetricsGroupV2(b *testing.B) {
+ v := MetricsGroupV2{}
+ 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)
+ }
+ }
+}
diff --git a/cmd/metrics-v2_test.go b/cmd/metrics-v2_test.go
new file mode 100644
index 00000000..a3994fd0
--- /dev/null
+++ b/cmd/metrics-v2_test.go
@@ -0,0 +1,215 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "slices"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+func TestGetHistogramMetrics_BucketCount(t *testing.T) {
+ histBuckets := []float64{0.05, 0.1, 0.25, 0.5, 0.75}
+ labels := []string{"GetObject", "PutObject", "CopyObject", "CompleteMultipartUpload"}
+ ttfbHist := prometheus.NewHistogramVec(
+ prometheus.HistogramOpts{
+ Name: "s3_ttfb_seconds",
+ Help: "Time taken by requests served by current MinIO server instance",
+ Buckets: histBuckets,
+ },
+ []string{"api"},
+ )
+ observations := []struct {
+ val float64
+ label string
+ }{
+ {
+ val: 0.02,
+ label: labels[0],
+ },
+ {
+ val: 0.07,
+ label: labels[1],
+ },
+ {
+ val: 0.11,
+ label: labels[1],
+ },
+ {
+ val: 0.19,
+ label: labels[1],
+ },
+ {
+ val: 0.31,
+ label: labels[1],
+ },
+ {
+ val: 0.61,
+ label: labels[3],
+ },
+ {
+ val: 0.79,
+ label: labels[2],
+ },
+ }
+ ticker := time.NewTicker(1 * time.Millisecond)
+ defer ticker.Stop()
+ for _, obs := range observations {
+ // Send observations once every 1ms, to simulate delay between
+ // observations. This is to test the channel based
+ // synchronization used internally.
+ <-ticker.C
+ ttfbHist.With(prometheus.Labels{"api": obs.label}).Observe(obs.val)
+ }
+
+ metrics := getHistogramMetrics(ttfbHist, getBucketTTFBDistributionMD(), false, false)
+ // additional labels for +Inf for all histogram metrics
+ if expPoints := len(labels) * (len(histBuckets) + 1); expPoints != len(metrics) {
+ t.Fatalf("Expected %v data points when toLowerAPILabels=false but got %v", expPoints, len(metrics))
+ }
+
+ metrics = getHistogramMetrics(ttfbHist, getBucketTTFBDistributionMD(), true, false)
+ // additional labels for +Inf for all histogram metrics
+ if expPoints := len(labels) * (len(histBuckets) + 1); expPoints != len(metrics) {
+ t.Fatalf("Expected %v data points when toLowerAPILabels=true but got %v", expPoints, len(metrics))
+ }
+}
+
+func TestGetHistogramMetrics_Values(t *testing.T) {
+ histBuckets := []float64{0.50, 5.00}
+ labels := []string{"PutObject", "CopyObject"}
+ ttfbHist := prometheus.NewHistogramVec(
+ prometheus.HistogramOpts{
+ Name: "s3_ttfb_seconds",
+ Help: "Time taken by requests served by current MinIO server instance",
+ Buckets: histBuckets,
+ },
+ []string{"api"},
+ )
+ observations := []struct {
+ val float64
+ label string
+ }{
+ {
+ val: 0.02,
+ label: labels[0],
+ },
+ {
+ val: 0.19,
+ label: labels[1],
+ },
+ {
+ val: 0.31,
+ label: labels[1],
+ },
+ {
+ val: 0.61,
+ label: labels[0],
+ },
+ {
+ val: 6.79,
+ label: labels[1],
+ },
+ }
+ ticker := time.NewTicker(1 * time.Millisecond)
+ defer ticker.Stop()
+ for _, obs := range observations {
+ // Send observations once every 1ms, to simulate delay between
+ // observations. This is to test the channel based
+ // synchronization used internally.
+ <-ticker.C
+ ttfbHist.With(prometheus.Labels{"api": obs.label}).Observe(obs.val)
+ }
+
+ // Accumulate regular-cased API label metrics for 'PutObject' for deeper verification
+ metrics := getHistogramMetrics(ttfbHist, getBucketTTFBDistributionMD(), false, false)
+ capitalPutObjects := make([]MetricV2, 0, len(histBuckets)+1)
+ for _, metric := range metrics {
+ if value := metric.VariableLabels["api"]; value == "PutObject" {
+ capitalPutObjects = append(capitalPutObjects, metric)
+ }
+ }
+ if expMetricsPerAPI := len(histBuckets) + 1; expMetricsPerAPI != len(capitalPutObjects) {
+ t.Fatalf("Expected %d api=PutObject metrics but got %d", expMetricsPerAPI, len(capitalPutObjects))
+ }
+
+ // Deterministic ordering
+ slices.SortFunc(capitalPutObjects, func(a MetricV2, b MetricV2) int {
+ le1 := a.VariableLabels["le"]
+ le2 := a.VariableLabels["le"]
+ return strings.Compare(le1, le2)
+ })
+ if le := capitalPutObjects[0].VariableLabels["le"]; le != "0.500" {
+ t.Errorf("Expected le='0.050' api=PutObject metrics but got '%v'", le)
+ }
+ if value := capitalPutObjects[0].Value; value != 1 {
+ t.Errorf("Expected le='0.050' api=PutObject value to be 1 but got '%v'", value)
+ }
+ if le := capitalPutObjects[1].VariableLabels["le"]; le != "5.000" {
+ t.Errorf("Expected le='5.000' api=PutObject metrics but got '%v'", le)
+ }
+ if value := capitalPutObjects[1].Value; value != 2 {
+ t.Errorf("Expected le='5.000' api=PutObject value to be 2 but got '%v'", value)
+ }
+ if le := capitalPutObjects[2].VariableLabels["le"]; le != "+Inf" {
+ t.Errorf("Expected le='+Inf' api=PutObject metrics but got '%v'", le)
+ }
+ if value := capitalPutObjects[2].Value; value != 2 {
+ t.Errorf("Expected le='+Inf' api=PutObject value to be 2 but got '%v'", value)
+ }
+
+ // Accumulate lower-cased API label metrics for 'copyobject' for deeper verification
+ metrics = getHistogramMetrics(ttfbHist, getBucketTTFBDistributionMD(), true, false)
+ lowerCopyObjects := make([]MetricV2, 0, len(histBuckets)+1)
+ for _, metric := range metrics {
+ if value := metric.VariableLabels["api"]; value == "copyobject" {
+ lowerCopyObjects = append(lowerCopyObjects, metric)
+ }
+ }
+ if expMetricsPerAPI := len(histBuckets) + 1; expMetricsPerAPI != len(lowerCopyObjects) {
+ t.Fatalf("Expected %d api=copyobject metrics but got %d", expMetricsPerAPI, len(lowerCopyObjects))
+ }
+
+ // Deterministic ordering
+ slices.SortFunc(lowerCopyObjects, func(a MetricV2, b MetricV2) int {
+ le1 := a.VariableLabels["le"]
+ le2 := a.VariableLabels["le"]
+ return strings.Compare(le1, le2)
+ })
+ if le := lowerCopyObjects[0].VariableLabels["le"]; le != "0.500" {
+ t.Errorf("Expected le='0.050' api=copyobject metrics but got '%v'", le)
+ }
+ if value := lowerCopyObjects[0].Value; value != 2 {
+ t.Errorf("Expected le='0.050' api=copyobject value to be 2 but got '%v'", value)
+ }
+ if le := lowerCopyObjects[1].VariableLabels["le"]; le != "5.000" {
+ t.Errorf("Expected le='5.000' api=copyobject metrics but got '%v'", le)
+ }
+ if value := lowerCopyObjects[1].Value; value != 2 {
+ t.Errorf("Expected le='5.000' api=copyobject value to be 2 but got '%v'", value)
+ }
+ if le := lowerCopyObjects[2].VariableLabels["le"]; le != "+Inf" {
+ t.Errorf("Expected le='+Inf' api=copyobject metrics but got '%v'", le)
+ }
+ if value := lowerCopyObjects[2].Value; value != 3 {
+ t.Errorf("Expected le='+Inf' api=copyobject value to be 3 but got '%v'", value)
+ }
+}
diff --git a/cmd/metrics-v3-api.go b/cmd/metrics-v3-api.go
new file mode 100644
index 00000000..07265f03
--- /dev/null
+++ b/cmd/metrics-v3-api.go
@@ -0,0 +1,224 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+
+ "github.com/minio/minio-go/v7/pkg/set"
+)
+
+const (
+ apiRejectedAuthTotal MetricName = "rejected_auth_total"
+ apiRejectedHeaderTotal MetricName = "rejected_header_total"
+ apiRejectedTimestampTotal MetricName = "rejected_timestamp_total"
+ apiRejectedInvalidTotal MetricName = "rejected_invalid_total"
+
+ apiRequestsWaitingTotal MetricName = "waiting_total"
+ apiRequestsIncomingTotal MetricName = "incoming_total"
+
+ apiRequestsInFlightTotal MetricName = "inflight_total"
+ apiRequestsTotal MetricName = "total"
+ apiRequestsErrorsTotal MetricName = "errors_total"
+ apiRequests5xxErrorsTotal MetricName = "5xx_errors_total"
+ apiRequests4xxErrorsTotal MetricName = "4xx_errors_total"
+ apiRequestsCanceledTotal MetricName = "canceled_total"
+
+ apiRequestsTTFBSecondsDistribution MetricName = "ttfb_seconds_distribution"
+
+ apiTrafficSentBytes MetricName = "traffic_sent_bytes"
+ apiTrafficRecvBytes MetricName = "traffic_received_bytes"
+)
+
+var (
+ apiRejectedAuthTotalMD = NewCounterMD(apiRejectedAuthTotal,
+ "Total number of requests rejected for auth failure", "type")
+ apiRejectedHeaderTotalMD = NewCounterMD(apiRejectedHeaderTotal,
+ "Total number of requests rejected for invalid header", "type")
+ apiRejectedTimestampTotalMD = NewCounterMD(apiRejectedTimestampTotal,
+ "Total number of requests rejected for invalid timestamp", "type")
+ apiRejectedInvalidTotalMD = NewCounterMD(apiRejectedInvalidTotal,
+ "Total number of invalid requests", "type")
+
+ apiRequestsWaitingTotalMD = NewGaugeMD(apiRequestsWaitingTotal,
+ "Total number of requests in the waiting queue", "type")
+ apiRequestsIncomingTotalMD = NewGaugeMD(apiRequestsIncomingTotal,
+ "Total number of incoming requests", "type")
+
+ apiRequestsInFlightTotalMD = NewGaugeMD(apiRequestsInFlightTotal,
+ "Total number of requests currently in flight", "name", "type")
+ apiRequestsTotalMD = NewCounterMD(apiRequestsTotal,
+ "Total number of requests", "name", "type")
+ apiRequestsErrorsTotalMD = NewCounterMD(apiRequestsErrorsTotal,
+ "Total number of requests with (4xx and 5xx) errors", "name", "type")
+ apiRequests5xxErrorsTotalMD = NewCounterMD(apiRequests5xxErrorsTotal,
+ "Total number of requests with 5xx errors", "name", "type")
+ apiRequests4xxErrorsTotalMD = NewCounterMD(apiRequests4xxErrorsTotal,
+ "Total number of requests with 4xx errors", "name", "type")
+ apiRequestsCanceledTotalMD = NewCounterMD(apiRequestsCanceledTotal,
+ "Total number of requests canceled by the client", "name", "type")
+
+ apiRequestsTTFBSecondsDistributionMD = NewCounterMD(apiRequestsTTFBSecondsDistribution,
+ "Distribution of time to first byte across API calls", "name", "type", "le")
+
+ apiTrafficSentBytesMD = NewCounterMD(apiTrafficSentBytes,
+ "Total number of bytes sent", "type")
+ apiTrafficRecvBytesMD = NewCounterMD(apiTrafficRecvBytes,
+ "Total number of bytes received", "type")
+)
+
+// loadAPIRequestsHTTPMetrics - reads S3 HTTP metrics.
+//
+// This is a `MetricsLoaderFn`.
+//
+// This includes node level S3 HTTP metrics.
+//
+// This function currently ignores `opts`.
+func loadAPIRequestsHTTPMetrics(ctx context.Context, m MetricValues, _ *metricsCache) error {
+ // Collect node level S3 HTTP metrics.
+ httpStats := globalHTTPStats.toServerHTTPStats(false)
+
+ // Currently we only collect S3 API related stats, so we set the "type"
+ // label to "s3".
+
+ m.Set(apiRejectedAuthTotal, float64(httpStats.TotalS3RejectedAuth), "type", "s3")
+ m.Set(apiRejectedTimestampTotal, float64(httpStats.TotalS3RejectedTime), "type", "s3")
+ m.Set(apiRejectedHeaderTotal, float64(httpStats.TotalS3RejectedHeader), "type", "s3")
+ m.Set(apiRejectedInvalidTotal, float64(httpStats.TotalS3RejectedInvalid), "type", "s3")
+ m.Set(apiRequestsWaitingTotal, float64(httpStats.S3RequestsInQueue), "type", "s3")
+ m.Set(apiRequestsIncomingTotal, float64(httpStats.S3RequestsIncoming), "type", "s3")
+
+ for name, value := range httpStats.CurrentS3Requests.APIStats {
+ m.Set(apiRequestsInFlightTotal, float64(value), "name", name, "type", "s3")
+ }
+ for name, value := range httpStats.TotalS3Requests.APIStats {
+ m.Set(apiRequestsTotal, float64(value), "name", name, "type", "s3")
+ }
+ for name, value := range httpStats.TotalS3Errors.APIStats {
+ m.Set(apiRequestsErrorsTotal, float64(value), "name", name, "type", "s3")
+ }
+ for name, value := range httpStats.TotalS35xxErrors.APIStats {
+ m.Set(apiRequests5xxErrorsTotal, float64(value), "name", name, "type", "s3")
+ }
+ for name, value := range httpStats.TotalS34xxErrors.APIStats {
+ m.Set(apiRequests4xxErrorsTotal, float64(value), "name", name, "type", "s3")
+ }
+ for name, value := range httpStats.TotalS3Canceled.APIStats {
+ m.Set(apiRequestsCanceledTotal, float64(value), "name", name, "type", "s3")
+ }
+ return nil
+}
+
+// loadAPIRequestsTTFBMetrics - loads S3 TTFB metrics.
+//
+// This is a `MetricsLoaderFn`.
+func loadAPIRequestsTTFBMetrics(ctx context.Context, m MetricValues, _ *metricsCache) error {
+ renameLabels := map[string]string{"api": "name"}
+ labelsFilter := map[string]set.StringSet{}
+ m.SetHistogram(apiRequestsTTFBSecondsDistribution, httpRequestsDuration, labelsFilter, renameLabels, nil,
+ "type", "s3")
+ return nil
+}
+
+// loadAPIRequestsNetworkMetrics - loads S3 network metrics.
+//
+// This is a `MetricsLoaderFn`.
+func loadAPIRequestsNetworkMetrics(ctx context.Context, m MetricValues, _ *metricsCache) error {
+ connStats := globalConnStats.toServerConnStats()
+ m.Set(apiTrafficSentBytes, float64(connStats.s3OutputBytes), "type", "s3")
+ m.Set(apiTrafficRecvBytes, float64(connStats.s3InputBytes), "type", "s3")
+ return nil
+}
+
+// Metric Descriptions for bucket level S3 metrics.
+var (
+ bucketAPITrafficSentBytesMD = NewCounterMD(apiTrafficSentBytes,
+ "Total number of bytes received for a bucket", "bucket", "type")
+ bucketAPITrafficRecvBytesMD = NewCounterMD(apiTrafficRecvBytes,
+ "Total number of bytes sent for a bucket", "bucket", "type")
+
+ bucketAPIRequestsInFlightMD = NewGaugeMD(apiRequestsInFlightTotal,
+ "Total number of requests currently in flight for a bucket", "bucket", "name", "type")
+ bucketAPIRequestsTotalMD = NewCounterMD(apiRequestsTotal,
+ "Total number of requests for a bucket", "bucket", "name", "type")
+ bucketAPIRequestsCanceledMD = NewCounterMD(apiRequestsCanceledTotal,
+ "Total number of requests canceled by the client for a bucket", "bucket", "name", "type")
+ bucketAPIRequests4xxErrorsMD = NewCounterMD(apiRequests4xxErrorsTotal,
+ "Total number of requests with 4xx errors for a bucket", "bucket", "name", "type")
+ bucketAPIRequests5xxErrorsMD = NewCounterMD(apiRequests5xxErrorsTotal,
+ "Total number of requests with 5xx errors for a bucket", "bucket", "name", "type")
+
+ bucketAPIRequestsTTFBSecondsDistributionMD = NewCounterMD(apiRequestsTTFBSecondsDistribution,
+ "Distribution of time to first byte across API calls for a bucket",
+ "bucket", "name", "le", "type")
+)
+
+// loadBucketAPIHTTPMetrics - loads bucket level S3 HTTP metrics.
+//
+// This is a `MetricsLoaderFn`.
+//
+// This includes bucket level S3 HTTP metrics and S3 network in/out metrics.
+func loadBucketAPIHTTPMetrics(ctx context.Context, m MetricValues, _ *metricsCache, buckets []string) error {
+ if len(buckets) == 0 {
+ return nil
+ }
+ for bucket, inOut := range globalBucketConnStats.getBucketS3InOutBytes(buckets) {
+ recvBytes := inOut.In
+ if recvBytes > 0 {
+ m.Set(apiTrafficSentBytes, float64(recvBytes), "bucket", bucket, "type", "s3")
+ }
+ sentBytes := inOut.Out
+ if sentBytes > 0 {
+ m.Set(apiTrafficRecvBytes, float64(sentBytes), "bucket", bucket, "type", "s3")
+ }
+
+ httpStats := globalBucketHTTPStats.load(bucket)
+ for k, v := range httpStats.currentS3Requests.Load(false) {
+ m.Set(apiRequestsInFlightTotal, float64(v), "bucket", bucket, "name", k, "type", "s3")
+ }
+
+ for k, v := range httpStats.totalS3Requests.Load(false) {
+ m.Set(apiRequestsTotal, float64(v), "bucket", bucket, "name", k, "type", "s3")
+ }
+
+ for k, v := range httpStats.totalS3Canceled.Load(false) {
+ m.Set(apiRequestsCanceledTotal, float64(v), "bucket", bucket, "name", k, "type", "s3")
+ }
+
+ for k, v := range httpStats.totalS34xxErrors.Load(false) {
+ m.Set(apiRequests4xxErrorsTotal, float64(v), "bucket", bucket, "name", k, "type", "s3")
+ }
+
+ for k, v := range httpStats.totalS35xxErrors.Load(false) {
+ m.Set(apiRequests5xxErrorsTotal, float64(v), "bucket", bucket, "name", k, "type", "s3")
+ }
+ }
+
+ return nil
+}
+
+// loadBucketAPITTFBMetrics - loads bucket S3 TTFB metrics.
+//
+// This is a `MetricsLoaderFn`.
+func loadBucketAPITTFBMetrics(ctx context.Context, m MetricValues, _ *metricsCache, buckets []string) error {
+ renameLabels := map[string]string{"api": "name"}
+ labelsFilter := map[string]set.StringSet{}
+ m.SetHistogram(apiRequestsTTFBSecondsDistribution, bucketHTTPRequestsDuration, labelsFilter, renameLabels,
+ buckets, "type", "s3")
+ return nil
+}
diff --git a/cmd/metrics-v3-audit.go b/cmd/metrics-v3-audit.go
new file mode 100644
index 00000000..a9f89796
--- /dev/null
+++ b/cmd/metrics-v3-audit.go
@@ -0,0 +1,57 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+
+ "github.com/minio/minio/internal/logger"
+)
+
+const (
+ auditFailedMessages = "failed_messages"
+ auditTargetQueueLength = "target_queue_length"
+ auditTotalMessages = "total_messages"
+ targetID = "target_id"
+)
+
+var (
+ auditFailedMessagesMD = NewCounterMD(auditFailedMessages,
+ "Total number of messages that failed to send since start",
+ targetID)
+ auditTargetQueueLengthMD = NewGaugeMD(auditTargetQueueLength,
+ "Number of unsent messages in queue for target",
+ targetID)
+ auditTotalMessagesMD = NewCounterMD(auditTotalMessages,
+ "Total number of messages sent since start",
+ targetID)
+)
+
+// loadAuditMetrics - `MetricsLoaderFn` for audit
+// such as failed messages and total messages.
+func loadAuditMetrics(_ context.Context, m MetricValues, c *metricsCache) error {
+ audit := logger.CurrentStats()
+ for id, st := range audit {
+ labels := []string{targetID, id}
+ m.Set(auditFailedMessages, float64(st.FailedMessages), labels...)
+ m.Set(auditTargetQueueLength, float64(st.QueueLength), labels...)
+ m.Set(auditTotalMessages, float64(st.TotalMessages), labels...)
+ }
+
+ return nil
+}
diff --git a/cmd/metrics-v3-bucket-replication.go b/cmd/metrics-v3-bucket-replication.go
new file mode 100644
index 00000000..5f16d32e
--- /dev/null
+++ b/cmd/metrics-v3-bucket-replication.go
@@ -0,0 +1,155 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+)
+
+const (
+ bucketReplLastHrFailedBytes = "last_hour_failed_bytes"
+ bucketReplLastHrFailedCount = "last_hour_failed_count"
+ bucketReplLastMinFailedBytes = "last_minute_failed_bytes"
+ bucketReplLastMinFailedCount = "last_minute_failed_count"
+ bucketReplLatencyMs = "latency_ms"
+ bucketReplProxiedDeleteTaggingRequestsTotal = "proxied_delete_tagging_requests_total"
+ bucketReplProxiedGetRequestsFailures = "proxied_get_requests_failures"
+ bucketReplProxiedGetRequestsTotal = "proxied_get_requests_total"
+ bucketReplProxiedGetTaggingRequestsFailures = "proxied_get_tagging_requests_failures"
+ bucketReplProxiedGetTaggingRequestsTotal = "proxied_get_tagging_requests_total"
+ bucketReplProxiedHeadRequestsFailures = "proxied_head_requests_failures"
+ bucketReplProxiedHeadRequestsTotal = "proxied_head_requests_total"
+ bucketReplProxiedPutTaggingRequestsFailures = "proxied_put_tagging_requests_failures"
+ bucketReplProxiedPutTaggingRequestsTotal = "proxied_put_tagging_requests_total"
+ bucketReplSentBytes = "sent_bytes"
+ bucketReplSentCount = "sent_count"
+ bucketReplTotalFailedBytes = "total_failed_bytes"
+ bucketReplTotalFailedCount = "total_failed_count"
+ bucketReplProxiedDeleteTaggingRequestsFailures = "proxied_delete_tagging_requests_failures"
+ bucketL = "bucket"
+ operationL = "operation"
+ targetArnL = "targetArn"
+)
+
+var (
+ bucketReplLastHrFailedBytesMD = NewGaugeMD(bucketReplLastHrFailedBytes,
+ "Total number of bytes failed at least once to replicate in the last hour on a bucket",
+ bucketL, targetArnL)
+ bucketReplLastHrFailedCountMD = NewGaugeMD(bucketReplLastHrFailedCount,
+ "Total number of objects which failed replication in the last hour on a bucket",
+ bucketL, targetArnL)
+ bucketReplLastMinFailedBytesMD = NewGaugeMD(bucketReplLastMinFailedBytes,
+ "Total number of bytes failed at least once to replicate in the last full minute on a bucket",
+ bucketL, targetArnL)
+ bucketReplLastMinFailedCountMD = NewGaugeMD(bucketReplLastMinFailedCount,
+ "Total number of objects which failed replication in the last full minute on a bucket",
+ bucketL, targetArnL)
+ bucketReplLatencyMsMD = NewGaugeMD(bucketReplLatencyMs,
+ "Replication latency on a bucket in milliseconds",
+ bucketL, operationL, rangeL, targetArnL)
+ bucketReplProxiedDeleteTaggingRequestsTotalMD = NewCounterMD(bucketReplProxiedDeleteTaggingRequestsTotal,
+ "Number of DELETE tagging requests proxied to replication target",
+ bucketL, targetArnL)
+ bucketReplProxiedGetRequestsFailuresMD = NewCounterMD(bucketReplProxiedGetRequestsFailures,
+ "Number of failures in GET requests proxied to replication target",
+ bucketL, targetArnL)
+ bucketReplProxiedGetRequestsTotalMD = NewCounterMD(bucketReplProxiedGetRequestsTotal,
+ "Number of GET requests proxied to replication target",
+ bucketL, targetArnL)
+ bucketReplProxiedGetTaggingRequestsFailuresMD = NewCounterMD(bucketReplProxiedGetTaggingRequestsFailures,
+ "Number of failures in GET tagging requests proxied to replication target",
+ bucketL, targetArnL)
+ bucketReplProxiedGetTaggingRequestsTotalMD = NewCounterMD(bucketReplProxiedGetTaggingRequestsTotal,
+ "Number of GET tagging requests proxied to replication target",
+ bucketL, targetArnL)
+ bucketReplProxiedHeadRequestsFailuresMD = NewCounterMD(bucketReplProxiedHeadRequestsFailures,
+ "Number of failures in HEAD requests proxied to replication target",
+ bucketL, targetArnL)
+ bucketReplProxiedHeadRequestsTotalMD = NewCounterMD(bucketReplProxiedHeadRequestsTotal,
+ "Number of HEAD requests proxied to replication target",
+ bucketL, targetArnL)
+ bucketReplProxiedPutTaggingRequestsFailuresMD = NewCounterMD(bucketReplProxiedPutTaggingRequestsFailures,
+ "Number of failures in PUT tagging requests proxied to replication target",
+ bucketL, targetArnL)
+ bucketReplProxiedPutTaggingRequestsTotalMD = NewCounterMD(bucketReplProxiedPutTaggingRequestsTotal,
+ "Number of PUT tagging requests proxied to replication target",
+ bucketL, targetArnL)
+ bucketReplSentBytesMD = NewCounterMD(bucketReplSentBytes,
+ "Total number of bytes replicated to the target",
+ bucketL, targetArnL)
+ bucketReplSentCountMD = NewCounterMD(bucketReplSentCount,
+ "Total number of objects replicated to the target",
+ bucketL, targetArnL)
+ bucketReplTotalFailedBytesMD = NewCounterMD(bucketReplTotalFailedBytes,
+ "Total number of bytes failed at least once to replicate since server start",
+ bucketL, targetArnL)
+ bucketReplTotalFailedCountMD = NewCounterMD(bucketReplTotalFailedCount,
+ "Total number of objects which failed replication since server start",
+ bucketL, targetArnL)
+ bucketReplProxiedDeleteTaggingRequestsFailuresMD = NewCounterMD(bucketReplProxiedDeleteTaggingRequestsFailures,
+ "Number of failures in DELETE tagging requests proxied to replication target",
+ bucketL, targetArnL)
+)
+
+// loadBucketReplicationMetrics - `BucketMetricsLoaderFn` for bucket replication metrics
+// such as latency and sent bytes.
+func loadBucketReplicationMetrics(ctx context.Context, m MetricValues, c *metricsCache, buckets []string) error {
+ if globalSiteReplicationSys.isEnabled() {
+ return nil
+ }
+
+ dataUsageInfo, err := c.dataUsageInfo.Get()
+ if err != nil {
+ metricsLogIf(ctx, err)
+ return nil
+ }
+
+ bucketReplStats := globalReplicationStats.Load().getAllLatest(dataUsageInfo.BucketsUsage)
+ for _, bucket := range buckets {
+ if s, ok := bucketReplStats[bucket]; ok {
+ stats := s.ReplicationStats
+ if stats.hasReplicationUsage() {
+ for arn, stat := range stats.Stats {
+ labels := []string{bucketL, bucket, targetArnL, arn}
+ m.Set(bucketReplLastHrFailedBytes, float64(stat.Failed.LastHour.Bytes), labels...)
+ m.Set(bucketReplLastHrFailedCount, float64(stat.Failed.LastHour.Count), labels...)
+ m.Set(bucketReplLastMinFailedBytes, float64(stat.Failed.LastMinute.Bytes), labels...)
+ m.Set(bucketReplLastMinFailedCount, float64(stat.Failed.LastMinute.Count), labels...)
+ m.Set(bucketReplProxiedDeleteTaggingRequestsTotal, float64(s.ProxyStats.RmvTagTotal), labels...)
+ m.Set(bucketReplProxiedGetRequestsFailures, float64(s.ProxyStats.GetFailedTotal), labels...)
+ m.Set(bucketReplProxiedGetRequestsTotal, float64(s.ProxyStats.GetTotal), labels...)
+ m.Set(bucketReplProxiedGetTaggingRequestsFailures, float64(s.ProxyStats.GetTagFailedTotal), labels...)
+ m.Set(bucketReplProxiedGetTaggingRequestsTotal, float64(s.ProxyStats.GetTagTotal), labels...)
+ m.Set(bucketReplProxiedHeadRequestsFailures, float64(s.ProxyStats.HeadFailedTotal), labels...)
+ m.Set(bucketReplProxiedHeadRequestsTotal, float64(s.ProxyStats.HeadTotal), labels...)
+ m.Set(bucketReplProxiedPutTaggingRequestsFailures, float64(s.ProxyStats.PutTagFailedTotal), labels...)
+ m.Set(bucketReplProxiedPutTaggingRequestsTotal, float64(s.ProxyStats.PutTagTotal), labels...)
+ m.Set(bucketReplSentCount, float64(stat.ReplicatedCount), labels...)
+ m.Set(bucketReplTotalFailedBytes, float64(stat.Failed.Totals.Bytes), labels...)
+ m.Set(bucketReplTotalFailedCount, float64(stat.Failed.Totals.Count), labels...)
+ m.Set(bucketReplProxiedDeleteTaggingRequestsFailures, float64(s.ProxyStats.RmvTagFailedTotal), labels...)
+ m.Set(bucketReplSentBytes, float64(stat.ReplicatedSize), labels...)
+
+ SetHistogramValues(m, bucketReplLatencyMs, stat.Latency.getUploadLatency(), bucketL, bucket, operationL, "upload", targetArnL, arn)
+ }
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/cmd/metrics-v3-cache.go b/cmd/metrics-v3-cache.go
new file mode 100644
index 00000000..e94eeef7
--- /dev/null
+++ b/cmd/metrics-v3-cache.go
@@ -0,0 +1,277 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "sync"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/cachevalue"
+)
+
+// metricsCache - cache for metrics.
+//
+// When serving metrics, this cache is passed to the MetricsLoaderFn.
+//
+// This cache is used for metrics that would result in network/storage calls.
+type metricsCache struct {
+ dataUsageInfo *cachevalue.Cache[DataUsageInfo]
+ esetHealthResult *cachevalue.Cache[HealthResult]
+ driveMetrics *cachevalue.Cache[storageMetrics]
+ memoryMetrics *cachevalue.Cache[madmin.MemInfo]
+ cpuMetrics *cachevalue.Cache[madmin.CPUMetrics]
+ clusterDriveMetrics *cachevalue.Cache[storageMetrics]
+ nodesUpDown *cachevalue.Cache[nodesOnline]
+}
+
+func newMetricsCache() *metricsCache {
+ return &metricsCache{
+ dataUsageInfo: newDataUsageInfoCache(),
+ esetHealthResult: newESetHealthResultCache(),
+ driveMetrics: newDriveMetricsCache(),
+ memoryMetrics: newMemoryMetricsCache(),
+ cpuMetrics: newCPUMetricsCache(),
+ clusterDriveMetrics: newClusterStorageInfoCache(),
+ nodesUpDown: newNodesUpDownCache(),
+ }
+}
+
+type nodesOnline struct {
+ Online, Offline int
+}
+
+func newNodesUpDownCache() *cachevalue.Cache[nodesOnline] {
+ loadNodesUpDown := func(ctx context.Context) (v nodesOnline, err error) {
+ v.Online, v.Offline = globalNotificationSys.GetPeerOnlineCount()
+ return v, err
+ }
+ return cachevalue.NewFromFunc(1*time.Minute,
+ cachevalue.Opts{ReturnLastGood: true},
+ loadNodesUpDown)
+}
+
+type driveIOStatMetrics struct {
+ readsPerSec float64
+ readsKBPerSec float64
+ readsAwait float64
+ writesPerSec float64
+ writesKBPerSec float64
+ writesAwait float64
+ percUtil float64
+}
+
+// storageMetrics - cached storage metrics.
+type storageMetrics struct {
+ storageInfo madmin.StorageInfo
+ ioStats map[string]driveIOStatMetrics
+ onlineDrives, offlineDrives, totalDrives int
+}
+
+func newDataUsageInfoCache() *cachevalue.Cache[DataUsageInfo] {
+ loadDataUsage := func(ctx context.Context) (u DataUsageInfo, err error) {
+ objLayer := newObjectLayerFn()
+ if objLayer == nil {
+ return u, err
+ }
+
+ // Collect cluster level object metrics.
+ u, err = loadDataUsageFromBackend(GlobalContext, objLayer)
+ return u, err
+ }
+ return cachevalue.NewFromFunc(1*time.Minute,
+ cachevalue.Opts{ReturnLastGood: true},
+ loadDataUsage)
+}
+
+func newESetHealthResultCache() *cachevalue.Cache[HealthResult] {
+ loadHealth := func(ctx context.Context) (r HealthResult, err error) {
+ objLayer := newObjectLayerFn()
+ if objLayer == nil {
+ return r, err
+ }
+
+ r = objLayer.Health(GlobalContext, HealthOptions{})
+ return r, err
+ }
+ return cachevalue.NewFromFunc(1*time.Minute,
+ cachevalue.Opts{ReturnLastGood: true},
+ loadHealth,
+ )
+}
+
+func getDiffStats(initialStats, currentStats madmin.DiskIOStats) madmin.DiskIOStats {
+ return madmin.DiskIOStats{
+ ReadIOs: currentStats.ReadIOs - initialStats.ReadIOs,
+ WriteIOs: currentStats.WriteIOs - initialStats.WriteIOs,
+ ReadSectors: currentStats.ReadSectors - initialStats.ReadSectors,
+ WriteSectors: currentStats.WriteSectors - initialStats.WriteSectors,
+ ReadTicks: currentStats.ReadTicks - initialStats.ReadTicks,
+ WriteTicks: currentStats.WriteTicks - initialStats.WriteTicks,
+ TotalTicks: currentStats.TotalTicks - initialStats.TotalTicks,
+ }
+}
+
+func getDriveIOStatMetrics(ioStats madmin.DiskIOStats, duration time.Duration) (m driveIOStatMetrics) {
+ durationSecs := duration.Seconds()
+
+ m.readsPerSec = float64(ioStats.ReadIOs) / durationSecs
+ m.readsKBPerSec = float64(ioStats.ReadSectors) * float64(sectorSize) / kib / durationSecs
+ if ioStats.ReadIOs > 0 {
+ m.readsAwait = float64(ioStats.ReadTicks) / float64(ioStats.ReadIOs)
+ }
+
+ m.writesPerSec = float64(ioStats.WriteIOs) / durationSecs
+ m.writesKBPerSec = float64(ioStats.WriteSectors) * float64(sectorSize) / kib / durationSecs
+ if ioStats.WriteIOs > 0 {
+ m.writesAwait = float64(ioStats.WriteTicks) / float64(ioStats.WriteIOs)
+ }
+
+ // TotalTicks is in milliseconds
+ m.percUtil = float64(ioStats.TotalTicks) * 100 / (durationSecs * 1000)
+
+ return m
+}
+
+func newDriveMetricsCache() *cachevalue.Cache[storageMetrics] {
+ var (
+ // prevDriveIOStats is used to calculate "per second"
+ // values for IOStat related disk metrics e.g. reads/sec.
+ prevDriveIOStats map[string]madmin.DiskIOStats
+ prevDriveIOStatsMu sync.RWMutex
+ prevDriveIOStatsRefreshedAt time.Time
+ )
+
+ loadDriveMetrics := func(ctx context.Context) (v storageMetrics, err error) {
+ objLayer := newObjectLayerFn()
+ if objLayer == nil {
+ return v, err
+ }
+
+ storageInfo := objLayer.LocalStorageInfo(GlobalContext, true)
+ onlineDrives, offlineDrives := getOnlineOfflineDisksStats(storageInfo.Disks)
+ totalDrives := onlineDrives.Merge(offlineDrives)
+
+ v = storageMetrics{
+ storageInfo: storageInfo,
+ onlineDrives: onlineDrives.Sum(),
+ offlineDrives: offlineDrives.Sum(),
+ totalDrives: totalDrives.Sum(),
+ ioStats: map[string]driveIOStatMetrics{},
+ }
+
+ currentStats := getCurrentDriveIOStats()
+ now := time.Now().UTC()
+
+ prevDriveIOStatsMu.Lock()
+ if prevDriveIOStats != nil {
+ duration := now.Sub(prevDriveIOStatsRefreshedAt)
+ if duration.Seconds() > 1 {
+ for d, cs := range currentStats {
+ if ps, found := prevDriveIOStats[d]; found {
+ v.ioStats[d] = getDriveIOStatMetrics(getDiffStats(ps, cs), duration)
+ }
+ }
+ }
+ }
+
+ prevDriveIOStats = currentStats
+ prevDriveIOStatsRefreshedAt = now
+ prevDriveIOStatsMu.Unlock()
+
+ return v, err
+ }
+
+ return cachevalue.NewFromFunc(1*time.Minute,
+ cachevalue.Opts{ReturnLastGood: true},
+ loadDriveMetrics)
+}
+
+func newCPUMetricsCache() *cachevalue.Cache[madmin.CPUMetrics] {
+ loadCPUMetrics := func(ctx context.Context) (v madmin.CPUMetrics, err error) {
+ types := madmin.MetricsCPU
+
+ m := collectLocalMetrics(types, collectMetricsOpts{
+ hosts: map[string]struct{}{
+ globalLocalNodeName: {},
+ },
+ })
+
+ for _, hm := range m.ByHost {
+ if hm.CPU != nil {
+ v = *hm.CPU
+ break
+ }
+ }
+
+ return v, err
+ }
+
+ return cachevalue.NewFromFunc(1*time.Minute,
+ cachevalue.Opts{ReturnLastGood: true},
+ loadCPUMetrics)
+}
+
+func newMemoryMetricsCache() *cachevalue.Cache[madmin.MemInfo] {
+ loadMemoryMetrics := func(ctx context.Context) (v madmin.MemInfo, err error) {
+ types := madmin.MetricsMem
+
+ m := collectLocalMetrics(types, collectMetricsOpts{
+ hosts: map[string]struct{}{
+ globalLocalNodeName: {},
+ },
+ })
+
+ for _, hm := range m.ByHost {
+ if hm.Mem != nil && len(hm.Mem.Info.Addr) > 0 {
+ v = hm.Mem.Info
+ break
+ }
+ }
+
+ return v, err
+ }
+
+ return cachevalue.NewFromFunc(1*time.Minute,
+ cachevalue.Opts{ReturnLastGood: true},
+ loadMemoryMetrics)
+}
+
+func newClusterStorageInfoCache() *cachevalue.Cache[storageMetrics] {
+ loadStorageInfo := func(ctx context.Context) (v storageMetrics, err error) {
+ objLayer := newObjectLayerFn()
+ if objLayer == nil {
+ return storageMetrics{}, nil
+ }
+ storageInfo := objLayer.StorageInfo(GlobalContext, true)
+ onlineDrives, offlineDrives := getOnlineOfflineDisksStats(storageInfo.Disks)
+ totalDrives := onlineDrives.Merge(offlineDrives)
+ v = storageMetrics{
+ storageInfo: storageInfo,
+ onlineDrives: onlineDrives.Sum(),
+ offlineDrives: offlineDrives.Sum(),
+ totalDrives: totalDrives.Sum(),
+ }
+ return v, err
+ }
+ return cachevalue.NewFromFunc(1*time.Minute,
+ cachevalue.Opts{ReturnLastGood: true},
+ loadStorageInfo,
+ )
+}
diff --git a/cmd/metrics-v3-cluster-config.go b/cmd/metrics-v3-cluster-config.go
new file mode 100644
index 00000000..9d96c6bb
--- /dev/null
+++ b/cmd/metrics-v3-cluster-config.go
@@ -0,0 +1,46 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import "context"
+
+const (
+ configRRSParity = "rrs_parity"
+ configStandardParity = "standard_parity"
+)
+
+var (
+ configRRSParityMD = NewGaugeMD(configRRSParity,
+ "Reduced redundancy storage class parity")
+ configStandardParityMD = NewGaugeMD(configStandardParity,
+ "Standard storage class parity")
+)
+
+// loadClusterConfigMetrics - `MetricsLoaderFn` for cluster config
+// such as standard and RRS parity.
+func loadClusterConfigMetrics(ctx context.Context, m MetricValues, c *metricsCache) error {
+ clusterDriveMetrics, err := c.clusterDriveMetrics.Get()
+ if err != nil {
+ metricsLogIf(ctx, err)
+ } else {
+ m.Set(configStandardParity, float64(clusterDriveMetrics.storageInfo.Backend.StandardSCParity))
+ m.Set(configRRSParity, float64(clusterDriveMetrics.storageInfo.Backend.RRSCParity))
+ }
+
+ return nil
+}
diff --git a/cmd/metrics-v3-cluster-erasure-set.go b/cmd/metrics-v3-cluster-erasure-set.go
new file mode 100644
index 00000000..04824c62
--- /dev/null
+++ b/cmd/metrics-v3-cluster-erasure-set.go
@@ -0,0 +1,117 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "strconv"
+)
+
+const (
+ erasureSetOverallWriteQuorum = "overall_write_quorum"
+ erasureSetOverallHealth = "overall_health"
+ erasureSetReadQuorum = "read_quorum"
+ erasureSetWriteQuorum = "write_quorum"
+ erasureSetOnlineDrivesCount = "online_drives_count"
+ erasureSetHealingDrivesCount = "healing_drives_count"
+ erasureSetHealth = "health"
+ erasureSetReadTolerance = "read_tolerance"
+ erasureSetWriteTolerance = "write_tolerance"
+ erasureSetReadHealth = "read_health"
+ erasureSetWriteHealth = "write_health"
+)
+
+const (
+ poolIDL = "pool_id"
+ setIDL = "set_id"
+)
+
+var (
+ erasureSetOverallWriteQuorumMD = NewGaugeMD(erasureSetOverallWriteQuorum,
+ "Overall write quorum across pools and sets")
+ erasureSetOverallHealthMD = NewGaugeMD(erasureSetOverallHealth,
+ "Overall health across pools and sets (1=healthy, 0=unhealthy)")
+ erasureSetReadQuorumMD = NewGaugeMD(erasureSetReadQuorum,
+ "Read quorum for the erasure set in a pool", poolIDL, setIDL)
+ erasureSetWriteQuorumMD = NewGaugeMD(erasureSetWriteQuorum,
+ "Write quorum for the erasure set in a pool", poolIDL, setIDL)
+ erasureSetOnlineDrivesCountMD = NewGaugeMD(erasureSetOnlineDrivesCount,
+ "Count of online drives in the erasure set in a pool", poolIDL, setIDL)
+ erasureSetHealingDrivesCountMD = NewGaugeMD(erasureSetHealingDrivesCount,
+ "Count of healing drives in the erasure set in a pool", poolIDL, setIDL)
+ erasureSetHealthMD = NewGaugeMD(erasureSetHealth,
+ "Health of the erasure set in a pool (1=healthy, 0=unhealthy)",
+ poolIDL, setIDL)
+ erasureSetReadToleranceMD = NewGaugeMD(erasureSetReadTolerance,
+ "No of drive failures that can be tolerated without disrupting read operations",
+ poolIDL, setIDL)
+ erasureSetWriteToleranceMD = NewGaugeMD(erasureSetWriteTolerance,
+ "No of drive failures that can be tolerated without disrupting write operations",
+ poolIDL, setIDL)
+ erasureSetReadHealthMD = NewGaugeMD(erasureSetReadHealth,
+ "Health of the erasure set in a pool for read operations (1=healthy, 0=unhealthy)",
+ poolIDL, setIDL)
+ erasureSetWriteHealthMD = NewGaugeMD(erasureSetWriteHealth,
+ "Health of the erasure set in a pool for write operations (1=healthy, 0=unhealthy)",
+ poolIDL, setIDL)
+)
+
+func b2f(v bool) float64 {
+ if v {
+ return 1
+ }
+ return 0
+}
+
+// loadClusterErasureSetMetrics - `MetricsLoaderFn` for cluster storage erasure
+// set metrics.
+func loadClusterErasureSetMetrics(ctx context.Context, m MetricValues, c *metricsCache) error {
+ result, _ := c.esetHealthResult.Get()
+
+ m.Set(erasureSetOverallWriteQuorum, float64(result.WriteQuorum))
+ m.Set(erasureSetOverallHealth, b2f(result.Healthy))
+
+ for _, h := range result.ESHealth {
+ poolLV := strconv.Itoa(h.PoolID)
+ setLV := strconv.Itoa(h.SetID)
+ labels := []string{poolIDL, poolLV, setIDL, setLV}
+ m.Set(erasureSetReadQuorum, float64(h.ReadQuorum), labels...)
+ m.Set(erasureSetWriteQuorum, float64(h.WriteQuorum), labels...)
+ m.Set(erasureSetOnlineDrivesCount, float64(h.HealthyDrives), labels...)
+ m.Set(erasureSetHealingDrivesCount, float64(h.HealingDrives), labels...)
+ m.Set(erasureSetHealth, b2f(h.Healthy), labels...)
+
+ readHealthy := true
+ readTolerance := float64(h.HealthyDrives - h.ReadQuorum)
+ if readTolerance < 0 {
+ readHealthy = false
+ }
+ m.Set(erasureSetReadTolerance, readTolerance, labels...)
+ m.Set(erasureSetReadHealth, b2f(readHealthy), labels...)
+
+ writeHealthy := true
+ writeTolerance := float64(h.HealthyDrives + h.HealingDrives - h.WriteQuorum)
+ if writeTolerance < 0 {
+ writeHealthy = false
+ }
+ m.Set(erasureSetWriteTolerance, writeTolerance, labels...)
+ m.Set(erasureSetWriteHealth, b2f(writeHealthy), labels...)
+ }
+
+ return nil
+}
diff --git a/cmd/metrics-v3-cluster-health.go b/cmd/metrics-v3-cluster-health.go
new file mode 100644
index 00000000..8636fc96
--- /dev/null
+++ b/cmd/metrics-v3-cluster-health.go
@@ -0,0 +1,109 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import "context"
+
+const (
+ healthDrivesOfflineCount = "drives_offline_count"
+ healthDrivesOnlineCount = "drives_online_count"
+ healthDrivesCount = "drives_count"
+)
+
+var (
+ healthDrivesOfflineCountMD = NewGaugeMD(healthDrivesOfflineCount,
+ "Count of offline drives in the cluster")
+ healthDrivesOnlineCountMD = NewGaugeMD(healthDrivesOnlineCount,
+ "Count of online drives in the cluster")
+ healthDrivesCountMD = NewGaugeMD(healthDrivesCount,
+ "Count of all drives in the cluster")
+)
+
+// loadClusterHealthDriveMetrics - `MetricsLoaderFn` for cluster storage drive metrics
+// such as online, offline and total drives.
+func loadClusterHealthDriveMetrics(ctx context.Context, m MetricValues,
+ c *metricsCache,
+) error {
+ clusterDriveMetrics, _ := c.clusterDriveMetrics.Get()
+
+ m.Set(healthDrivesOfflineCount, float64(clusterDriveMetrics.offlineDrives))
+ m.Set(healthDrivesOnlineCount, float64(clusterDriveMetrics.onlineDrives))
+ m.Set(healthDrivesCount, float64(clusterDriveMetrics.totalDrives))
+
+ return nil
+}
+
+const (
+ healthNodesOfflineCount = "nodes_offline_count"
+ healthNodesOnlineCount = "nodes_online_count"
+)
+
+var (
+ healthNodesOfflineCountMD = NewGaugeMD(healthNodesOfflineCount,
+ "Count of offline nodes in the cluster")
+ healthNodesOnlineCountMD = NewGaugeMD(healthNodesOnlineCount,
+ "Count of online nodes in the cluster")
+)
+
+// loadClusterHealthNodeMetrics - `MetricsLoaderFn` for cluster health node
+// metrics.
+func loadClusterHealthNodeMetrics(ctx context.Context, m MetricValues,
+ c *metricsCache,
+) error {
+ nodesUpDown, _ := c.nodesUpDown.Get()
+
+ m.Set(healthNodesOfflineCount, float64(nodesUpDown.Offline))
+ m.Set(healthNodesOnlineCount, float64(nodesUpDown.Online))
+
+ return nil
+}
+
+const (
+ healthCapacityRawTotalBytes = "capacity_raw_total_bytes"
+ healthCapacityRawFreeBytes = "capacity_raw_free_bytes"
+ healthCapacityUsableTotalBytes = "capacity_usable_total_bytes"
+ healthCapacityUsableFreeBytes = "capacity_usable_free_bytes"
+)
+
+var (
+ healthCapacityRawTotalBytesMD = NewGaugeMD(healthCapacityRawTotalBytes,
+ "Total cluster raw storage capacity in bytes")
+ healthCapacityRawFreeBytesMD = NewGaugeMD(healthCapacityRawFreeBytes,
+ "Total cluster raw storage free in bytes")
+ healthCapacityUsableTotalBytesMD = NewGaugeMD(healthCapacityUsableTotalBytes,
+ "Total cluster usable storage capacity in bytes")
+ healthCapacityUsableFreeBytesMD = NewGaugeMD(healthCapacityUsableFreeBytes,
+ "Total cluster usable storage free in bytes")
+)
+
+// loadClusterHealthCapacityMetrics - `MetricsLoaderFn` for cluster storage
+// capacity metrics.
+func loadClusterHealthCapacityMetrics(ctx context.Context, m MetricValues,
+ c *metricsCache,
+) error {
+ clusterDriveMetrics, _ := c.clusterDriveMetrics.Get()
+
+ storageInfo := clusterDriveMetrics.storageInfo
+
+ m.Set(healthCapacityRawTotalBytes, float64(GetTotalCapacity(storageInfo.Disks)))
+ m.Set(healthCapacityRawFreeBytes, float64(GetTotalCapacityFree(storageInfo.Disks)))
+ m.Set(healthCapacityUsableTotalBytes, float64(GetTotalUsableCapacity(storageInfo.Disks, storageInfo)))
+ m.Set(healthCapacityUsableFreeBytes, float64(GetTotalUsableCapacityFree(storageInfo.Disks, storageInfo)))
+
+ return nil
+}
diff --git a/cmd/metrics-v3-cluster-iam.go b/cmd/metrics-v3-cluster-iam.go
new file mode 100644
index 00000000..6689fe55
--- /dev/null
+++ b/cmd/metrics-v3-cluster-iam.go
@@ -0,0 +1,69 @@
+// Copyright (c) 2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "sync/atomic"
+ "time"
+)
+
+const (
+ lastSyncDurationMillis = "last_sync_duration_millis"
+ pluginAuthnServiceFailedRequestsMinute = "plugin_authn_service_failed_requests_minute"
+ pluginAuthnServiceLastFailSeconds = "plugin_authn_service_last_fail_seconds"
+ pluginAuthnServiceLastSuccSeconds = "plugin_authn_service_last_succ_seconds"
+ pluginAuthnServiceSuccAvgRttMsMinute = "plugin_authn_service_succ_avg_rtt_ms_minute"
+ pluginAuthnServiceSuccMaxRttMsMinute = "plugin_authn_service_succ_max_rtt_ms_minute"
+ pluginAuthnServiceTotalRequestsMinute = "plugin_authn_service_total_requests_minute"
+ sinceLastSyncMillis = "since_last_sync_millis"
+ syncFailures = "sync_failures"
+ syncSuccesses = "sync_successes"
+)
+
+var (
+ lastSyncDurationMillisMD = NewCounterMD(lastSyncDurationMillis, "Last successful IAM data sync duration in milliseconds")
+ pluginAuthnServiceFailedRequestsMinuteMD = NewCounterMD(pluginAuthnServiceFailedRequestsMinute, "When plugin authentication is configured, returns failed requests count in the last full minute")
+ pluginAuthnServiceLastFailSecondsMD = NewCounterMD(pluginAuthnServiceLastFailSeconds, "When plugin authentication is configured, returns time (in seconds) since the last failed request to the service")
+ pluginAuthnServiceLastSuccSecondsMD = NewCounterMD(pluginAuthnServiceLastSuccSeconds, "When plugin authentication is configured, returns time (in seconds) since the last successful request to the service")
+ pluginAuthnServiceSuccAvgRttMsMinuteMD = NewCounterMD(pluginAuthnServiceSuccAvgRttMsMinute, "When plugin authentication is configured, returns average round-trip-time of successful requests in the last full minute")
+ pluginAuthnServiceSuccMaxRttMsMinuteMD = NewCounterMD(pluginAuthnServiceSuccMaxRttMsMinute, "When plugin authentication is configured, returns maximum round-trip-time of successful requests in the last full minute")
+ pluginAuthnServiceTotalRequestsMinuteMD = NewCounterMD(pluginAuthnServiceTotalRequestsMinute, "When plugin authentication is configured, returns total requests count in the last full minute")
+ sinceLastSyncMillisMD = NewCounterMD(sinceLastSyncMillis, "Time (in milliseconds) since last successful IAM data sync.")
+ syncFailuresMD = NewCounterMD(syncFailures, "Number of failed IAM data syncs since server start.")
+ syncSuccessesMD = NewCounterMD(syncSuccesses, "Number of successful IAM data syncs since server start.")
+)
+
+// loadClusterIAMMetrics - `MetricsLoaderFn` for cluster IAM metrics.
+func loadClusterIAMMetrics(_ context.Context, m MetricValues, _ *metricsCache) error {
+ m.Set(lastSyncDurationMillis, float64(atomic.LoadUint64(&globalIAMSys.LastRefreshDurationMilliseconds)))
+ pluginAuthNMetrics := globalAuthNPlugin.Metrics()
+ m.Set(pluginAuthnServiceFailedRequestsMinute, float64(pluginAuthNMetrics.FailedRequests))
+ m.Set(pluginAuthnServiceLastFailSeconds, pluginAuthNMetrics.LastUnreachableSecs)
+ m.Set(pluginAuthnServiceLastSuccSeconds, pluginAuthNMetrics.LastReachableSecs)
+ m.Set(pluginAuthnServiceSuccAvgRttMsMinute, pluginAuthNMetrics.AvgSuccRTTMs)
+ m.Set(pluginAuthnServiceSuccMaxRttMsMinute, pluginAuthNMetrics.MaxSuccRTTMs)
+ m.Set(pluginAuthnServiceTotalRequestsMinute, float64(pluginAuthNMetrics.TotalRequests))
+ lastSyncTime := atomic.LoadUint64(&globalIAMSys.LastRefreshTimeUnixNano)
+ if lastSyncTime != 0 {
+ m.Set(sinceLastSyncMillis, float64((uint64(time.Now().UnixNano())-lastSyncTime)/uint64(time.Millisecond)))
+ }
+ m.Set(syncFailures, float64(atomic.LoadUint64(&globalIAMSys.TotalRefreshFailures)))
+ m.Set(syncSuccesses, float64(atomic.LoadUint64(&globalIAMSys.TotalRefreshSuccesses)))
+ return nil
+}
diff --git a/cmd/metrics-v3-cluster-notification.go b/cmd/metrics-v3-cluster-notification.go
new file mode 100644
index 00000000..f4d76b62
--- /dev/null
+++ b/cmd/metrics-v3-cluster-notification.go
@@ -0,0 +1,51 @@
+// Copyright (c) 2024 MinIO, Inc.
+//
+// # This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+)
+
+const (
+ notificationCurrentSendInProgress = "current_send_in_progress"
+ notificationEventsErrorsTotal = "events_errors_total"
+ notificationEventsSentTotal = "events_sent_total"
+ notificationEventsSkippedTotal = "events_skipped_total"
+)
+
+var (
+ notificationCurrentSendInProgressMD = NewCounterMD(notificationCurrentSendInProgress, "Number of concurrent async Send calls active to all targets")
+ notificationEventsErrorsTotalMD = NewCounterMD(notificationEventsErrorsTotal, "Events that were failed to be sent to the targets")
+ notificationEventsSentTotalMD = NewCounterMD(notificationEventsSentTotal, "Total number of events sent to the targets")
+ notificationEventsSkippedTotalMD = NewCounterMD(notificationEventsSkippedTotal, "Events that were skipped to be sent to the targets due to the in-memory queue being full")
+)
+
+// loadClusterNotificationMetrics - `MetricsLoaderFn` for cluster notification metrics.
+func loadClusterNotificationMetrics(_ context.Context, m MetricValues, _ *metricsCache) error {
+ if globalEventNotifier == nil {
+ return nil
+ }
+
+ nstats := globalEventNotifier.targetList.Stats()
+ m.Set(notificationCurrentSendInProgress, float64(nstats.CurrentSendCalls))
+ m.Set(notificationEventsErrorsTotal, float64(nstats.EventsErrorsTotal))
+ m.Set(notificationEventsSentTotal, float64(nstats.TotalEvents))
+ m.Set(notificationEventsSkippedTotal, float64(nstats.EventsSkipped))
+
+ return nil
+}
diff --git a/cmd/metrics-v3-cluster-usage.go b/cmd/metrics-v3-cluster-usage.go
new file mode 100644
index 00000000..38dc0aef
--- /dev/null
+++ b/cmd/metrics-v3-cluster-usage.go
@@ -0,0 +1,182 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "time"
+)
+
+const (
+ usageSinceLastUpdateSeconds = "since_last_update_seconds"
+ usageTotalBytes = "total_bytes"
+ usageObjectsCount = "count"
+ usageVersionsCount = "versions_count"
+ usageDeleteMarkersCount = "delete_markers_count"
+ usageBucketsCount = "buckets_count"
+ usageSizeDistribution = "size_distribution"
+ usageVersionCountDistribution = "version_count_distribution"
+)
+
+var (
+ usageSinceLastUpdateSecondsMD = NewGaugeMD(usageSinceLastUpdateSeconds,
+ "Time since last update of usage metrics in seconds")
+ usageTotalBytesMD = NewGaugeMD(usageTotalBytes,
+ "Total cluster usage in bytes")
+ usageObjectsCountMD = NewGaugeMD(usageObjectsCount,
+ "Total cluster objects count")
+ usageVersionsCountMD = NewGaugeMD(usageVersionsCount,
+ "Total cluster object versions (including delete markers) count")
+ usageDeleteMarkersCountMD = NewGaugeMD(usageDeleteMarkersCount,
+ "Total cluster delete markers count")
+ usageBucketsCountMD = NewGaugeMD(usageBucketsCount,
+ "Total cluster buckets count")
+ usageObjectsDistributionMD = NewGaugeMD(usageSizeDistribution,
+ "Cluster object size distribution", "range")
+ usageVersionsDistributionMD = NewGaugeMD(usageVersionCountDistribution,
+ "Cluster object version count distribution", "range")
+)
+
+// loadClusterUsageObjectMetrics - reads cluster usage metrics.
+//
+// This is a `MetricsLoaderFn`.
+func loadClusterUsageObjectMetrics(ctx context.Context, m MetricValues, c *metricsCache) error {
+ dataUsageInfo, err := c.dataUsageInfo.Get()
+ if err != nil {
+ metricsLogIf(ctx, err)
+ return nil
+ }
+
+ // data usage has not captured any data yet.
+ if dataUsageInfo.LastUpdate.IsZero() {
+ return nil
+ }
+
+ var (
+ clusterSize uint64
+ clusterBuckets uint64
+ clusterObjectsCount uint64
+ clusterVersionsCount uint64
+ clusterDeleteMarkersCount uint64
+ )
+
+ clusterObjectSizesHistogram := map[string]uint64{}
+ clusterVersionsHistogram := map[string]uint64{}
+ for _, usage := range dataUsageInfo.BucketsUsage {
+ clusterBuckets++
+ clusterSize += usage.Size
+ clusterObjectsCount += usage.ObjectsCount
+ clusterVersionsCount += usage.VersionsCount
+ clusterDeleteMarkersCount += usage.DeleteMarkersCount
+ for k, v := range usage.ObjectSizesHistogram {
+ clusterObjectSizesHistogram[k] += v
+ }
+ for k, v := range usage.ObjectVersionsHistogram {
+ clusterVersionsHistogram[k] += v
+ }
+ }
+
+ m.Set(usageSinceLastUpdateSeconds, time.Since(dataUsageInfo.LastUpdate).Seconds())
+ m.Set(usageTotalBytes, float64(clusterSize))
+ m.Set(usageObjectsCount, float64(clusterObjectsCount))
+ m.Set(usageVersionsCount, float64(clusterVersionsCount))
+ m.Set(usageDeleteMarkersCount, float64(clusterDeleteMarkersCount))
+ m.Set(usageBucketsCount, float64(clusterBuckets))
+ for k, v := range clusterObjectSizesHistogram {
+ m.Set(usageSizeDistribution, float64(v), "range", k)
+ }
+ for k, v := range clusterVersionsHistogram {
+ m.Set(usageVersionCountDistribution, float64(v), "range", k)
+ }
+
+ return nil
+}
+
+const (
+ usageBucketQuotaTotalBytes = "quota_total_bytes"
+
+ usageBucketTotalBytes = "total_bytes"
+ usageBucketObjectsCount = "objects_count"
+ usageBucketVersionsCount = "versions_count"
+ usageBucketDeleteMarkersCount = "delete_markers_count"
+ usageBucketObjectSizeDistribution = "object_size_distribution"
+ usageBucketObjectVersionCountDistribution = "object_version_count_distribution"
+)
+
+var (
+ usageBucketTotalBytesMD = NewGaugeMD(usageBucketTotalBytes,
+ "Total bucket size in bytes", "bucket")
+ usageBucketObjectsTotalMD = NewGaugeMD(usageBucketObjectsCount,
+ "Total objects count in bucket", "bucket")
+ usageBucketVersionsCountMD = NewGaugeMD(usageBucketVersionsCount,
+ "Total object versions (including delete markers) count in bucket", "bucket")
+ usageBucketDeleteMarkersCountMD = NewGaugeMD(usageBucketDeleteMarkersCount,
+ "Total delete markers count in bucket", "bucket")
+
+ usageBucketQuotaTotalBytesMD = NewGaugeMD(usageBucketQuotaTotalBytes,
+ "Total bucket quota in bytes", "bucket")
+
+ usageBucketObjectSizeDistributionMD = NewGaugeMD(usageBucketObjectSizeDistribution,
+ "Bucket object size distribution", "range", "bucket")
+ usageBucketObjectVersionCountDistributionMD = NewGaugeMD(
+ usageBucketObjectVersionCountDistribution,
+ "Bucket object version count distribution", "range", "bucket")
+)
+
+// loadClusterUsageBucketMetrics - `MetricsLoaderFn` to load bucket usage metrics.
+func loadClusterUsageBucketMetrics(ctx context.Context, m MetricValues, c *metricsCache) error {
+ dataUsageInfo, err := c.dataUsageInfo.Get()
+ if err != nil {
+ metricsLogIf(ctx, err)
+ return nil
+ }
+
+ // data usage has not been captured yet.
+ if dataUsageInfo.LastUpdate.IsZero() {
+ return nil
+ }
+
+ m.Set(usageSinceLastUpdateSeconds, float64(time.Since(dataUsageInfo.LastUpdate)))
+
+ for bucket, usage := range dataUsageInfo.BucketsUsage {
+ quota, err := globalBucketQuotaSys.Get(ctx, bucket)
+ if err != nil {
+ // Log and continue if we are unable to retrieve metrics for this
+ // bucket.
+ metricsLogIf(ctx, err)
+ continue
+ }
+
+ m.Set(usageBucketTotalBytes, float64(usage.Size), "bucket", bucket)
+ m.Set(usageBucketObjectsCount, float64(usage.ObjectsCount), "bucket", bucket)
+ m.Set(usageBucketVersionsCount, float64(usage.VersionsCount), "bucket", bucket)
+ m.Set(usageBucketDeleteMarkersCount, float64(usage.DeleteMarkersCount), "bucket", bucket)
+
+ if quota != nil && quota.Quota > 0 {
+ m.Set(usageBucketQuotaTotalBytes, float64(quota.Quota), "bucket", bucket)
+ }
+
+ for k, v := range usage.ObjectSizesHistogram {
+ m.Set(usageBucketObjectSizeDistribution, float64(v), "range", k, "bucket", bucket)
+ }
+ for k, v := range usage.ObjectVersionsHistogram {
+ m.Set(usageBucketObjectVersionCountDistribution, float64(v), "range", k, "bucket", bucket)
+ }
+ }
+ return nil
+}
diff --git a/cmd/metrics-v3-handler.go b/cmd/metrics-v3-handler.go
new file mode 100644
index 00000000..7f07f58e
--- /dev/null
+++ b/cmd/metrics-v3-handler.go
@@ -0,0 +1,251 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "slices"
+ "strings"
+ "sync"
+
+ "github.com/minio/minio/internal/config"
+ "github.com/minio/minio/internal/mcontext"
+ "github.com/minio/mux"
+ "github.com/minio/pkg/v3/env"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
+)
+
+type promLogger struct{}
+
+func (p promLogger) Println(v ...any) {
+ metricsLogIf(GlobalContext, fmt.Errorf("metrics handler error: %v", v))
+}
+
+type metricsV3Server struct {
+ registry *prometheus.Registry
+ opts promhttp.HandlerOpts
+ auth func(http.Handler) http.Handler
+
+ metricsData *metricsV3Collection
+}
+
+var (
+ globalMetricsV3CollectorPaths []collectorPath
+ globalMetricsV3Once sync.Once
+)
+
+func newMetricsV3Server(auth func(h http.Handler) http.Handler) *metricsV3Server {
+ registry := prometheus.NewRegistry()
+ metricGroups := newMetricGroups(registry)
+ globalMetricsV3Once.Do(func() {
+ globalMetricsV3CollectorPaths = metricGroups.collectorPaths
+ })
+ return &metricsV3Server{
+ registry: registry,
+ opts: promhttp.HandlerOpts{
+ ErrorLog: promLogger{},
+ ErrorHandling: promhttp.ContinueOnError,
+ Registry: registry,
+ MaxRequestsInFlight: 2,
+ EnableOpenMetrics: env.Get(EnvPrometheusOpenMetrics, config.EnableOff) == config.EnableOn,
+ ProcessStartTime: globalBootTime,
+ },
+ auth: auth,
+ metricsData: metricGroups,
+ }
+}
+
+// metricDisplay - contains info on a metric for display purposes.
+type metricDisplay struct {
+ Name string `json:"name"`
+ Help string `json:"help"`
+ Type string `json:"type"`
+ Labels []string `json:"labels"`
+}
+
+func (md metricDisplay) String() string {
+ return fmt.Sprintf("Name: %s\nType: %s\nHelp: %s\nLabels: {%s}\n", md.Name, md.Type, md.Help, strings.Join(md.Labels, ","))
+}
+
+func (md metricDisplay) TableRow() string {
+ labels := strings.Join(md.Labels, ",")
+ if labels == "" {
+ labels = ""
+ } else {
+ labels = "`" + labels + "`"
+ }
+ return fmt.Sprintf("| `%s` | `%s` | %s | %s |\n", md.Name, md.Type, md.Help, labels)
+}
+
+// listMetrics - returns a handler that lists all the metrics that could be
+// returned for the requested path.
+//
+// FIXME: It currently only lists `minio_` prefixed metrics.
+func (h *metricsV3Server) listMetrics(path string) http.Handler {
+ // First collect all matching MetricsGroup's
+ matchingMG := make(map[collectorPath]*MetricsGroup)
+ for _, collPath := range h.metricsData.collectorPaths {
+ if collPath.isDescendantOf(path) {
+ if v, ok := h.metricsData.mgMap[collPath]; ok {
+ matchingMG[collPath] = v
+ } else if v, ok := h.metricsData.bucketMGMap[collPath]; ok {
+ matchingMG[collPath] = v
+ }
+ }
+ }
+
+ if len(matchingMG) == 0 {
+ return nil
+ }
+
+ var metrics []metricDisplay
+ for _, collectorPath := range h.metricsData.collectorPaths {
+ if mg, ok := matchingMG[collectorPath]; ok {
+ var commonLabels []string
+ for k := range mg.ExtraLabels {
+ commonLabels = append(commonLabels, k)
+ }
+ for _, d := range mg.Descriptors {
+ labels := slices.Clone(d.VariableLabels)
+ labels = append(labels, commonLabels...)
+ metric := metricDisplay{
+ Name: mg.MetricFQN(d.Name),
+ Help: d.Help,
+ Type: d.Type.String(),
+ Labels: labels,
+ }
+ metrics = append(metrics, metric)
+ }
+ }
+ }
+
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ contentType := r.Header.Get("Content-Type")
+ if contentType == "application/json" {
+ w.Header().Set("Content-Type", "application/json")
+ jsonEncoder := json.NewEncoder(w)
+ jsonEncoder.Encode(metrics)
+ return
+ }
+
+ // If not JSON, return plain text. We format it as a markdown table for
+ // readability.
+ w.Header().Set("Content-Type", "text/plain")
+ var b strings.Builder
+ b.WriteString("| Name | Type | Help | Labels |\n")
+ b.WriteString("| ---- | ---- | ---- | ------ |\n")
+ for _, metric := range metrics {
+ b.WriteString(metric.TableRow())
+ }
+ w.Write([]byte(b.String()))
+ })
+}
+
+func (h *metricsV3Server) handle(path string, isListingRequest bool, buckets []string) http.Handler {
+ var notFoundHandler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ http.Error(w, "Metrics Resource Not found", http.StatusNotFound)
+ })
+
+ // Require that metrics path has one component at least.
+ if path == "/" {
+ return notFoundHandler
+ }
+
+ if isListingRequest {
+ handler := h.listMetrics(path)
+ if handler == nil {
+ return notFoundHandler
+ }
+ return handler
+ }
+
+ // In each of the following cases, we check if the collect path is a
+ // descendant of `path`, and if so, we add the corresponding gatherer to
+ // the list of gatherers. This way, /api/a will return all metrics returned
+ // by /api/a/b and /api/a/c (and any other matching descendant collector
+ // paths).
+
+ var gatherers []prometheus.Gatherer
+ for _, collectorPath := range h.metricsData.collectorPaths {
+ if collectorPath.isDescendantOf(path) {
+ gatherer := h.metricsData.mgGatherers[collectorPath]
+
+ // For Bucket metrics we need to set the buckets argument inside the
+ // metric group, so that it will affect collection. If no buckets
+ // are provided, we will not return bucket metrics.
+ if bmg, ok := h.metricsData.bucketMGMap[collectorPath]; ok {
+ if len(buckets) == 0 {
+ continue
+ }
+ unLocker := bmg.LockAndSetBuckets(buckets)
+ defer unLocker()
+ }
+ gatherers = append(gatherers, gatherer)
+ }
+ }
+
+ if len(gatherers) == 0 {
+ return notFoundHandler
+ }
+
+ return promhttp.HandlerFor(prometheus.Gatherers(gatherers), h.opts)
+}
+
+// ServeHTTP - implements http.Handler interface.
+//
+// When the `list` query parameter is provided (its value is ignored), the
+// server lists all metrics that could be returned for the requested path.
+//
+// The (repeatable) `buckets` query parameter is a list of bucket names (or it
+// could be a comma separated value) to return metrics with a bucket label.
+// Bucket metrics will be returned only for the provided buckets. If no buckets
+// parameter is provided, no bucket metrics are returned.
+func (h *metricsV3Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ pathComponents := mux.Vars(r)["pathComps"]
+ isListingRequest := r.Form.Has("list")
+
+ var buckets []string
+ if strings.HasPrefix(pathComponents, "/bucket/") {
+ // bucket specific metrics, extract the bucket name from the path.
+ // it's the last part of the path. e.g. /bucket/api/
+ bucketIdx := strings.LastIndex(pathComponents, "/")
+ buckets = append(buckets, pathComponents[bucketIdx+1:])
+ // remove bucket from pathComponents as it is dynamic and
+ // hence not included in the collector path.
+ pathComponents = pathComponents[:bucketIdx]
+ }
+
+ innerHandler := h.handle(pathComponents, isListingRequest, buckets)
+
+ // Add tracing to the prom. handler
+ tracedHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
+ if ok {
+ tc.FuncName = "handler.MetricsV3"
+ tc.ResponseRecorder.LogErrBody = true
+ }
+
+ innerHandler.ServeHTTP(w, r)
+ })
+
+ // Add authentication
+ h.auth(tracedHandler).ServeHTTP(w, r)
+}
diff --git a/cmd/metrics-v3-ilm.go b/cmd/metrics-v3-ilm.go
new file mode 100644
index 00000000..604beaae
--- /dev/null
+++ b/cmd/metrics-v3-ilm.go
@@ -0,0 +1,53 @@
+// Copyright (c) 2024 MinIO, Inc.
+//
+// # This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+)
+
+const (
+ expiryPendingTasks = "expiry_pending_tasks"
+ transitionActiveTasks = "transition_active_tasks"
+ transitionPendingTasks = "transition_pending_tasks"
+ transitionMissedImmediateTasks = "transition_missed_immediate_tasks"
+ versionsScanned = "versions_scanned"
+)
+
+var (
+ ilmExpiryPendingTasksMD = NewGaugeMD(expiryPendingTasks, "Number of pending ILM expiry tasks in the queue")
+ ilmTransitionActiveTasksMD = NewGaugeMD(transitionActiveTasks, "Number of active ILM transition tasks")
+ ilmTransitionPendingTasksMD = NewGaugeMD(transitionPendingTasks, "Number of pending ILM transition tasks in the queue")
+ ilmTransitionMissedImmediateTasksMD = NewCounterMD(transitionMissedImmediateTasks, "Number of missed immediate ILM transition tasks")
+ ilmVersionsScannedMD = NewCounterMD(versionsScanned, "Total number of object versions checked for ILM actions since server start")
+)
+
+// loadILMMetrics - `MetricsLoaderFn` for ILM metrics.
+func loadILMMetrics(_ context.Context, m MetricValues, _ *metricsCache) error {
+ if globalExpiryState != nil {
+ m.Set(expiryPendingTasks, float64(globalExpiryState.PendingTasks()))
+ }
+ if globalTransitionState != nil {
+ m.Set(transitionActiveTasks, float64(globalTransitionState.ActiveTasks()))
+ m.Set(transitionPendingTasks, float64(globalTransitionState.PendingTasks()))
+ m.Set(transitionMissedImmediateTasks, float64(globalTransitionState.MissedImmediateTasks()))
+ }
+ m.Set(versionsScanned, float64(globalScannerMetrics.lifetime(scannerMetricILM)))
+
+ return nil
+}
diff --git a/cmd/metrics-v3-logger-webhook.go b/cmd/metrics-v3-logger-webhook.go
new file mode 100644
index 00000000..aa85de94
--- /dev/null
+++ b/cmd/metrics-v3-logger-webhook.go
@@ -0,0 +1,59 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+
+ "github.com/minio/minio/internal/logger"
+)
+
+const (
+ webhookQueueLength = "queue_length"
+ webhookTotalMessages = "total_messages"
+ webhookFailedMessages = "failed_messages"
+ nameL = "name"
+ endpointL = "endpoint"
+)
+
+var (
+ allWebhookLabels = []string{nameL, endpointL}
+ webhookFailedMessagesMD = NewCounterMD(webhookFailedMessages,
+ "Number of messages that failed to send",
+ allWebhookLabels...)
+ webhookQueueLengthMD = NewGaugeMD(webhookQueueLength,
+ "Webhook queue length",
+ allWebhookLabels...)
+ webhookTotalMessagesMD = NewCounterMD(webhookTotalMessages,
+ "Total number of messages sent to this target",
+ allWebhookLabels...)
+)
+
+// loadLoggerWebhookMetrics - `MetricsLoaderFn` for logger webhook
+// such as failed messages and total messages.
+func loadLoggerWebhookMetrics(ctx context.Context, m MetricValues, c *metricsCache) error {
+ tgts := append(logger.SystemTargets(), logger.AuditTargets()...)
+ for _, t := range tgts {
+ labels := []string{nameL, t.String(), endpointL, t.Endpoint()}
+ m.Set(webhookFailedMessages, float64(t.Stats().FailedMessages), labels...)
+ m.Set(webhookQueueLength, float64(t.Stats().QueueLength), labels...)
+ m.Set(webhookTotalMessages, float64(t.Stats().TotalMessages), labels...)
+ }
+
+ return nil
+}
diff --git a/cmd/metrics-v3-replication.go b/cmd/metrics-v3-replication.go
new file mode 100644
index 00000000..44a8e87a
--- /dev/null
+++ b/cmd/metrics-v3-replication.go
@@ -0,0 +1,101 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+)
+
+const (
+ replicationAverageActiveWorkers = "average_active_workers"
+ replicationAverageQueuedBytes = "average_queued_bytes"
+ replicationAverageQueuedCount = "average_queued_count"
+ replicationAverageDataTransferRate = "average_data_transfer_rate"
+ replicationCurrentActiveWorkers = "current_active_workers"
+ replicationCurrentDataTransferRate = "current_data_transfer_rate"
+ replicationLastMinuteQueuedBytes = "last_minute_queued_bytes"
+ replicationLastMinuteQueuedCount = "last_minute_queued_count"
+ replicationMaxActiveWorkers = "max_active_workers"
+ replicationMaxQueuedBytes = "max_queued_bytes"
+ replicationMaxQueuedCount = "max_queued_count"
+ replicationMaxDataTransferRate = "max_data_transfer_rate"
+ replicationRecentBacklogCount = "recent_backlog_count"
+)
+
+var (
+ replicationAverageActiveWorkersMD = NewGaugeMD(replicationAverageActiveWorkers,
+ "Average number of active replication workers")
+ replicationAverageQueuedBytesMD = NewGaugeMD(replicationAverageQueuedBytes,
+ "Average number of bytes queued for replication since server start")
+ replicationAverageQueuedCountMD = NewGaugeMD(replicationAverageQueuedCount,
+ "Average number of objects queued for replication since server start")
+ replicationAverageDataTransferRateMD = NewGaugeMD(replicationAverageDataTransferRate,
+ "Average replication data transfer rate in bytes/sec")
+ replicationCurrentActiveWorkersMD = NewGaugeMD(replicationCurrentActiveWorkers,
+ "Total number of active replication workers")
+ replicationCurrentDataTransferRateMD = NewGaugeMD(replicationCurrentDataTransferRate,
+ "Current replication data transfer rate in bytes/sec")
+ replicationLastMinuteQueuedBytesMD = NewGaugeMD(replicationLastMinuteQueuedBytes,
+ "Number of bytes queued for replication in the last full minute")
+ replicationLastMinuteQueuedCountMD = NewGaugeMD(replicationLastMinuteQueuedCount,
+ "Number of objects queued for replication in the last full minute")
+ replicationMaxActiveWorkersMD = NewGaugeMD(replicationMaxActiveWorkers,
+ "Maximum number of active replication workers seen since server start")
+ replicationMaxQueuedBytesMD = NewGaugeMD(replicationMaxQueuedBytes,
+ "Maximum number of bytes queued for replication since server start")
+ replicationMaxQueuedCountMD = NewGaugeMD(replicationMaxQueuedCount,
+ "Maximum number of objects queued for replication since server start")
+ replicationMaxDataTransferRateMD = NewGaugeMD(replicationMaxDataTransferRate,
+ "Maximum replication data transfer rate in bytes/sec seen since server start")
+ replicationRecentBacklogCountMD = NewGaugeMD(replicationRecentBacklogCount,
+ "Total number of objects seen in replication backlog in the last 5 minutes")
+)
+
+// loadClusterReplicationMetrics - `MetricsLoaderFn` for cluster replication metrics
+// such as transfer rate and objects queued.
+func loadClusterReplicationMetrics(ctx context.Context, m MetricValues, c *metricsCache) error {
+ st := globalReplicationStats.Load()
+ if st == nil {
+ return nil
+ }
+
+ qs := st.getNodeQueueStatsSummary()
+
+ qt := qs.QStats
+ m.Set(replicationAverageQueuedBytes, float64(qt.Avg.Bytes))
+ m.Set(replicationAverageQueuedCount, float64(qt.Avg.Count))
+ m.Set(replicationMaxQueuedBytes, float64(qt.Max.Bytes))
+ m.Set(replicationMaxQueuedCount, float64(qt.Max.Count))
+ m.Set(replicationLastMinuteQueuedBytes, float64(qt.Curr.Bytes))
+ m.Set(replicationLastMinuteQueuedCount, float64(qt.Curr.Count))
+
+ qa := qs.ActiveWorkers
+ m.Set(replicationAverageActiveWorkers, float64(qa.Avg))
+ m.Set(replicationCurrentActiveWorkers, float64(qa.Curr))
+ m.Set(replicationMaxActiveWorkers, float64(qa.Max))
+
+ if len(qs.XferStats) > 0 {
+ tots := qs.XferStats[Total]
+ m.Set(replicationAverageDataTransferRate, tots.Avg)
+ m.Set(replicationCurrentDataTransferRate, tots.Curr)
+ m.Set(replicationMaxDataTransferRate, tots.Peak)
+ }
+ m.Set(replicationRecentBacklogCount, float64(qs.MRFStats.LastFailedCount))
+
+ return nil
+}
diff --git a/cmd/metrics-v3-scanner.go b/cmd/metrics-v3-scanner.go
new file mode 100644
index 00000000..8f661e23
--- /dev/null
+++ b/cmd/metrics-v3-scanner.go
@@ -0,0 +1,66 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "time"
+)
+
+const (
+ scannerBucketScansFinished = "bucket_scans_finished"
+ scannerBucketScansStarted = "bucket_scans_started"
+ scannerDirectoriesScanned = "directories_scanned"
+ scannerObjectsScanned = "objects_scanned"
+ scannerVersionsScanned = "versions_scanned"
+ scannerLastActivitySeconds = "last_activity_seconds"
+)
+
+var (
+ scannerBucketScansFinishedMD = NewCounterMD(scannerBucketScansFinished,
+ "Total number of bucket scans finished since server start")
+ scannerBucketScansStartedMD = NewCounterMD(scannerBucketScansStarted,
+ "Total number of bucket scans started since server start")
+ scannerDirectoriesScannedMD = NewCounterMD(scannerDirectoriesScanned,
+ "Total number of directories scanned since server start")
+ scannerObjectsScannedMD = NewCounterMD(scannerObjectsScanned,
+ "Total number of unique objects scanned since server start")
+ scannerVersionsScannedMD = NewCounterMD(scannerVersionsScanned,
+ "Total number of object versions scanned since server start")
+ scannerLastActivitySecondsMD = NewGaugeMD(scannerLastActivitySeconds,
+ "Time elapsed (in seconds) since last scan activity.")
+)
+
+// loadClusterScannerMetrics - `MetricsLoaderFn` for cluster webhook
+// such as failed objects and directories scanned.
+func loadClusterScannerMetrics(ctx context.Context, m MetricValues, c *metricsCache) error {
+ m.Set(scannerBucketScansFinished, float64(globalScannerMetrics.lifetime(scannerMetricScanBucketDrive)))
+ m.Set(scannerBucketScansStarted, float64(globalScannerMetrics.lifetime(scannerMetricScanBucketDrive)+uint64(globalScannerMetrics.activeDrives())))
+ m.Set(scannerDirectoriesScanned, float64(globalScannerMetrics.lifetime(scannerMetricScanFolder)))
+ m.Set(scannerObjectsScanned, float64(globalScannerMetrics.lifetime(scannerMetricScanObject)))
+ m.Set(scannerVersionsScanned, float64(globalScannerMetrics.lifetime(scannerMetricApplyVersion)))
+
+ dui, err := c.dataUsageInfo.Get()
+ if err != nil {
+ metricsLogIf(ctx, err)
+ } else {
+ m.Set(scannerLastActivitySeconds, time.Since(dui.LastUpdate).Seconds())
+ }
+
+ return nil
+}
diff --git a/cmd/metrics-v3-system-cpu.go b/cmd/metrics-v3-system-cpu.go
new file mode 100644
index 00000000..cb31b835
--- /dev/null
+++ b/cmd/metrics-v3-system-cpu.go
@@ -0,0 +1,84 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// # This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "math"
+)
+
+const (
+ sysCPUAvgIdle = "avg_idle"
+ sysCPUAvgIOWait = "avg_iowait"
+ sysCPULoad = "load"
+ sysCPULoadPerc = "load_perc"
+ sysCPUNice = "nice"
+ sysCPUSteal = "steal"
+ sysCPUSystem = "system"
+ sysCPUUser = "user"
+)
+
+var (
+ sysCPUAvgIdleMD = NewGaugeMD(sysCPUAvgIdle, "Average CPU idle time")
+ sysCPUAvgIOWaitMD = NewGaugeMD(sysCPUAvgIOWait, "Average CPU IOWait time")
+ sysCPULoadMD = NewGaugeMD(sysCPULoad, "CPU load average 1min")
+ sysCPULoadPercMD = NewGaugeMD(sysCPULoadPerc, "CPU load average 1min (percentage)")
+ sysCPUNiceMD = NewGaugeMD(sysCPUNice, "CPU nice time")
+ sysCPUStealMD = NewGaugeMD(sysCPUSteal, "CPU steal time")
+ sysCPUSystemMD = NewGaugeMD(sysCPUSystem, "CPU system time")
+ sysCPUUserMD = NewGaugeMD(sysCPUUser, "CPU user time")
+)
+
+// loadCPUMetrics - `MetricsLoaderFn` for system CPU metrics.
+func loadCPUMetrics(ctx context.Context, m MetricValues, c *metricsCache) error {
+ cpuMetrics, _ := c.cpuMetrics.Get()
+
+ if cpuMetrics.LoadStat != nil {
+ m.Set(sysCPULoad, cpuMetrics.LoadStat.Load1)
+ perc := cpuMetrics.LoadStat.Load1 * 100 / float64(cpuMetrics.CPUCount)
+ m.Set(sysCPULoadPerc, math.Round(perc*100)/100)
+ }
+
+ ts := cpuMetrics.TimesStat
+ if ts != nil {
+ tot := ts.User + ts.System + ts.Idle + ts.Iowait + ts.Nice + ts.Steal
+ cpuUserVal := math.Round(ts.User/tot*100*100) / 100
+ m.Set(sysCPUUser, cpuUserVal)
+ cpuSystemVal := math.Round(ts.System/tot*100*100) / 100
+ m.Set(sysCPUSystem, cpuSystemVal)
+ cpuNiceVal := math.Round(ts.Nice/tot*100*100) / 100
+ m.Set(sysCPUNice, cpuNiceVal)
+ cpuStealVal := math.Round(ts.Steal/tot*100*100) / 100
+ m.Set(sysCPUSteal, cpuStealVal)
+ }
+
+ // metrics-resource.go runs a job to collect resource metrics including their Avg values and
+ // stores them in resourceMetricsMap. We can use it to get the Avg values of CPU idle and IOWait.
+ cpuResourceMetrics, found := resourceMetricsMap[cpuSubsystem]
+ if found {
+ if cpuIdleMetric, ok := cpuResourceMetrics[getResourceKey(cpuIdle, nil)]; ok {
+ avgVal := math.Round(cpuIdleMetric.Avg*100) / 100
+ m.Set(sysCPUAvgIdle, avgVal)
+ }
+ if cpuIOWaitMetric, ok := cpuResourceMetrics[getResourceKey(cpuIOWait, nil)]; ok {
+ avgVal := math.Round(cpuIOWaitMetric.Avg*100) / 100
+ m.Set(sysCPUAvgIOWait, avgVal)
+ }
+ }
+ return nil
+}
diff --git a/cmd/metrics-v3-system-drive.go b/cmd/metrics-v3-system-drive.go
new file mode 100644
index 00000000..d25a623c
--- /dev/null
+++ b/cmd/metrics-v3-system-drive.go
@@ -0,0 +1,234 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "strconv"
+
+ "github.com/minio/madmin-go/v3"
+)
+
+// label constants
+const (
+ driveL = "drive"
+ poolIndexL = "pool_index"
+ setIndexL = "set_index"
+ driveIndexL = "drive_index"
+
+ apiL = "api"
+
+ sectorSize = uint64(512)
+ kib = float64(1 << 10)
+
+ driveHealthOffline = float64(0)
+ driveHealthOnline = float64(1)
+ driveHealthHealing = float64(2)
+)
+
+var allDriveLabels = []string{driveL, poolIndexL, setIndexL, driveIndexL}
+
+const (
+ driveUsedBytes = "used_bytes"
+ driveFreeBytes = "free_bytes"
+ driveTotalBytes = "total_bytes"
+ driveUsedInodes = "used_inodes"
+ driveFreeInodes = "free_inodes"
+ driveTotalInodes = "total_inodes"
+ driveTimeoutErrorsTotal = "timeout_errors_total"
+ driveIOErrorsTotal = "io_errors_total"
+ driveAvailabilityErrorsTotal = "availability_errors_total"
+ driveWaitingIO = "waiting_io"
+ driveAPILatencyMicros = "api_latency_micros"
+ driveHealth = "health"
+
+ driveOfflineCount = "offline_count"
+ driveOnlineCount = "online_count"
+ driveCount = "count"
+
+ // iostat related
+ driveReadsPerSec = "reads_per_sec"
+ driveReadsKBPerSec = "reads_kb_per_sec"
+ driveReadsAwait = "reads_await"
+ driveWritesPerSec = "writes_per_sec"
+ driveWritesKBPerSec = "writes_kb_per_sec"
+ driveWritesAwait = "writes_await"
+ drivePercUtil = "perc_util"
+)
+
+var (
+ driveUsedBytesMD = NewGaugeMD(driveUsedBytes,
+ "Total storage used on a drive in bytes", allDriveLabels...)
+ driveFreeBytesMD = NewGaugeMD(driveFreeBytes,
+ "Total storage free on a drive in bytes", allDriveLabels...)
+ driveTotalBytesMD = NewGaugeMD(driveTotalBytes,
+ "Total storage available on a drive in bytes", allDriveLabels...)
+ driveUsedInodesMD = NewGaugeMD(driveUsedInodes,
+ "Total used inodes on a drive", allDriveLabels...)
+ driveFreeInodesMD = NewGaugeMD(driveFreeInodes,
+ "Total free inodes on a drive", allDriveLabels...)
+ driveTotalInodesMD = NewGaugeMD(driveTotalInodes,
+ "Total inodes available on a drive", allDriveLabels...)
+ driveTimeoutErrorsMD = NewCounterMD(driveTimeoutErrorsTotal,
+ "Total timeout errors on a drive", allDriveLabels...)
+ driveIOErrorsMD = NewCounterMD(driveIOErrorsTotal,
+ "Total I/O errors on a drive", allDriveLabels...)
+ driveAvailabilityErrorsMD = NewCounterMD(driveAvailabilityErrorsTotal,
+ "Total availability errors (I/O errors, timeouts) on a drive",
+ allDriveLabels...)
+ driveWaitingIOMD = NewGaugeMD(driveWaitingIO,
+ "Total waiting I/O operations on a drive", allDriveLabels...)
+ driveAPILatencyMD = NewGaugeMD(driveAPILatencyMicros,
+ "Average last minute latency in µs for drive API storage operations",
+ append(allDriveLabels, apiL)...)
+ driveHealthMD = NewGaugeMD(driveHealth,
+ "Drive health (0 = offline, 1 = healthy, 2 = healing)", allDriveLabels...)
+
+ driveOfflineCountMD = NewGaugeMD(driveOfflineCount,
+ "Count of offline drives")
+ driveOnlineCountMD = NewGaugeMD(driveOnlineCount,
+ "Count of online drives")
+ driveCountMD = NewGaugeMD(driveCount,
+ "Count of all drives")
+
+ // iostat related
+ driveReadsPerSecMD = NewGaugeMD(driveReadsPerSec,
+ "Reads per second on a drive",
+ allDriveLabels...)
+ driveReadsKBPerSecMD = NewGaugeMD(driveReadsKBPerSec,
+ "Kilobytes read per second on a drive",
+ allDriveLabels...)
+ driveReadsAwaitMD = NewGaugeMD(driveReadsAwait,
+ "Average time for read requests served on a drive",
+ allDriveLabels...)
+ driveWritesPerSecMD = NewGaugeMD(driveWritesPerSec,
+ "Writes per second on a drive",
+ allDriveLabels...)
+ driveWritesKBPerSecMD = NewGaugeMD(driveWritesKBPerSec,
+ "Kilobytes written per second on a drive",
+ allDriveLabels...)
+ driveWritesAwaitMD = NewGaugeMD(driveWritesAwait,
+ "Average time for write requests served on a drive",
+ allDriveLabels...)
+ drivePercUtilMD = NewGaugeMD(drivePercUtil,
+ "Percentage of time the disk was busy",
+ allDriveLabels...)
+)
+
+func getCurrentDriveIOStats() map[string]madmin.DiskIOStats {
+ types := madmin.MetricsDisk
+ driveRealtimeMetrics := collectLocalMetrics(types, collectMetricsOpts{
+ hosts: map[string]struct{}{
+ globalLocalNodeName: {},
+ },
+ })
+
+ stats := map[string]madmin.DiskIOStats{}
+ for d, m := range driveRealtimeMetrics.ByDisk {
+ stats[d] = m.IOStats
+ }
+ return stats
+}
+
+func (m *MetricValues) setDriveBasicMetrics(drive madmin.Disk, labels []string) {
+ m.Set(driveUsedBytes, float64(drive.UsedSpace), labels...)
+ m.Set(driveFreeBytes, float64(drive.AvailableSpace), labels...)
+ m.Set(driveTotalBytes, float64(drive.TotalSpace), labels...)
+ m.Set(driveUsedInodes, float64(drive.UsedInodes), labels...)
+ m.Set(driveFreeInodes, float64(drive.FreeInodes), labels...)
+ m.Set(driveTotalInodes, float64(drive.UsedInodes+drive.FreeInodes), labels...)
+
+ var health float64
+ switch drive.Healing {
+ case true:
+ health = driveHealthHealing
+ case false:
+ if drive.State == "ok" {
+ health = driveHealthOnline
+ } else {
+ health = driveHealthOffline
+ }
+ }
+ m.Set(driveHealth, health, labels...)
+}
+
+func (m *MetricValues) setDriveAPIMetrics(disk madmin.Disk, labels []string) {
+ if disk.Metrics == nil {
+ return
+ }
+
+ m.Set(driveTimeoutErrorsTotal, float64(disk.Metrics.TotalErrorsTimeout), labels...)
+ m.Set(driveIOErrorsTotal, float64(disk.Metrics.TotalErrorsAvailability-disk.Metrics.TotalErrorsTimeout), labels...)
+ m.Set(driveAvailabilityErrorsTotal, float64(disk.Metrics.TotalErrorsAvailability), labels...)
+ m.Set(driveWaitingIO, float64(disk.Metrics.TotalWaiting), labels...)
+
+ // Append the api label for the drive API latencies.
+ labels = append(labels, "api", "")
+ lastIdx := len(labels) - 1
+ for apiName, latency := range disk.Metrics.LastMinute {
+ labels[lastIdx] = "storage." + apiName
+ m.Set(driveAPILatencyMicros, float64(latency.Avg().Microseconds()),
+ labels...)
+ }
+}
+
+func (m *MetricValues) setDriveIOStatMetrics(ioStats driveIOStatMetrics, labels []string) {
+ m.Set(driveReadsPerSec, ioStats.readsPerSec, labels...)
+ m.Set(driveReadsKBPerSec, ioStats.readsKBPerSec, labels...)
+ if ioStats.readsPerSec > 0 {
+ m.Set(driveReadsAwait, ioStats.readsAwait, labels...)
+ }
+
+ m.Set(driveWritesPerSec, ioStats.writesPerSec, labels...)
+ m.Set(driveWritesKBPerSec, ioStats.writesKBPerSec, labels...)
+ if ioStats.writesPerSec > 0 {
+ m.Set(driveWritesAwait, ioStats.writesAwait, labels...)
+ }
+
+ m.Set(drivePercUtil, ioStats.percUtil, labels...)
+}
+
+// loadDriveMetrics - `MetricsLoaderFn` for node drive metrics.
+func loadDriveMetrics(ctx context.Context, m MetricValues, c *metricsCache) error {
+ driveMetrics, err := c.driveMetrics.Get()
+ if err != nil {
+ metricsLogIf(ctx, err)
+ return nil
+ }
+
+ for _, disk := range driveMetrics.storageInfo.Disks {
+ labels := []string{
+ driveL, disk.DrivePath,
+ poolIndexL, strconv.Itoa(disk.PoolIndex),
+ setIndexL, strconv.Itoa(disk.SetIndex),
+ driveIndexL, strconv.Itoa(disk.DiskIndex),
+ }
+
+ m.setDriveBasicMetrics(disk, labels)
+ if dm, found := driveMetrics.ioStats[disk.DrivePath]; found {
+ m.setDriveIOStatMetrics(dm, labels)
+ }
+ m.setDriveAPIMetrics(disk, labels)
+ }
+
+ m.Set(driveOfflineCount, float64(driveMetrics.offlineDrives))
+ m.Set(driveOnlineCount, float64(driveMetrics.onlineDrives))
+ m.Set(driveCount, float64(driveMetrics.totalDrives))
+
+ return nil
+}
diff --git a/cmd/metrics-v3-system-memory.go b/cmd/metrics-v3-system-memory.go
new file mode 100644
index 00000000..f304631b
--- /dev/null
+++ b/cmd/metrics-v3-system-memory.go
@@ -0,0 +1,65 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// # This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+)
+
+const (
+ memTotal = "total"
+ memUsed = "used"
+ memFree = "free"
+ memBuffers = "buffers"
+ memCache = "cache"
+ memUsedPerc = "used_perc"
+ memShared = "shared"
+ memAvailable = "available"
+)
+
+var (
+ memTotalMD = NewGaugeMD(memTotal, "Total memory on the node")
+ memUsedMD = NewGaugeMD(memUsed, "Used memory on the node")
+ memUsedPercMD = NewGaugeMD(memUsedPerc, "Used memory percentage on the node")
+ memFreeMD = NewGaugeMD(memFree, "Free memory on the node")
+ memBuffersMD = NewGaugeMD(memBuffers, "Buffers memory on the node")
+ memCacheMD = NewGaugeMD(memCache, "Cache memory on the node")
+ memSharedMD = NewGaugeMD(memShared, "Shared memory on the node")
+ memAvailableMD = NewGaugeMD(memAvailable, "Available memory on the node")
+)
+
+// loadMemoryMetrics - `MetricsLoaderFn` for node memory metrics.
+func loadMemoryMetrics(ctx context.Context, m MetricValues, c *metricsCache) error {
+ memMetrics, err := c.memoryMetrics.Get()
+ if err != nil {
+ metricsLogIf(ctx, err)
+ return err
+ }
+
+ m.Set(memTotal, float64(memMetrics.Total))
+ m.Set(memUsed, float64(memMetrics.Used))
+ usedPerc := float64(memMetrics.Used) * 100 / float64(memMetrics.Total)
+ m.Set(memUsedPerc, usedPerc)
+ m.Set(memFree, float64(memMetrics.Free))
+ m.Set(memBuffers, float64(memMetrics.Buffers))
+ m.Set(memCache, float64(memMetrics.Cache))
+ m.Set(memShared, float64(memMetrics.Shared))
+ m.Set(memAvailable, float64(memMetrics.Available))
+
+ return nil
+}
diff --git a/cmd/metrics-v3-system-network.go b/cmd/metrics-v3-system-network.go
new file mode 100644
index 00000000..e0328afc
--- /dev/null
+++ b/cmd/metrics-v3-system-network.go
@@ -0,0 +1,61 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+
+ "github.com/minio/minio/internal/rest"
+)
+
+const (
+ internodeErrorsTotal MetricName = "errors_total"
+ internodeDialErrorsTotal MetricName = "dial_errors_total"
+ internodeDialAvgTimeNanos MetricName = "dial_avg_time_nanos"
+ internodeSentBytesTotal MetricName = "sent_bytes_total"
+ internodeRecvBytesTotal MetricName = "recv_bytes_total"
+)
+
+var (
+ internodeErrorsTotalMD = NewCounterMD(internodeErrorsTotal,
+ "Total number of failed internode calls")
+ internodeDialedErrorsTotalMD = NewCounterMD(internodeDialErrorsTotal,
+ "Total number of internode TCP dial timeouts and errors")
+ internodeDialAvgTimeNanosMD = NewGaugeMD(internodeDialAvgTimeNanos,
+ "Average dial time of internode TCP calls in nanoseconds")
+ internodeSentBytesTotalMD = NewCounterMD(internodeSentBytesTotal,
+ "Total number of bytes sent to other peer nodes")
+ internodeRecvBytesTotalMD = NewCounterMD(internodeRecvBytesTotal,
+ "Total number of bytes received from other peer nodes")
+)
+
+// loadNetworkInternodeMetrics - reads internode network metrics.
+//
+// This is a `MetricsLoaderFn`.
+func loadNetworkInternodeMetrics(ctx context.Context, m MetricValues, _ *metricsCache) error {
+ connStats := globalConnStats.toServerConnStats()
+ rpcStats := rest.GetRPCStats()
+ if globalIsDistErasure {
+ m.Set(internodeErrorsTotal, float64(rpcStats.Errs))
+ m.Set(internodeDialErrorsTotal, float64(rpcStats.DialErrs))
+ m.Set(internodeDialAvgTimeNanos, float64(rpcStats.DialAvgDuration))
+ m.Set(internodeSentBytesTotal, float64(connStats.internodeOutputBytes))
+ m.Set(internodeRecvBytesTotal, float64(connStats.internodeInputBytes))
+ }
+ return nil
+}
diff --git a/cmd/metrics-v3-system-process.go b/cmd/metrics-v3-system-process.go
new file mode 100644
index 00000000..01dbba88
--- /dev/null
+++ b/cmd/metrics-v3-system-process.go
@@ -0,0 +1,174 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// # This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "runtime"
+ "time"
+
+ "github.com/prometheus/procfs"
+)
+
+const (
+ processLocksReadTotal = "locks_read_total"
+ processLocksWriteTotal = "locks_write_total"
+ processCPUTotalSeconds = "cpu_total_seconds"
+ processGoRoutineTotal = "go_routine_total"
+ processIORCharBytes = "io_rchar_bytes"
+ processIOReadBytes = "io_read_bytes"
+ processIOWCharBytes = "io_wchar_bytes"
+ processIOWriteBytes = "io_write_bytes"
+ processStartTimeSeconds = "start_time_seconds"
+ processUptimeSeconds = "uptime_seconds"
+ processFileDescriptorLimitTotal = "file_descriptor_limit_total"
+ processFileDescriptorOpenTotal = "file_descriptor_open_total"
+ processSyscallReadTotal = "syscall_read_total"
+ processSyscallWriteTotal = "syscall_write_total"
+ processResidentMemoryBytes = "resident_memory_bytes"
+ processVirtualMemoryBytes = "virtual_memory_bytes"
+ processVirtualMemoryMaxBytes = "virtual_memory_max_bytes"
+)
+
+var (
+ processLocksReadTotalMD = NewGaugeMD(processLocksReadTotal, "Number of current READ locks on this peer")
+ processLocksWriteTotalMD = NewGaugeMD(processLocksWriteTotal, "Number of current WRITE locks on this peer")
+ processCPUTotalSecondsMD = NewCounterMD(processCPUTotalSeconds, "Total user and system CPU time spent in seconds")
+ processGoRoutineTotalMD = NewGaugeMD(processGoRoutineTotal, "Total number of go routines running")
+ processIORCharBytesMD = NewCounterMD(processIORCharBytes, "Total bytes read by the process from the underlying storage system including cache, /proc/[pid]/io rchar")
+ processIOReadBytesMD = NewCounterMD(processIOReadBytes, "Total bytes read by the process from the underlying storage system, /proc/[pid]/io read_bytes")
+ processIOWCharBytesMD = NewCounterMD(processIOWCharBytes, "Total bytes written by the process to the underlying storage system including page cache, /proc/[pid]/io wchar")
+ processIOWriteBytesMD = NewCounterMD(processIOWriteBytes, "Total bytes written by the process to the underlying storage system, /proc/[pid]/io write_bytes")
+ processStarttimeSecondsMD = NewGaugeMD(processStartTimeSeconds, "Start time for MinIO process in seconds since Unix epoc")
+ processUptimeSecondsMD = NewGaugeMD(processUptimeSeconds, "Uptime for MinIO process in seconds")
+ processFileDescriptorLimitTotalMD = NewGaugeMD(processFileDescriptorLimitTotal, "Limit on total number of open file descriptors for the MinIO Server process")
+ processFileDescriptorOpenTotalMD = NewGaugeMD(processFileDescriptorOpenTotal, "Total number of open file descriptors by the MinIO Server process")
+ processSyscallReadTotalMD = NewCounterMD(processSyscallReadTotal, "Total read SysCalls to the kernel. /proc/[pid]/io syscr")
+ processSyscallWriteTotalMD = NewCounterMD(processSyscallWriteTotal, "Total write SysCalls to the kernel. /proc/[pid]/io syscw")
+ processResidentMemoryBytesMD = NewGaugeMD(processResidentMemoryBytes, "Resident memory size in bytes")
+ processVirtualMemoryBytesMD = NewGaugeMD(processVirtualMemoryBytes, "Virtual memory size in bytes")
+ processVirtualMemoryMaxBytesMD = NewGaugeMD(processVirtualMemoryMaxBytes, "Maximum virtual memory size in bytes")
+)
+
+func loadProcStatMetrics(ctx context.Context, stat procfs.ProcStat, m MetricValues) {
+ if stat.CPUTime() > 0 {
+ m.Set(processCPUTotalSeconds, float64(stat.CPUTime()))
+ }
+
+ if stat.ResidentMemory() > 0 {
+ m.Set(processResidentMemoryBytes, float64(stat.ResidentMemory()))
+ }
+
+ if stat.VirtualMemory() > 0 {
+ m.Set(processVirtualMemoryBytes, float64(stat.VirtualMemory()))
+ }
+
+ startTime, err := stat.StartTime()
+ if err != nil {
+ metricsLogIf(ctx, err)
+ } else if startTime > 0 {
+ m.Set(processStartTimeSeconds, float64(startTime))
+ }
+}
+
+func loadProcIOMetrics(ctx context.Context, io procfs.ProcIO, m MetricValues) {
+ if io.RChar > 0 {
+ m.Set(processIORCharBytes, float64(io.RChar))
+ }
+
+ if io.ReadBytes > 0 {
+ m.Set(processIOReadBytes, float64(io.ReadBytes))
+ }
+
+ if io.WChar > 0 {
+ m.Set(processIOWCharBytes, float64(io.WChar))
+ }
+
+ if io.WriteBytes > 0 {
+ m.Set(processIOWriteBytes, float64(io.WriteBytes))
+ }
+
+ if io.SyscR > 0 {
+ m.Set(processSyscallReadTotal, float64(io.SyscR))
+ }
+
+ if io.SyscW > 0 {
+ m.Set(processSyscallWriteTotal, float64(io.SyscW))
+ }
+}
+
+func loadProcFSMetrics(ctx context.Context, p procfs.Proc, m MetricValues) {
+ stat, err := p.Stat()
+ if err != nil {
+ metricsLogIf(ctx, err)
+ } else {
+ loadProcStatMetrics(ctx, stat, m)
+ }
+
+ io, err := p.IO()
+ if err != nil {
+ metricsLogIf(ctx, err)
+ } else {
+ loadProcIOMetrics(ctx, io, m)
+ }
+
+ l, err := p.Limits()
+ if err != nil {
+ metricsLogIf(ctx, err)
+ } else {
+ if l.OpenFiles > 0 {
+ m.Set(processFileDescriptorLimitTotal, float64(l.OpenFiles))
+ }
+
+ if l.AddressSpace > 0 {
+ m.Set(processVirtualMemoryMaxBytes, float64(l.AddressSpace))
+ }
+ }
+
+ openFDs, err := p.FileDescriptorsLen()
+ if err != nil {
+ metricsLogIf(ctx, err)
+ } else if openFDs > 0 {
+ m.Set(processFileDescriptorOpenTotal, float64(openFDs))
+ }
+}
+
+// loadProcessMetrics - `MetricsLoaderFn` for process metrics
+func loadProcessMetrics(ctx context.Context, m MetricValues, c *metricsCache) error {
+ m.Set(processGoRoutineTotal, float64(runtime.NumGoroutine()))
+
+ if !globalBootTime.IsZero() {
+ m.Set(processUptimeSeconds, time.Since(globalBootTime).Seconds())
+ }
+
+ if runtime.GOOS != globalWindowsOSName && runtime.GOOS != globalMacOSName {
+ p, err := procfs.Self()
+ if err != nil {
+ metricsLogIf(ctx, err)
+ } else {
+ loadProcFSMetrics(ctx, p, m)
+ }
+ }
+
+ if globalIsDistErasure && globalLockServer != nil {
+ st := globalLockServer.stats()
+ m.Set(processLocksReadTotal, float64(st.Reads))
+ m.Set(processLocksWriteTotal, float64(st.Writes))
+ }
+ return nil
+}
diff --git a/cmd/metrics-v3-types.go b/cmd/metrics-v3-types.go
new file mode 100644
index 00000000..92004c96
--- /dev/null
+++ b/cmd/metrics-v3-types.go
@@ -0,0 +1,515 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "slices"
+ "strings"
+ "sync"
+
+ "github.com/minio/minio-go/v7/pkg/set"
+ "github.com/minio/minio/internal/logger"
+ "github.com/pkg/errors"
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+type collectorPath string
+
+// metricPrefix converts a collector path to a metric name prefix. The path is
+// converted to snake-case (by replaced '/' and '-' with '_') and prefixed with
+// `minio_`.
+func (cp collectorPath) metricPrefix() string {
+ s := strings.TrimPrefix(string(cp), SlashSeparator)
+ s = strings.ReplaceAll(s, SlashSeparator, "_")
+ s = strings.ReplaceAll(s, "-", "_")
+ return "minio_" + s
+}
+
+// isDescendantOf returns true if it is a descendant of (or the same as)
+// `ancestor`.
+//
+// For example:
+//
+// /a, /a/b, /a/b/c are all descendants of /a.
+// /abc or /abd/a are not descendants of /ab.
+func (cp collectorPath) isDescendantOf(arg string) bool {
+ descendant := string(cp)
+ if descendant == arg {
+ return true
+ }
+ if len(arg) >= len(descendant) {
+ return false
+ }
+ if !strings.HasSuffix(arg, SlashSeparator) {
+ arg += SlashSeparator
+ }
+ return strings.HasPrefix(descendant, arg)
+}
+
+// MetricType - represents the type of a metric.
+type MetricType int
+
+const (
+ // CounterMT - represents a counter metric.
+ CounterMT MetricType = iota
+ // GaugeMT - represents a gauge metric.
+ GaugeMT
+ // HistogramMT - represents a histogram metric.
+ HistogramMT
+)
+
+// rangeL - represents a range label.
+const rangeL = "range"
+
+func (mt MetricType) String() string {
+ switch mt {
+ case CounterMT:
+ return "counter"
+ case GaugeMT:
+ return "gauge"
+ case HistogramMT:
+ return "histogram"
+ default:
+ return "*unknown*"
+ }
+}
+
+func (mt MetricType) toProm() prometheus.ValueType {
+ switch mt {
+ case CounterMT:
+ return prometheus.CounterValue
+ case GaugeMT:
+ return prometheus.GaugeValue
+ case HistogramMT:
+ return prometheus.CounterValue
+ default:
+ panic(fmt.Sprintf("unknown metric type: %d", mt))
+ }
+}
+
+// MetricDescriptor - represents a metric descriptor.
+type MetricDescriptor struct {
+ Name MetricName
+ Type MetricType
+ Help string
+ VariableLabels []string
+
+ // managed values follow:
+ labelSet map[string]struct{}
+}
+
+func (md *MetricDescriptor) getLabelSet() map[string]struct{} {
+ if md.labelSet != nil {
+ return md.labelSet
+ }
+ md.labelSet = make(map[string]struct{}, len(md.VariableLabels))
+ for _, label := range md.VariableLabels {
+ md.labelSet[label] = struct{}{}
+ }
+ return md.labelSet
+}
+
+func (md *MetricDescriptor) toPromName(namePrefix string) string {
+ return prometheus.BuildFQName(namePrefix, "", string(md.Name))
+}
+
+func (md *MetricDescriptor) toPromDesc(namePrefix string, extraLabels map[string]string) *prometheus.Desc {
+ return prometheus.NewDesc(
+ md.toPromName(namePrefix),
+ md.Help,
+ md.VariableLabels, extraLabels,
+ )
+}
+
+// NewCounterMD - creates a new counter metric descriptor.
+func NewCounterMD(name MetricName, help string, labels ...string) MetricDescriptor {
+ return MetricDescriptor{
+ Name: name,
+ Type: CounterMT,
+ Help: help,
+ VariableLabels: labels,
+ }
+}
+
+// NewGaugeMD - creates a new gauge metric descriptor.
+func NewGaugeMD(name MetricName, help string, labels ...string) MetricDescriptor {
+ return MetricDescriptor{
+ Name: name,
+ Type: GaugeMT,
+ Help: help,
+ VariableLabels: labels,
+ }
+}
+
+type metricValue struct {
+ Labels map[string]string
+ Value float64
+}
+
+// MetricValues - type to set metric values retrieved while loading metrics. A
+// value of this type is passed to the `MetricsLoaderFn`.
+type MetricValues struct {
+ values map[MetricName][]metricValue
+ descriptors map[MetricName]MetricDescriptor
+}
+
+func newMetricValues(d map[MetricName]MetricDescriptor) MetricValues {
+ return MetricValues{
+ values: make(map[MetricName][]metricValue, len(d)),
+ descriptors: d,
+ }
+}
+
+// ToPromMetrics - converts the internal metric values to Prometheus
+// adding the given name prefix. The extraLabels are added to each metric as
+// constant labels.
+func (m *MetricValues) ToPromMetrics(namePrefix string, extraLabels map[string]string,
+) []prometheus.Metric {
+ metrics := make([]prometheus.Metric, 0, len(m.values))
+ for metricName, mv := range m.values {
+ desc := m.descriptors[metricName]
+ promDesc := desc.toPromDesc(namePrefix, extraLabels)
+ for _, v := range mv {
+ // labelValues is in the same order as the variable labels in the
+ // descriptor.
+ labelValues := make([]string, 0, len(v.Labels))
+ for _, k := range desc.VariableLabels {
+ labelValues = append(labelValues, v.Labels[k])
+ }
+ metrics = append(metrics,
+ prometheus.MustNewConstMetric(promDesc, desc.Type.toProm(), v.Value,
+ labelValues...))
+ }
+ }
+ return metrics
+}
+
+// Set - sets a metric value along with any provided labels. It is used only
+// with Gauge and Counter metrics.
+//
+// If the MetricName given here is not present in the `MetricsGroup`'s
+// descriptors, this function panics.
+//
+// Panics if `labels` is not a list of ordered label name and label value pairs
+// or if all labels for the metric are not provided.
+func (m *MetricValues) Set(name MetricName, value float64, labels ...string) {
+ desc, ok := m.descriptors[name]
+ if !ok {
+ panic(fmt.Sprintf("metric has no description: %s", name))
+ }
+
+ if len(labels)%2 != 0 {
+ panic("labels must be a list of ordered key-value pairs")
+ }
+
+ validLabels := desc.getLabelSet()
+ labelMap := make(map[string]string, len(labels)/2)
+ for i := 0; i < len(labels); i += 2 {
+ if _, ok := validLabels[labels[i]]; !ok {
+ panic(fmt.Sprintf("invalid label: %s (metric: %s)", labels[i], name))
+ }
+ labelMap[labels[i]] = labels[i+1]
+ }
+
+ if len(labels)/2 != len(validLabels) {
+ panic("not all labels were given values")
+ }
+
+ v, ok := m.values[name]
+ if !ok {
+ v = make([]metricValue, 0, 1)
+ }
+ // If valid non zero value set the metrics
+ if value > 0 {
+ m.values[name] = append(v, metricValue{
+ Labels: labelMap,
+ Value: value,
+ })
+ }
+}
+
+// SetHistogram - sets values for the given MetricName using the provided
+// histogram.
+//
+// `filterByLabels` is a map of label names to list of allowed label values to
+// filter by. Note that this filtering happens before any renaming of labels.
+//
+// `renameLabels` is a map of label names to rename. The keys are the original
+// label names and the values are the new label names.
+//
+// `bucketFilter` is a list of bucket values to filter. If this is non-empty,
+// only metrics for the given buckets are added.
+//
+// `extraLabels` are additional labels to add to each metric. They are ordered
+// label name and value pairs.
+func (m *MetricValues) SetHistogram(name MetricName, hist *prometheus.HistogramVec,
+ filterByLabels map[string]set.StringSet, renameLabels map[string]string, bucketFilter []string,
+ extraLabels ...string,
+) {
+ if _, ok := m.descriptors[name]; !ok {
+ panic(fmt.Sprintf("metric has no description: %s", name))
+ }
+ dummyDesc := MetricDescription{}
+ metricsV2 := getHistogramMetrics(hist, dummyDesc, false, false)
+mainLoop:
+ for _, metric := range metricsV2 {
+ for label, allowedValues := range filterByLabels {
+ if !allowedValues.Contains(metric.VariableLabels[label]) {
+ continue mainLoop
+ }
+ }
+
+ // If a bucket filter is provided, only add metrics for the given
+ // buckets.
+ if len(bucketFilter) > 0 && !slices.Contains(bucketFilter, metric.VariableLabels["bucket"]) {
+ continue
+ }
+
+ labels := make([]string, 0, len(metric.VariableLabels)*2)
+ for k, v := range metric.VariableLabels {
+ if newLabel, ok := renameLabels[k]; ok {
+ labels = append(labels, newLabel, v)
+ } else {
+ labels = append(labels, k, v)
+ }
+ }
+ labels = append(labels, extraLabels...)
+ // If valid non zero value set the metrics
+ if metric.Value > 0 {
+ m.Set(name, metric.Value, labels...)
+ }
+ }
+}
+
+// SetHistogramValues - sets values for the given MetricName using the provided map of
+// range to value.
+func SetHistogramValues[V uint64 | int64 | float64](m MetricValues, name MetricName, values map[string]V, labels ...string) {
+ for rng, val := range values {
+ m.Set(name, float64(val), append(labels, rangeL, rng)...)
+ }
+}
+
+// MetricsLoaderFn - represents a function to load metrics from the
+// metricsCache.
+//
+// Note that returning an error here will cause the Metrics handler to return a
+// 500 Internal Server Error.
+type MetricsLoaderFn func(context.Context, MetricValues, *metricsCache) error
+
+// JoinLoaders - joins multiple loaders into a single loader. The returned
+// loader will call each of the given loaders in order. If any of the loaders
+// return an error, the returned loader will return that error.
+func JoinLoaders(loaders ...MetricsLoaderFn) MetricsLoaderFn {
+ return func(ctx context.Context, m MetricValues, c *metricsCache) error {
+ for _, loader := range loaders {
+ if err := loader(ctx, m, c); err != nil {
+ return err
+ }
+ }
+ return nil
+ }
+}
+
+// BucketMetricsLoaderFn - represents a function to load metrics from the
+// metricsCache and the system for a given list of buckets.
+//
+// Note that returning an error here will cause the Metrics handler to return a
+// 500 Internal Server Error.
+type BucketMetricsLoaderFn func(context.Context, MetricValues, *metricsCache, []string) error
+
+// JoinBucketLoaders - joins multiple bucket loaders into a single loader,
+// similar to `JoinLoaders`.
+func JoinBucketLoaders(loaders ...BucketMetricsLoaderFn) BucketMetricsLoaderFn {
+ return func(ctx context.Context, m MetricValues, c *metricsCache, b []string) error {
+ for _, loader := range loaders {
+ if err := loader(ctx, m, c, b); err != nil {
+ return err
+ }
+ }
+ return nil
+ }
+}
+
+// MetricsGroup - represents a group of metrics. It includes a `MetricsLoaderFn`
+// function that provides a way to load the metrics from the system. The metrics
+// are cached and refreshed after a given timeout.
+//
+// For metrics with a `bucket` dimension, a list of buckets argument is required
+// to collect the metrics.
+//
+// It implements the prometheus.Collector interface for metric groups without a
+// bucket dimension. For metric groups with a bucket dimension, use the
+// `GetBucketCollector` method to get a `BucketCollector` that implements the
+// prometheus.Collector interface.
+type MetricsGroup struct {
+ // Path (relative to the Metrics v3 base endpoint) at which this group of
+ // metrics is served. This value is converted into a metric name prefix
+ // using `.metricPrefix()` and is added to each metric returned.
+ CollectorPath collectorPath
+ // List of all metric descriptors that could be returned by the loader.
+ Descriptors []MetricDescriptor
+ // (Optional) Extra (constant) label KV pairs to be added to each metric in
+ // the group.
+ ExtraLabels map[string]string
+
+ // Loader functions to load metrics. Only one of these will be set. Metrics
+ // returned by these functions must be present in the `Descriptors` list.
+ loader MetricsLoaderFn
+ bucketLoader BucketMetricsLoaderFn
+
+ // Cache for all metrics groups. Set via `.SetCache` method.
+ cache *metricsCache
+
+ // managed values follow:
+
+ // map of metric descriptors by metric name.
+ descriptorMap map[MetricName]MetricDescriptor
+
+ // For bucket metrics, the list of buckets is stored here. It is used in the
+ // Collect() call. This is protected by the `bucketsLock`.
+ bucketsLock sync.Mutex
+ buckets []string
+}
+
+// NewMetricsGroup creates a new MetricsGroup. To create a metrics group for
+// metrics with a `bucket` dimension (label), use `NewBucketMetricsGroup`.
+//
+// The `loader` function loads metrics from the cache and the system.
+func NewMetricsGroup(path collectorPath, descriptors []MetricDescriptor,
+ loader MetricsLoaderFn,
+) *MetricsGroup {
+ mg := &MetricsGroup{
+ CollectorPath: path,
+ Descriptors: descriptors,
+ loader: loader,
+ }
+ mg.validate()
+ return mg
+}
+
+// NewBucketMetricsGroup creates a new MetricsGroup for metrics with a `bucket`
+// dimension (label).
+//
+// The `loader` function loads metrics from the cache and the system for a given
+// list of buckets.
+func NewBucketMetricsGroup(path collectorPath, descriptors []MetricDescriptor,
+ loader BucketMetricsLoaderFn,
+) *MetricsGroup {
+ mg := &MetricsGroup{
+ CollectorPath: path,
+ Descriptors: descriptors,
+ bucketLoader: loader,
+ }
+ mg.validate()
+ return mg
+}
+
+// AddExtraLabels - adds extra (constant) label KV pairs to the metrics group.
+// This is a helper to initialize the `ExtraLabels` field. The argument is a
+// list of ordered label name and value pairs.
+func (mg *MetricsGroup) AddExtraLabels(labels ...string) {
+ if len(labels)%2 != 0 {
+ panic("Labels must be an ordered list of name value pairs")
+ }
+ if mg.ExtraLabels == nil {
+ mg.ExtraLabels = make(map[string]string, len(labels))
+ }
+ for i := 0; i < len(labels); i += 2 {
+ mg.ExtraLabels[labels[i]] = labels[i+1]
+ }
+}
+
+// IsBucketMetricsGroup - returns true if the given MetricsGroup is a bucket
+// metrics group.
+func (mg *MetricsGroup) IsBucketMetricsGroup() bool {
+ return mg.bucketLoader != nil
+}
+
+// Describe - implements prometheus.Collector interface.
+func (mg *MetricsGroup) Describe(ch chan<- *prometheus.Desc) {
+ for _, desc := range mg.Descriptors {
+ ch <- desc.toPromDesc(mg.CollectorPath.metricPrefix(), mg.ExtraLabels)
+ }
+}
+
+// Collect - implements prometheus.Collector interface.
+func (mg *MetricsGroup) Collect(ch chan<- prometheus.Metric) {
+ metricValues := newMetricValues(mg.descriptorMap)
+
+ var err error
+ if mg.IsBucketMetricsGroup() {
+ err = mg.bucketLoader(GlobalContext, metricValues, mg.cache, mg.buckets)
+ } else {
+ err = mg.loader(GlobalContext, metricValues, mg.cache)
+ }
+
+ // There is no way to handle errors here, so we panic the current goroutine
+ // and the Metrics API handler returns a 500 HTTP status code. This should
+ // normally not happen, and usually indicates a bug.
+ logger.CriticalIf(GlobalContext, errors.Wrap(err, "failed to get metrics"))
+
+ promMetrics := metricValues.ToPromMetrics(mg.CollectorPath.metricPrefix(),
+ mg.ExtraLabels)
+ for _, metric := range promMetrics {
+ ch <- metric
+ }
+}
+
+// LockAndSetBuckets - locks the buckets and sets the given buckets. It returns
+// a function to unlock the buckets.
+func (mg *MetricsGroup) LockAndSetBuckets(buckets []string) func() {
+ mg.bucketsLock.Lock()
+ mg.buckets = buckets
+ return func() {
+ mg.bucketsLock.Unlock()
+ }
+}
+
+// MetricFQN - returns the fully qualified name for the given metric name.
+func (mg *MetricsGroup) MetricFQN(name MetricName) string {
+ v, ok := mg.descriptorMap[name]
+ if !ok {
+ // This should never happen.
+ return ""
+ }
+ return v.toPromName(mg.CollectorPath.metricPrefix())
+}
+
+func (mg *MetricsGroup) validate() {
+ if len(mg.Descriptors) == 0 {
+ panic("Descriptors must be set")
+ }
+
+ // For bools A and B, A XOR B <=> A != B.
+ isExactlyOneSet := (mg.loader == nil) != (mg.bucketLoader == nil)
+ if !isExactlyOneSet {
+ panic("Exactly one Loader function must be set")
+ }
+
+ mg.descriptorMap = make(map[MetricName]MetricDescriptor, len(mg.Descriptors))
+ for _, desc := range mg.Descriptors {
+ mg.descriptorMap[desc.Name] = desc
+ }
+}
+
+// SetCache is a helper to initialize MetricsGroup. It sets the cache object.
+func (mg *MetricsGroup) SetCache(c *metricsCache) {
+ mg.cache = c
+}
diff --git a/cmd/metrics-v3.go b/cmd/metrics-v3.go
new file mode 100644
index 00000000..93749258
--- /dev/null
+++ b/cmd/metrics-v3.go
@@ -0,0 +1,487 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "slices"
+ "strings"
+
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/collectors"
+)
+
+// Collector paths.
+//
+// These are paths under the top-level /minio/metrics/v3 metrics endpoint. Each
+// of these paths returns a set of V3 metrics.
+//
+// Per-bucket metrics endpoints always start with /bucket and the bucket name is
+// appended to the path. e.g. if the collector path is /bucket/api, the endpoint
+// for the bucket "mybucket" would be /minio/metrics/v3/bucket/api/mybucket
+const (
+ apiRequestsCollectorPath collectorPath = "/api/requests"
+
+ bucketAPICollectorPath collectorPath = "/bucket/api"
+ bucketReplicationCollectorPath collectorPath = "/bucket/replication"
+
+ systemNetworkInternodeCollectorPath collectorPath = "/system/network/internode"
+ systemDriveCollectorPath collectorPath = "/system/drive"
+ systemMemoryCollectorPath collectorPath = "/system/memory"
+ systemCPUCollectorPath collectorPath = "/system/cpu"
+ systemProcessCollectorPath collectorPath = "/system/process"
+
+ debugGoCollectorPath collectorPath = "/debug/go"
+
+ clusterHealthCollectorPath collectorPath = "/cluster/health"
+ clusterUsageObjectsCollectorPath collectorPath = "/cluster/usage/objects"
+ clusterUsageBucketsCollectorPath collectorPath = "/cluster/usage/buckets"
+ clusterErasureSetCollectorPath collectorPath = "/cluster/erasure-set"
+ clusterIAMCollectorPath collectorPath = "/cluster/iam"
+ clusterConfigCollectorPath collectorPath = "/cluster/config"
+
+ ilmCollectorPath collectorPath = "/ilm"
+ auditCollectorPath collectorPath = "/audit"
+ loggerWebhookCollectorPath collectorPath = "/logger/webhook"
+ replicationCollectorPath collectorPath = "/replication"
+ notificationCollectorPath collectorPath = "/notification"
+ scannerCollectorPath collectorPath = "/scanner"
+)
+
+const (
+ clusterBasePath = "/cluster"
+)
+
+type metricsV3Collection struct {
+ mgMap map[collectorPath]*MetricsGroup
+ bucketMGMap map[collectorPath]*MetricsGroup
+
+ // Gatherers for non-bucket MetricsGroup's
+ mgGatherers map[collectorPath]prometheus.Gatherer
+
+ collectorPaths []collectorPath
+}
+
+func newMetricGroups(r *prometheus.Registry) *metricsV3Collection {
+ // Create all metric groups.
+ apiRequestsMG := NewMetricsGroup(apiRequestsCollectorPath,
+ []MetricDescriptor{
+ apiRejectedAuthTotalMD,
+ apiRejectedHeaderTotalMD,
+ apiRejectedTimestampTotalMD,
+ apiRejectedInvalidTotalMD,
+
+ apiRequestsWaitingTotalMD,
+ apiRequestsIncomingTotalMD,
+ apiRequestsInFlightTotalMD,
+ apiRequestsTotalMD,
+ apiRequestsErrorsTotalMD,
+ apiRequests5xxErrorsTotalMD,
+ apiRequests4xxErrorsTotalMD,
+ apiRequestsCanceledTotalMD,
+
+ apiRequestsTTFBSecondsDistributionMD,
+
+ apiTrafficSentBytesMD,
+ apiTrafficRecvBytesMD,
+ },
+ JoinLoaders(loadAPIRequestsHTTPMetrics, loadAPIRequestsTTFBMetrics,
+ loadAPIRequestsNetworkMetrics),
+ )
+
+ bucketAPIMG := NewBucketMetricsGroup(bucketAPICollectorPath,
+ []MetricDescriptor{
+ bucketAPITrafficRecvBytesMD,
+ bucketAPITrafficSentBytesMD,
+
+ bucketAPIRequestsInFlightMD,
+ bucketAPIRequestsTotalMD,
+ bucketAPIRequestsCanceledMD,
+ bucketAPIRequests4xxErrorsMD,
+ bucketAPIRequests5xxErrorsMD,
+
+ bucketAPIRequestsTTFBSecondsDistributionMD,
+ },
+ JoinBucketLoaders(loadBucketAPIHTTPMetrics, loadBucketAPITTFBMetrics),
+ )
+
+ bucketReplicationMG := NewBucketMetricsGroup(bucketReplicationCollectorPath,
+ []MetricDescriptor{
+ bucketReplLastHrFailedBytesMD,
+ bucketReplLastHrFailedCountMD,
+ bucketReplLastMinFailedBytesMD,
+ bucketReplLastMinFailedCountMD,
+ bucketReplLatencyMsMD,
+ bucketReplProxiedDeleteTaggingRequestsTotalMD,
+ bucketReplProxiedGetRequestsFailuresMD,
+ bucketReplProxiedGetRequestsTotalMD,
+ bucketReplProxiedGetTaggingRequestsFailuresMD,
+ bucketReplProxiedGetTaggingRequestsTotalMD,
+ bucketReplProxiedHeadRequestsFailuresMD,
+ bucketReplProxiedHeadRequestsTotalMD,
+ bucketReplProxiedPutTaggingRequestsFailuresMD,
+ bucketReplProxiedPutTaggingRequestsTotalMD,
+ bucketReplSentBytesMD,
+ bucketReplSentCountMD,
+ bucketReplTotalFailedBytesMD,
+ bucketReplTotalFailedCountMD,
+ bucketReplProxiedDeleteTaggingRequestsFailuresMD,
+ },
+ loadBucketReplicationMetrics,
+ )
+
+ systemNetworkInternodeMG := NewMetricsGroup(systemNetworkInternodeCollectorPath,
+ []MetricDescriptor{
+ internodeErrorsTotalMD,
+ internodeDialedErrorsTotalMD,
+ internodeDialAvgTimeNanosMD,
+ internodeSentBytesTotalMD,
+ internodeRecvBytesTotalMD,
+ },
+ loadNetworkInternodeMetrics,
+ )
+
+ systemMemoryMG := NewMetricsGroup(systemMemoryCollectorPath,
+ []MetricDescriptor{
+ memTotalMD,
+ memUsedMD,
+ memFreeMD,
+ memAvailableMD,
+ memBuffersMD,
+ memCacheMD,
+ memSharedMD,
+ memUsedPercMD,
+ },
+ loadMemoryMetrics,
+ )
+
+ systemCPUMG := NewMetricsGroup(systemCPUCollectorPath,
+ []MetricDescriptor{
+ sysCPUAvgIdleMD,
+ sysCPUAvgIOWaitMD,
+ sysCPULoadMD,
+ sysCPULoadPercMD,
+ sysCPUNiceMD,
+ sysCPUStealMD,
+ sysCPUSystemMD,
+ sysCPUUserMD,
+ },
+ loadCPUMetrics,
+ )
+
+ systemProcessMG := NewMetricsGroup(systemProcessCollectorPath,
+ []MetricDescriptor{
+ processLocksReadTotalMD,
+ processLocksWriteTotalMD,
+ processCPUTotalSecondsMD,
+ processGoRoutineTotalMD,
+ processIORCharBytesMD,
+ processIOReadBytesMD,
+ processIOWCharBytesMD,
+ processIOWriteBytesMD,
+ processStarttimeSecondsMD,
+ processUptimeSecondsMD,
+ processFileDescriptorLimitTotalMD,
+ processFileDescriptorOpenTotalMD,
+ processSyscallReadTotalMD,
+ processSyscallWriteTotalMD,
+ processResidentMemoryBytesMD,
+ processVirtualMemoryBytesMD,
+ processVirtualMemoryMaxBytesMD,
+ },
+ loadProcessMetrics,
+ )
+
+ systemDriveMG := NewMetricsGroup(systemDriveCollectorPath,
+ []MetricDescriptor{
+ driveUsedBytesMD,
+ driveFreeBytesMD,
+ driveTotalBytesMD,
+ driveUsedInodesMD,
+ driveFreeInodesMD,
+ driveTotalInodesMD,
+ driveTimeoutErrorsMD,
+ driveIOErrorsMD,
+ driveAvailabilityErrorsMD,
+ driveWaitingIOMD,
+ driveAPILatencyMD,
+ driveHealthMD,
+
+ driveOfflineCountMD,
+ driveOnlineCountMD,
+ driveCountMD,
+
+ // iostat related
+ driveReadsPerSecMD,
+ driveReadsKBPerSecMD,
+ driveReadsAwaitMD,
+ driveWritesPerSecMD,
+ driveWritesKBPerSecMD,
+ driveWritesAwaitMD,
+ drivePercUtilMD,
+ },
+ loadDriveMetrics,
+ )
+
+ clusterHealthMG := NewMetricsGroup(clusterHealthCollectorPath,
+ []MetricDescriptor{
+ healthDrivesOfflineCountMD,
+ healthDrivesOnlineCountMD,
+ healthDrivesCountMD,
+
+ healthNodesOfflineCountMD,
+ healthNodesOnlineCountMD,
+
+ healthCapacityRawTotalBytesMD,
+ healthCapacityRawFreeBytesMD,
+ healthCapacityUsableTotalBytesMD,
+ healthCapacityUsableFreeBytesMD,
+ },
+ JoinLoaders(loadClusterHealthDriveMetrics,
+ loadClusterHealthNodeMetrics,
+ loadClusterHealthCapacityMetrics),
+ )
+
+ clusterUsageObjectsMG := NewMetricsGroup(clusterUsageObjectsCollectorPath,
+ []MetricDescriptor{
+ usageSinceLastUpdateSecondsMD,
+ usageTotalBytesMD,
+ usageObjectsCountMD,
+ usageVersionsCountMD,
+ usageDeleteMarkersCountMD,
+ usageBucketsCountMD,
+ usageObjectsDistributionMD,
+ usageVersionsDistributionMD,
+ },
+ loadClusterUsageObjectMetrics,
+ )
+
+ clusterUsageBucketsMG := NewMetricsGroup(clusterUsageBucketsCollectorPath,
+ []MetricDescriptor{
+ usageSinceLastUpdateSecondsMD,
+ usageBucketTotalBytesMD,
+ usageBucketObjectsTotalMD,
+ usageBucketVersionsCountMD,
+ usageBucketDeleteMarkersCountMD,
+ usageBucketQuotaTotalBytesMD,
+ usageBucketObjectSizeDistributionMD,
+ usageBucketObjectVersionCountDistributionMD,
+ },
+ loadClusterUsageBucketMetrics,
+ )
+
+ clusterErasureSetMG := NewMetricsGroup(clusterErasureSetCollectorPath,
+ []MetricDescriptor{
+ erasureSetOverallWriteQuorumMD,
+ erasureSetOverallHealthMD,
+ erasureSetReadQuorumMD,
+ erasureSetWriteQuorumMD,
+ erasureSetOnlineDrivesCountMD,
+ erasureSetHealingDrivesCountMD,
+ erasureSetHealthMD,
+ erasureSetReadToleranceMD,
+ erasureSetWriteToleranceMD,
+ erasureSetReadHealthMD,
+ erasureSetWriteHealthMD,
+ },
+ loadClusterErasureSetMetrics,
+ )
+
+ clusterNotificationMG := NewMetricsGroup(notificationCollectorPath,
+ []MetricDescriptor{
+ notificationCurrentSendInProgressMD,
+ notificationEventsErrorsTotalMD,
+ notificationEventsSentTotalMD,
+ notificationEventsSkippedTotalMD,
+ },
+ loadClusterNotificationMetrics,
+ )
+
+ clusterIAMMG := NewMetricsGroup(clusterIAMCollectorPath,
+ []MetricDescriptor{
+ lastSyncDurationMillisMD,
+ pluginAuthnServiceFailedRequestsMinuteMD,
+ pluginAuthnServiceLastFailSecondsMD,
+ pluginAuthnServiceLastSuccSecondsMD,
+ pluginAuthnServiceSuccAvgRttMsMinuteMD,
+ pluginAuthnServiceSuccMaxRttMsMinuteMD,
+ pluginAuthnServiceTotalRequestsMinuteMD,
+ sinceLastSyncMillisMD,
+ syncFailuresMD,
+ syncSuccessesMD,
+ },
+ loadClusterIAMMetrics,
+ )
+
+ clusterReplicationMG := NewMetricsGroup(replicationCollectorPath,
+ []MetricDescriptor{
+ replicationAverageActiveWorkersMD,
+ replicationAverageQueuedBytesMD,
+ replicationAverageQueuedCountMD,
+ replicationAverageDataTransferRateMD,
+ replicationCurrentActiveWorkersMD,
+ replicationCurrentDataTransferRateMD,
+ replicationLastMinuteQueuedBytesMD,
+ replicationLastMinuteQueuedCountMD,
+ replicationMaxActiveWorkersMD,
+ replicationMaxQueuedBytesMD,
+ replicationMaxQueuedCountMD,
+ replicationMaxDataTransferRateMD,
+ replicationRecentBacklogCountMD,
+ },
+ loadClusterReplicationMetrics,
+ )
+
+ clusterConfigMG := NewMetricsGroup(clusterConfigCollectorPath,
+ []MetricDescriptor{
+ configRRSParityMD,
+ configStandardParityMD,
+ },
+ loadClusterConfigMetrics,
+ )
+
+ scannerMG := NewMetricsGroup(scannerCollectorPath,
+ []MetricDescriptor{
+ scannerBucketScansFinishedMD,
+ scannerBucketScansStartedMD,
+ scannerDirectoriesScannedMD,
+ scannerObjectsScannedMD,
+ scannerVersionsScannedMD,
+ scannerLastActivitySecondsMD,
+ },
+ loadClusterScannerMetrics,
+ )
+
+ loggerWebhookMG := NewMetricsGroup(loggerWebhookCollectorPath,
+ []MetricDescriptor{
+ webhookFailedMessagesMD,
+ webhookQueueLengthMD,
+ webhookTotalMessagesMD,
+ },
+ loadLoggerWebhookMetrics,
+ )
+
+ auditMG := NewMetricsGroup(auditCollectorPath,
+ []MetricDescriptor{
+ auditFailedMessagesMD,
+ auditTargetQueueLengthMD,
+ auditTotalMessagesMD,
+ },
+ loadAuditMetrics,
+ )
+
+ ilmMG := NewMetricsGroup(ilmCollectorPath,
+ []MetricDescriptor{
+ ilmExpiryPendingTasksMD,
+ ilmTransitionActiveTasksMD,
+ ilmTransitionPendingTasksMD,
+ ilmTransitionMissedImmediateTasksMD,
+ ilmVersionsScannedMD,
+ },
+ loadILMMetrics,
+ )
+
+ allMetricGroups := []*MetricsGroup{
+ apiRequestsMG,
+ bucketAPIMG,
+ bucketReplicationMG,
+
+ systemNetworkInternodeMG,
+ systemDriveMG,
+ systemMemoryMG,
+ systemCPUMG,
+ systemProcessMG,
+
+ clusterHealthMG,
+ clusterUsageObjectsMG,
+ clusterUsageBucketsMG,
+ clusterErasureSetMG,
+ clusterNotificationMG,
+ clusterIAMMG,
+ clusterReplicationMG,
+ clusterConfigMG,
+
+ ilmMG,
+ scannerMG,
+ auditMG,
+ loggerWebhookMG,
+ }
+
+ // Bucket metrics are special, they always include the bucket label. These
+ // metrics required a list of buckets to be passed to the loader, and the list
+ // of buckets is not known until the request is made. So we keep a separate
+ // map for bucket metrics and handle them specially.
+
+ // Add the serverName and poolIndex labels to all non-cluster metrics.
+ //
+ // Also create metric group maps and set the cache.
+ metricsCache := newMetricsCache()
+ mgMap := make(map[collectorPath]*MetricsGroup)
+ bucketMGMap := make(map[collectorPath]*MetricsGroup)
+ for _, mg := range allMetricGroups {
+ if !strings.HasPrefix(string(mg.CollectorPath), clusterBasePath) {
+ mg.AddExtraLabels(
+ serverName, globalLocalNodeName,
+ // poolIndex, strconv.Itoa(globalLocalPoolIdx),
+ )
+ }
+ mg.SetCache(metricsCache)
+ if mg.IsBucketMetricsGroup() {
+ bucketMGMap[mg.CollectorPath] = mg
+ } else {
+ mgMap[mg.CollectorPath] = mg
+ }
+ }
+
+ // Prepare to register the collectors. Other than `MetricGroup` collectors,
+ // we also have standard collectors like `GoCollector`.
+
+ // Create all Non-`MetricGroup` collectors here.
+ collectors := map[collectorPath]prometheus.Collector{
+ debugGoCollectorPath: collectors.NewGoCollector(),
+ }
+
+ // Add all `MetricGroup` collectors to the map.
+ for _, mg := range allMetricGroups {
+ collectors[mg.CollectorPath] = mg
+ }
+
+ // Helper function to register a collector and return a gatherer for it.
+ mustRegister := func(c ...prometheus.Collector) prometheus.Gatherer {
+ subRegistry := prometheus.NewRegistry()
+ for _, col := range c {
+ subRegistry.MustRegister(col)
+ }
+ r.MustRegister(subRegistry)
+ return subRegistry
+ }
+
+ // Register all collectors and create gatherers for them.
+ gatherers := make(map[collectorPath]prometheus.Gatherer, len(collectors))
+ collectorPaths := make([]collectorPath, 0, len(collectors))
+ for path, collector := range collectors {
+ gatherers[path] = mustRegister(collector)
+ collectorPaths = append(collectorPaths, path)
+ }
+ slices.Sort(collectorPaths)
+ return &metricsV3Collection{
+ mgMap: mgMap,
+ bucketMGMap: bucketMGMap,
+ mgGatherers: gatherers,
+ collectorPaths: collectorPaths,
+ }
+}
diff --git a/cmd/metrics.go b/cmd/metrics.go
new file mode 100644
index 00000000..0548ff2b
--- /dev/null
+++ b/cmd/metrics.go
@@ -0,0 +1,573 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "net/http"
+ "time"
+
+ "github.com/minio/minio/internal/auth"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/minio/internal/mcontext"
+ "github.com/minio/pkg/v3/policy"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/common/expfmt"
+)
+
+var (
+ httpRequestsDuration = prometheus.NewHistogramVec(
+ prometheus.HistogramOpts{
+ Name: "s3_ttfb_seconds",
+ Help: "Time taken by requests served by current MinIO server instance",
+ Buckets: []float64{.05, .1, .25, .5, 1, 2.5, 5, 10},
+ },
+ []string{"api"},
+ )
+ bucketHTTPRequestsDuration = prometheus.NewHistogramVec(
+ prometheus.HistogramOpts{
+ Name: "s3_ttfb_seconds",
+ Help: "Time taken by requests served by current MinIO server instance per bucket",
+ Buckets: []float64{.05, .1, .25, .5, 1, 2.5, 5, 10},
+ },
+ []string{"api", "bucket"},
+ )
+ minioVersionInfo = prometheus.NewGaugeVec(
+ prometheus.GaugeOpts{
+ Namespace: "minio",
+ Name: "version_info",
+ Help: "Version of current MinIO server instance",
+ },
+ []string{
+ // current version
+ "version",
+ // commit-id of the current version
+ "commit",
+ },
+ )
+)
+
+const (
+ healMetricsNamespace = "self_heal"
+ cacheNamespace = "cache"
+ s3Namespace = "s3"
+ bucketNamespace = "bucket"
+ minioNamespace = "minio"
+ diskNamespace = "disk"
+ interNodeNamespace = "internode"
+)
+
+func init() {
+ prometheus.MustRegister(httpRequestsDuration)
+ prometheus.MustRegister(newMinioCollector())
+ prometheus.MustRegister(minioVersionInfo)
+}
+
+// newMinioCollector describes the collector
+// and returns reference of minioCollector
+// It creates the Prometheus Description which is used
+// to define metric and help string
+func newMinioCollector() *minioCollector {
+ return &minioCollector{
+ desc: prometheus.NewDesc("minio_stats", "Statistics exposed by MinIO server", nil, nil),
+ }
+}
+
+// minioCollector is the Custom Collector
+type minioCollector struct {
+ desc *prometheus.Desc
+}
+
+// Describe sends the super-set of all possible descriptors of metrics
+func (c *minioCollector) Describe(ch chan<- *prometheus.Desc) {
+ ch <- c.desc
+}
+
+// Collect is called by the Prometheus registry when collecting metrics.
+func (c *minioCollector) Collect(ch chan<- prometheus.Metric) {
+ // Expose MinIO's version information
+ minioVersionInfo.WithLabelValues(Version, CommitID).Set(1.0)
+
+ storageMetricsPrometheus(ch)
+ nodeHealthMetricsPrometheus(ch)
+ bucketUsageMetricsPrometheus(ch)
+ networkMetricsPrometheus(ch)
+ httpMetricsPrometheus(ch)
+ healingMetricsPrometheus(ch)
+}
+
+func nodeHealthMetricsPrometheus(ch chan<- prometheus.Metric) {
+ nodesUp, nodesDown := globalNotificationSys.GetPeerOnlineCount()
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(minioNamespace, "nodes", "online"),
+ "Total number of MinIO nodes online",
+ nil, nil),
+ prometheus.GaugeValue,
+ float64(nodesUp),
+ )
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(minioNamespace, "nodes", "offline"),
+ "Total number of MinIO nodes offline",
+ nil, nil),
+ prometheus.GaugeValue,
+ float64(nodesDown),
+ )
+}
+
+// collects healing specific metrics for MinIO instance in Prometheus specific format
+// and sends to given channel
+func healingMetricsPrometheus(ch chan<- prometheus.Metric) {
+ bgSeq, exists := globalBackgroundHealState.getHealSequenceByToken(bgHealingUUID)
+ if !exists {
+ return
+ }
+
+ var dur time.Duration
+ if !bgSeq.lastHealActivity.IsZero() {
+ dur = time.Since(bgSeq.lastHealActivity)
+ }
+
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(healMetricsNamespace, "time", "since_last_activity"),
+ "Time elapsed (in nano seconds) since last self healing activity. This is set to -1 until initial self heal activity",
+ nil, nil),
+ prometheus.GaugeValue,
+ float64(dur),
+ )
+ for k, v := range bgSeq.getScannedItemsMap() {
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(healMetricsNamespace, "objects", "scanned"),
+ "Objects scanned since uptime",
+ []string{"type"}, nil),
+ prometheus.CounterValue,
+ float64(v), string(k),
+ )
+ }
+ for k, v := range bgSeq.getHealedItemsMap() {
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(healMetricsNamespace, "objects", "healed"),
+ "Objects healed since uptime",
+ []string{"type"}, nil),
+ prometheus.CounterValue,
+ float64(v), string(k),
+ )
+ }
+ for k, v := range bgSeq.getHealFailedItemsMap() {
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(healMetricsNamespace, "objects", "heal_failed"),
+ "Objects for which healing failed since uptime",
+ []string{"type"}, nil),
+ prometheus.CounterValue,
+ float64(v), string(k),
+ )
+ }
+}
+
+// collects http metrics for MinIO server in Prometheus specific format
+// and sends to given channel
+func httpMetricsPrometheus(ch chan<- prometheus.Metric) {
+ httpStats := globalHTTPStats.toServerHTTPStats(true)
+
+ for api, value := range httpStats.CurrentS3Requests.APIStats {
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(s3Namespace, "requests", "current"),
+ "Total number of running s3 requests in current MinIO server instance",
+ []string{"api"}, nil),
+ prometheus.CounterValue,
+ float64(value),
+ api,
+ )
+ }
+
+ for api, value := range httpStats.TotalS3Requests.APIStats {
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(s3Namespace, "requests", "total"),
+ "Total number of s3 requests in current MinIO server instance",
+ []string{"api"}, nil),
+ prometheus.CounterValue,
+ float64(value),
+ api,
+ )
+ }
+
+ for api, value := range httpStats.TotalS3Errors.APIStats {
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(s3Namespace, "errors", "total"),
+ "Total number of s3 errors in current MinIO server instance",
+ []string{"api"}, nil),
+ prometheus.CounterValue,
+ float64(value),
+ api,
+ )
+ }
+
+ for api, value := range httpStats.TotalS3Canceled.APIStats {
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(s3Namespace, "canceled", "total"),
+ "Total number of client canceled s3 request in current MinIO server instance",
+ []string{"api"}, nil),
+ prometheus.CounterValue,
+ float64(value),
+ api,
+ )
+ }
+}
+
+// collects network metrics for MinIO server in Prometheus specific format
+// and sends to given channel
+func networkMetricsPrometheus(ch chan<- prometheus.Metric) {
+ connStats := globalConnStats.toServerConnStats()
+
+ // Network Sent/Received Bytes (internode)
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(interNodeNamespace, "tx", "bytes_total"),
+ "Total number of bytes sent to the other peer nodes by current MinIO server instance",
+ nil, nil),
+ prometheus.CounterValue,
+ float64(connStats.internodeOutputBytes),
+ )
+
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(interNodeNamespace, "rx", "bytes_total"),
+ "Total number of internode bytes received by current MinIO server instance",
+ nil, nil),
+ prometheus.CounterValue,
+ float64(connStats.internodeInputBytes),
+ )
+
+ // Network Sent/Received Bytes (Outbound)
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(s3Namespace, "tx", "bytes_total"),
+ "Total number of s3 bytes sent by current MinIO server instance",
+ nil, nil),
+ prometheus.CounterValue,
+ float64(connStats.s3OutputBytes),
+ )
+
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(s3Namespace, "rx", "bytes_total"),
+ "Total number of s3 bytes received by current MinIO server instance",
+ nil, nil),
+ prometheus.CounterValue,
+ float64(connStats.s3InputBytes),
+ )
+}
+
+// Populates prometheus with bucket usage metrics, this metrics
+// is only enabled if scanner is enabled.
+func bucketUsageMetricsPrometheus(ch chan<- prometheus.Metric) {
+ objLayer := newObjectLayerFn()
+ // Service not initialized yet
+ if objLayer == nil {
+ return
+ }
+
+ dataUsageInfo, err := loadDataUsageFromBackend(GlobalContext, objLayer)
+ if err != nil {
+ return
+ }
+ // data usage has not captured any data yet.
+ if dataUsageInfo.LastUpdate.IsZero() {
+ return
+ }
+
+ for bucket, usageInfo := range dataUsageInfo.BucketsUsage {
+ stat := globalReplicationStats.Load().getLatestReplicationStats(bucket)
+ // Total space used by bucket
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(bucketNamespace, "usage", "size"),
+ "Total bucket size",
+ []string{"bucket"}, nil),
+ prometheus.GaugeValue,
+ float64(usageInfo.Size),
+ bucket,
+ )
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(bucketNamespace, "objects", "count"),
+ "Total number of objects in a bucket",
+ []string{"bucket"}, nil),
+ prometheus.GaugeValue,
+ float64(usageInfo.ObjectsCount),
+ bucket,
+ )
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName("bucket", "replication", "successful_size"),
+ "Total capacity replicated to destination",
+ []string{"bucket"}, nil),
+ prometheus.GaugeValue,
+ float64(stat.ReplicationStats.ReplicatedSize),
+ bucket,
+ )
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName("bucket", "replication", "received_size"),
+ "Total capacity replicated to this instance",
+ []string{"bucket"}, nil),
+ prometheus.GaugeValue,
+ float64(stat.ReplicationStats.ReplicaSize),
+ bucket,
+ )
+
+ for k, v := range usageInfo.ObjectSizesHistogram {
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(bucketNamespace, "objects", "histogram"),
+ "Total number of objects of different sizes in a bucket",
+ []string{"bucket", "object_size"}, nil),
+ prometheus.GaugeValue,
+ float64(v),
+ bucket,
+ k,
+ )
+ }
+ for k, v := range usageInfo.ObjectVersionsHistogram {
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(bucketNamespace, "objects", "histogram"),
+ "Total number of versions of objects in a bucket",
+ []string{"bucket", "object_versions"}, nil),
+ prometheus.GaugeValue,
+ float64(v),
+ bucket,
+ k,
+ )
+ }
+ }
+}
+
+// collects storage metrics for MinIO server in Prometheus specific format
+// and sends to given channel
+func storageMetricsPrometheus(ch chan<- prometheus.Metric) {
+ objLayer := newObjectLayerFn()
+ // Service not initialized yet
+ if objLayer == nil {
+ return
+ }
+
+ server := getLocalServerProperty(globalEndpoints, &http.Request{
+ Host: globalLocalNodeName,
+ }, true)
+
+ onlineDisks, offlineDisks := getOnlineOfflineDisksStats(server.Disks)
+ totalDisks := offlineDisks.Merge(onlineDisks)
+
+ // Report total capacity
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(minioNamespace, "capacity_raw", "total"),
+ "Total capacity online in the cluster",
+ nil, nil),
+ prometheus.GaugeValue,
+ float64(GetTotalCapacity(server.Disks)),
+ )
+
+ // Report total capacity free
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(minioNamespace, "capacity_raw_free", "total"),
+ "Total free capacity online in the cluster",
+ nil, nil),
+ prometheus.GaugeValue,
+ float64(GetTotalCapacityFree(server.Disks)),
+ )
+
+ sinfo := objLayer.StorageInfo(GlobalContext, true)
+
+ // Report total usable capacity
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(minioNamespace, "capacity_usable", "total"),
+ "Total usable capacity online in the cluster",
+ nil, nil),
+ prometheus.GaugeValue,
+ float64(GetTotalUsableCapacity(server.Disks, sinfo)),
+ )
+
+ // Report total usable capacity free
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(minioNamespace, "capacity_usable_free", "total"),
+ "Total free usable capacity online in the cluster",
+ nil, nil),
+ prometheus.GaugeValue,
+ float64(GetTotalUsableCapacityFree(server.Disks, sinfo)),
+ )
+
+ // MinIO Offline Disks per node
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(minioNamespace, "disks", "offline"),
+ "Total number of offline drives in current MinIO server instance",
+ nil, nil),
+ prometheus.GaugeValue,
+ float64(offlineDisks.Sum()),
+ )
+
+ // MinIO Total Disks per node
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(minioNamespace, "drives", "total"),
+ "Total number of drives for current MinIO server instance",
+ nil, nil),
+ prometheus.GaugeValue,
+ float64(totalDisks.Sum()),
+ )
+
+ for _, disk := range server.Disks {
+ // Total disk usage by the disk
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(diskNamespace, "storage", "used"),
+ "Total disk storage used on the drive",
+ []string{"disk"}, nil),
+ prometheus.GaugeValue,
+ float64(disk.UsedSpace),
+ disk.DrivePath,
+ )
+
+ // Total available space in the disk
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(diskNamespace, "storage", "available"),
+ "Total available space left on the drive",
+ []string{"disk"}, nil),
+ prometheus.GaugeValue,
+ float64(disk.AvailableSpace),
+ disk.DrivePath,
+ )
+
+ // Total storage space of the disk
+ ch <- prometheus.MustNewConstMetric(
+ prometheus.NewDesc(
+ prometheus.BuildFQName(diskNamespace, "storage", "total"),
+ "Total space on the drive",
+ []string{"disk"}, nil),
+ prometheus.GaugeValue,
+ float64(disk.TotalSpace),
+ disk.DrivePath,
+ )
+ }
+}
+
+func metricsHandler() http.Handler {
+ registry := prometheus.NewRegistry()
+
+ logger.CriticalIf(GlobalContext, registry.Register(minioVersionInfo))
+
+ logger.CriticalIf(GlobalContext, registry.Register(newMinioCollector()))
+
+ gatherers := prometheus.Gatherers{
+ prometheus.DefaultGatherer,
+ registry,
+ }
+
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
+ if ok {
+ tc.FuncName = "handler.MetricsLegacy"
+ tc.ResponseRecorder.LogErrBody = true
+ }
+
+ mfs, err := gatherers.Gather()
+ if err != nil {
+ if len(mfs) == 0 {
+ writeErrorResponseJSON(r.Context(), w, toAdminAPIErr(r.Context(), err), r.URL)
+ return
+ }
+ }
+
+ contentType := expfmt.Negotiate(r.Header)
+ w.Header().Set("Content-Type", string(contentType))
+
+ enc := expfmt.NewEncoder(w, contentType)
+ for _, mf := range mfs {
+ if err := enc.Encode(mf); err != nil {
+ // client may disconnect for any reasons
+ // we do not have to log this.
+ return
+ }
+ }
+ if closer, ok := enc.(expfmt.Closer); ok {
+ closer.Close()
+ }
+ })
+}
+
+// NoAuthMiddleware no auth middle ware.
+func NoAuthMiddleware(h http.Handler) http.Handler {
+ return h
+}
+
+// AuthMiddleware checks if the bearer token is valid and authorized.
+func AuthMiddleware(h http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
+
+ claims, groups, owner, authErr := metricsRequestAuthenticate(r)
+ if authErr != nil || (claims != nil && !claims.VerifyIssuer("prometheus", true)) {
+ if ok {
+ tc.FuncName = "handler.MetricsAuth"
+ tc.ResponseRecorder.LogErrBody = true
+ }
+
+ writeErrorResponseJSON(r.Context(), w, toAdminAPIErr(r.Context(), errAuthentication), r.URL)
+ return
+ }
+
+ cred := auth.Credentials{
+ AccessKey: claims.AccessKey,
+ Claims: claims.Map(),
+ Groups: groups,
+ }
+
+ // For authenticated users apply IAM policy.
+ if !globalIAMSys.IsAllowed(policy.Args{
+ AccountName: cred.AccessKey,
+ Groups: cred.Groups,
+ Action: policy.PrometheusAdminAction,
+ ConditionValues: getConditionValues(r, "", cred),
+ IsOwner: owner,
+ Claims: cred.Claims,
+ }) {
+ if ok {
+ tc.FuncName = "handler.MetricsAuth"
+ tc.ResponseRecorder.LogErrBody = true
+ }
+
+ writeErrorResponseJSON(r.Context(), w, toAdminAPIErr(r.Context(), errAuthentication), r.URL)
+ return
+ }
+ h.ServeHTTP(w, r)
+ })
+}
diff --git a/cmd/mrf.go b/cmd/mrf.go
new file mode 100644
index 00000000..4d002c27
--- /dev/null
+++ b/cmd/mrf.go
@@ -0,0 +1,281 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+//go:generate msgp -file=$GOFILE
+
+package cmd
+
+import (
+ "context"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/pkg/v3/wildcard"
+ "github.com/tinylib/msgp/msgp"
+)
+
+const (
+ mrfOpsQueueSize = 100000
+)
+
+const (
+ healDir = ".heal"
+ healMRFDir = bucketMetaPrefix + SlashSeparator + healDir + SlashSeparator + "mrf"
+ healMRFMetaFormat = 1
+ healMRFMetaVersionV1 = 1
+)
+
+// PartialOperation is a successful upload/delete of an object
+// but not written in all disks (having quorum)
+type PartialOperation struct {
+ Bucket string
+ Object string
+ VersionID string
+ Versions []byte
+ SetIndex, PoolIndex int
+ Queued time.Time
+ BitrotScan bool
+}
+
+// mrfState sncapsulates all the information
+// related to the global background MRF.
+type mrfState struct {
+ opCh chan PartialOperation
+
+ closed int32
+ closing int32
+ wg sync.WaitGroup
+}
+
+func newMRFState() mrfState {
+ return mrfState{
+ opCh: make(chan PartialOperation, mrfOpsQueueSize),
+ }
+}
+
+// Add a partial S3 operation (put/delete) when one or more disks are offline.
+func (m *mrfState) addPartialOp(op PartialOperation) {
+ if m == nil {
+ return
+ }
+
+ if atomic.LoadInt32(&m.closed) == 1 {
+ return
+ }
+
+ m.wg.Add(1)
+ defer m.wg.Done()
+
+ if atomic.LoadInt32(&m.closing) == 1 {
+ return
+ }
+
+ select {
+ case m.opCh <- op:
+ default:
+ }
+}
+
+// Do not accept new MRF operations anymore and start to save
+// the current heal status in one available disk
+func (m *mrfState) shutdown() {
+ atomic.StoreInt32(&m.closing, 1)
+ m.wg.Wait()
+ close(m.opCh)
+ atomic.StoreInt32(&m.closed, 1)
+
+ if len(m.opCh) > 0 {
+ healingLogEvent(context.Background(), "Saving MRF healing data (%d entries)", len(m.opCh))
+ }
+
+ newReader := func() io.ReadCloser {
+ r, w := io.Pipe()
+ go func() {
+ // Initialize MRF meta header.
+ var data [4]byte
+ binary.LittleEndian.PutUint16(data[0:2], healMRFMetaFormat)
+ binary.LittleEndian.PutUint16(data[2:4], healMRFMetaVersionV1)
+ mw := msgp.NewWriter(w)
+ n, err := mw.Write(data[:])
+ if err != nil {
+ w.CloseWithError(err)
+ return
+ }
+ if n != len(data) {
+ w.CloseWithError(io.ErrShortWrite)
+ return
+ }
+ for item := range m.opCh {
+ err = item.EncodeMsg(mw)
+ if err != nil {
+ break
+ }
+ }
+ mw.Flush()
+ w.CloseWithError(err)
+ }()
+ return r
+ }
+
+ globalLocalDrivesMu.RLock()
+ localDrives := cloneDrives(globalLocalDrivesMap)
+ globalLocalDrivesMu.RUnlock()
+
+ for _, localDrive := range localDrives {
+ r := newReader()
+ err := localDrive.CreateFile(context.Background(), "", minioMetaBucket, pathJoin(healMRFDir, "list.bin"), -1, r)
+ r.Close()
+ if err == nil {
+ break
+ }
+ }
+}
+
+func (m *mrfState) startMRFPersistence() {
+ loadMRF := func(rc io.ReadCloser, opCh chan PartialOperation) error {
+ defer rc.Close()
+ var data [4]byte
+ n, err := rc.Read(data[:])
+ if err != nil {
+ return err
+ }
+ if n != len(data) {
+ return errors.New("heal mrf: no data")
+ }
+ // Read resync meta header
+ switch binary.LittleEndian.Uint16(data[0:2]) {
+ case healMRFMetaFormat:
+ default:
+ return fmt.Errorf("heal mrf: unknown format: %d", binary.LittleEndian.Uint16(data[0:2]))
+ }
+ switch binary.LittleEndian.Uint16(data[2:4]) {
+ case healMRFMetaVersionV1:
+ default:
+ return fmt.Errorf("heal mrf: unknown version: %d", binary.LittleEndian.Uint16(data[2:4]))
+ }
+
+ mr := msgp.NewReader(rc)
+ for {
+ op := PartialOperation{}
+ err = op.DecodeMsg(mr)
+ if err != nil {
+ break
+ }
+ opCh <- op
+ }
+
+ return nil
+ }
+
+ globalLocalDrivesMu.RLock()
+ localDrives := cloneDrives(globalLocalDrivesMap)
+ globalLocalDrivesMu.RUnlock()
+
+ for _, localDrive := range localDrives {
+ if localDrive == nil {
+ continue
+ }
+ rc, err := localDrive.ReadFileStream(context.Background(), minioMetaBucket, pathJoin(healMRFDir, "list.bin"), 0, -1)
+ if err != nil {
+ continue
+ }
+ err = loadMRF(rc, m.opCh)
+ if err != nil {
+ continue
+ }
+ // finally delete the file after processing mrf entries
+ localDrive.Delete(GlobalContext, minioMetaBucket, pathJoin(healMRFDir, "list.bin"), DeleteOptions{})
+ break
+ }
+}
+
+var healSleeper = newDynamicSleeper(5, time.Second, false)
+
+// healRoutine listens to new disks reconnection events and
+// issues healing requests for queued objects belonging to the
+// corresponding erasure set
+func (m *mrfState) healRoutine(z *erasureServerPools) {
+ for {
+ select {
+ case <-GlobalContext.Done():
+ return
+ case u, ok := <-m.opCh:
+ if !ok {
+ return
+ }
+
+ // We might land at .metacache, .trash, .multipart
+ // no need to heal them skip, only when bucket
+ // is '.minio.sys'
+ if u.Bucket == minioMetaBucket {
+ // No MRF needed for temporary objects
+ if wildcard.Match("buckets/*/.metacache/*", u.Object) {
+ continue
+ }
+ if wildcard.Match("tmp/*", u.Object) {
+ continue
+ }
+ if wildcard.Match("multipart/*", u.Object) {
+ continue
+ }
+ if wildcard.Match("tmp-old/*", u.Object) {
+ continue
+ }
+ }
+
+ now := time.Now()
+ if now.Sub(u.Queued) < time.Second {
+ // let recently failed networks to reconnect
+ // making MRF wait for 1s before retrying,
+ // i.e 4 reconnect attempts.
+ time.Sleep(time.Second)
+ }
+
+ // wait on timer per heal
+ wait := healSleeper.Timer(context.Background())
+
+ scan := madmin.HealNormalScan
+ if u.BitrotScan {
+ scan = madmin.HealDeepScan
+ }
+
+ if u.Object == "" {
+ healBucket(u.Bucket, scan)
+ } else {
+ if len(u.Versions) > 0 {
+ vers := len(u.Versions) / 16
+ if vers > 0 {
+ for i := range vers {
+ healObject(u.Bucket, u.Object, uuid.UUID(u.Versions[16*i:]).String(), scan)
+ }
+ }
+ } else {
+ healObject(u.Bucket, u.Object, u.VersionID, scan)
+ }
+ }
+
+ wait()
+ }
+ }
+}
diff --git a/cmd/mrf_gen.go b/cmd/mrf_gen.go
new file mode 100644
index 00000000..8c0467b5
--- /dev/null
+++ b/cmd/mrf_gen.go
@@ -0,0 +1,285 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "github.com/tinylib/msgp/msgp"
+)
+
+// DecodeMsg implements msgp.Decodable
+func (z *PartialOperation) DecodeMsg(dc *msgp.Reader) (err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, err = dc.ReadMapHeader()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, err = dc.ReadMapKeyPtr()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Bucket":
+ z.Bucket, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "Object":
+ z.Object, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ case "VersionID":
+ z.VersionID, err = dc.ReadString()
+ if err != nil {
+ err = msgp.WrapError(err, "VersionID")
+ return
+ }
+ case "Versions":
+ z.Versions, err = dc.ReadBytes(z.Versions)
+ if err != nil {
+ err = msgp.WrapError(err, "Versions")
+ return
+ }
+ case "SetIndex":
+ z.SetIndex, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "SetIndex")
+ return
+ }
+ case "PoolIndex":
+ z.PoolIndex, err = dc.ReadInt()
+ if err != nil {
+ err = msgp.WrapError(err, "PoolIndex")
+ return
+ }
+ case "Queued":
+ z.Queued, err = dc.ReadTime()
+ if err != nil {
+ err = msgp.WrapError(err, "Queued")
+ return
+ }
+ case "BitrotScan":
+ z.BitrotScan, err = dc.ReadBool()
+ if err != nil {
+ err = msgp.WrapError(err, "BitrotScan")
+ return
+ }
+ default:
+ err = dc.Skip()
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ return
+}
+
+// EncodeMsg implements msgp.Encodable
+func (z *PartialOperation) EncodeMsg(en *msgp.Writer) (err error) {
+ // map header, size 8
+ // write "Bucket"
+ err = en.Append(0x88, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Bucket)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ // write "Object"
+ err = en.Append(0xa6, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.Object)
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ // write "VersionID"
+ err = en.Append(0xa9, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x44)
+ if err != nil {
+ return
+ }
+ err = en.WriteString(z.VersionID)
+ if err != nil {
+ err = msgp.WrapError(err, "VersionID")
+ return
+ }
+ // write "Versions"
+ err = en.Append(0xa8, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73)
+ if err != nil {
+ return
+ }
+ err = en.WriteBytes(z.Versions)
+ if err != nil {
+ err = msgp.WrapError(err, "Versions")
+ return
+ }
+ // write "SetIndex"
+ err = en.Append(0xa8, 0x53, 0x65, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.SetIndex)
+ if err != nil {
+ err = msgp.WrapError(err, "SetIndex")
+ return
+ }
+ // write "PoolIndex"
+ err = en.Append(0xa9, 0x50, 0x6f, 0x6f, 0x6c, 0x49, 0x6e, 0x64, 0x65, 0x78)
+ if err != nil {
+ return
+ }
+ err = en.WriteInt(z.PoolIndex)
+ if err != nil {
+ err = msgp.WrapError(err, "PoolIndex")
+ return
+ }
+ // write "Queued"
+ err = en.Append(0xa6, 0x51, 0x75, 0x65, 0x75, 0x65, 0x64)
+ if err != nil {
+ return
+ }
+ err = en.WriteTime(z.Queued)
+ if err != nil {
+ err = msgp.WrapError(err, "Queued")
+ return
+ }
+ // write "BitrotScan"
+ err = en.Append(0xaa, 0x42, 0x69, 0x74, 0x72, 0x6f, 0x74, 0x53, 0x63, 0x61, 0x6e)
+ if err != nil {
+ return
+ }
+ err = en.WriteBool(z.BitrotScan)
+ if err != nil {
+ err = msgp.WrapError(err, "BitrotScan")
+ return
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *PartialOperation) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 8
+ // string "Bucket"
+ o = append(o, 0x88, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ o = msgp.AppendString(o, z.Bucket)
+ // string "Object"
+ o = append(o, 0xa6, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74)
+ o = msgp.AppendString(o, z.Object)
+ // string "VersionID"
+ o = append(o, 0xa9, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x44)
+ o = msgp.AppendString(o, z.VersionID)
+ // string "Versions"
+ o = append(o, 0xa8, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73)
+ o = msgp.AppendBytes(o, z.Versions)
+ // string "SetIndex"
+ o = append(o, 0xa8, 0x53, 0x65, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78)
+ o = msgp.AppendInt(o, z.SetIndex)
+ // string "PoolIndex"
+ o = append(o, 0xa9, 0x50, 0x6f, 0x6f, 0x6c, 0x49, 0x6e, 0x64, 0x65, 0x78)
+ o = msgp.AppendInt(o, z.PoolIndex)
+ // string "Queued"
+ o = append(o, 0xa6, 0x51, 0x75, 0x65, 0x75, 0x65, 0x64)
+ o = msgp.AppendTime(o, z.Queued)
+ // string "BitrotScan"
+ o = append(o, 0xaa, 0x42, 0x69, 0x74, 0x72, 0x6f, 0x74, 0x53, 0x63, 0x61, 0x6e)
+ o = msgp.AppendBool(o, z.BitrotScan)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *PartialOperation) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Bucket":
+ z.Bucket, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "Object":
+ z.Object, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ case "VersionID":
+ z.VersionID, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "VersionID")
+ return
+ }
+ case "Versions":
+ z.Versions, bts, err = msgp.ReadBytesBytes(bts, z.Versions)
+ if err != nil {
+ err = msgp.WrapError(err, "Versions")
+ return
+ }
+ case "SetIndex":
+ z.SetIndex, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "SetIndex")
+ return
+ }
+ case "PoolIndex":
+ z.PoolIndex, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "PoolIndex")
+ return
+ }
+ case "Queued":
+ z.Queued, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Queued")
+ return
+ }
+ case "BitrotScan":
+ z.BitrotScan, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "BitrotScan")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *PartialOperation) Msgsize() (s int) {
+ s = 1 + 7 + msgp.StringPrefixSize + len(z.Bucket) + 7 + msgp.StringPrefixSize + len(z.Object) + 10 + msgp.StringPrefixSize + len(z.VersionID) + 9 + msgp.BytesPrefixSize + len(z.Versions) + 9 + msgp.IntSize + 10 + msgp.IntSize + 7 + msgp.TimeSize + 11 + msgp.BoolSize
+ return
+}
diff --git a/cmd/mrf_gen_test.go b/cmd/mrf_gen_test.go
new file mode 100644
index 00000000..b8e039f8
--- /dev/null
+++ b/cmd/mrf_gen_test.go
@@ -0,0 +1,123 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/tinylib/msgp/msgp"
+)
+
+func TestMarshalUnmarshalPartialOperation(t *testing.T) {
+ v := PartialOperation{}
+ 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 BenchmarkMarshalMsgPartialOperation(b *testing.B) {
+ v := PartialOperation{}
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v.MarshalMsg(nil)
+ }
+}
+
+func BenchmarkAppendMsgPartialOperation(b *testing.B) {
+ v := PartialOperation{}
+ 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 BenchmarkUnmarshalPartialOperation(b *testing.B) {
+ v := PartialOperation{}
+ 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 TestEncodeDecodePartialOperation(t *testing.T) {
+ v := PartialOperation{}
+ var buf bytes.Buffer
+ msgp.Encode(&buf, &v)
+
+ m := v.Msgsize()
+ if buf.Len() > m {
+ t.Log("WARNING: TestEncodeDecodePartialOperation Msgsize() is inaccurate")
+ }
+
+ vn := PartialOperation{}
+ 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 BenchmarkEncodePartialOperation(b *testing.B) {
+ v := PartialOperation{}
+ 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 BenchmarkDecodePartialOperation(b *testing.B) {
+ v := PartialOperation{}
+ 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)
+ }
+ }
+}
diff --git a/cmd/namespace-lock.go b/cmd/namespace-lock.go
new file mode 100644
index 00000000..0cfe44de
--- /dev/null
+++ b/cmd/namespace-lock.go
@@ -0,0 +1,329 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ pathutil "path"
+ "runtime"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/minio/minio/internal/dsync"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/minio/internal/lsync"
+)
+
+// local lock servers
+var globalLockServer *localLocker
+
+// RWLocker - locker interface to introduce GetRLock, RUnlock.
+type RWLocker interface {
+ GetLock(ctx context.Context, timeout *dynamicTimeout) (lkCtx LockContext, timedOutErr error)
+ Unlock(lkCtx LockContext)
+ GetRLock(ctx context.Context, timeout *dynamicTimeout) (lkCtx LockContext, timedOutErr error)
+ RUnlock(lkCtx LockContext)
+}
+
+// LockContext lock context holds the lock backed context and canceler for the context.
+type LockContext struct {
+ ctx context.Context
+ cancel context.CancelFunc
+}
+
+// Context returns lock context
+func (l LockContext) Context() context.Context {
+ return l.ctx
+}
+
+// Cancel function calls cancel() function
+func (l LockContext) Cancel() {
+ if l.cancel != nil {
+ l.cancel()
+ }
+}
+
+// newNSLock - return a new name space lock map.
+func newNSLock(isDistErasure bool) *nsLockMap {
+ nsMutex := nsLockMap{
+ isDistErasure: isDistErasure,
+ }
+ if isDistErasure {
+ return &nsMutex
+ }
+ nsMutex.lockMap = make(map[string]*nsLock)
+ return &nsMutex
+}
+
+// nsLock - provides primitives for locking critical namespace regions.
+type nsLock struct {
+ ref int32
+ *lsync.LRWMutex
+}
+
+// nsLockMap - namespace lock map, provides primitives to Lock,
+// Unlock, RLock and RUnlock.
+type nsLockMap struct {
+ // Indicates if namespace is part of a distributed setup.
+ isDistErasure bool
+ lockMap map[string]*nsLock
+ lockMapMutex sync.Mutex
+}
+
+// Lock the namespace resource.
+func (n *nsLockMap) lock(ctx context.Context, volume string, path string, lockSource, opsID string, readLock bool, timeout time.Duration) (locked bool) {
+ resource := pathJoin(volume, path)
+
+ n.lockMapMutex.Lock()
+ nsLk, found := n.lockMap[resource]
+ if !found {
+ nsLk = &nsLock{
+ LRWMutex: lsync.NewLRWMutex(),
+ }
+ // Add a count to indicate that a parallel unlock doesn't clear this entry.
+ }
+ nsLk.ref++
+ n.lockMap[resource] = nsLk
+ n.lockMapMutex.Unlock()
+
+ // Locking here will block (until timeout).
+ if readLock {
+ locked = nsLk.GetRLock(ctx, opsID, lockSource, timeout)
+ } else {
+ locked = nsLk.GetLock(ctx, opsID, lockSource, timeout)
+ }
+
+ if !locked { // We failed to get the lock
+ // Decrement ref count since we failed to get the lock
+ n.lockMapMutex.Lock()
+ n.lockMap[resource].ref--
+ if n.lockMap[resource].ref < 0 {
+ logger.CriticalIf(GlobalContext, errors.New("resource reference count was lower than 0"))
+ }
+ if n.lockMap[resource].ref == 0 {
+ // Remove from the map if there are no more references.
+ delete(n.lockMap, resource)
+ }
+ n.lockMapMutex.Unlock()
+ }
+
+ return locked
+}
+
+// Unlock the namespace resource.
+func (n *nsLockMap) unlock(volume string, path string, readLock bool) {
+ resource := pathJoin(volume, path)
+
+ n.lockMapMutex.Lock()
+ defer n.lockMapMutex.Unlock()
+ if _, found := n.lockMap[resource]; !found {
+ return
+ }
+ if readLock {
+ n.lockMap[resource].RUnlock()
+ } else {
+ n.lockMap[resource].Unlock()
+ }
+ n.lockMap[resource].ref--
+ if n.lockMap[resource].ref < 0 {
+ logger.CriticalIf(GlobalContext, errors.New("resource reference count was lower than 0"))
+ }
+ if n.lockMap[resource].ref == 0 {
+ // Remove from the map if there are no more references.
+ delete(n.lockMap, resource)
+ }
+}
+
+// dsync's distributed lock instance.
+type distLockInstance struct {
+ rwMutex *dsync.DRWMutex
+ opsID string
+}
+
+// Lock - block until write lock is taken or timeout has occurred.
+func (di *distLockInstance) GetLock(ctx context.Context, timeout *dynamicTimeout) (LockContext, error) {
+ lockSource := getSource(2)
+ start := UTCNow()
+
+ newCtx, cancel := context.WithCancel(ctx)
+ if !di.rwMutex.GetLock(newCtx, cancel, di.opsID, lockSource, dsync.Options{
+ Timeout: timeout.Timeout(),
+ RetryInterval: timeout.RetryInterval(),
+ }) {
+ timeout.LogFailure()
+ defer cancel()
+ if err := newCtx.Err(); err == context.Canceled {
+ return LockContext{ctx: ctx, cancel: func() {}}, err
+ }
+ return LockContext{ctx: ctx, cancel: func() {}}, OperationTimedOut{}
+ }
+ timeout.LogSuccess(UTCNow().Sub(start))
+ return LockContext{ctx: newCtx, cancel: cancel}, nil
+}
+
+// Unlock - block until write lock is released.
+func (di *distLockInstance) Unlock(lc LockContext) {
+ if lc.cancel != nil {
+ lc.cancel()
+ }
+ di.rwMutex.Unlock(context.Background())
+}
+
+// RLock - block until read lock is taken or timeout has occurred.
+func (di *distLockInstance) GetRLock(ctx context.Context, timeout *dynamicTimeout) (LockContext, error) {
+ lockSource := getSource(2)
+ start := UTCNow()
+
+ newCtx, cancel := context.WithCancel(ctx)
+ if !di.rwMutex.GetRLock(ctx, cancel, di.opsID, lockSource, dsync.Options{
+ Timeout: timeout.Timeout(),
+ RetryInterval: timeout.RetryInterval(),
+ }) {
+ timeout.LogFailure()
+ defer cancel()
+ if errors.Is(newCtx.Err(), context.Canceled) {
+ return LockContext{ctx: ctx, cancel: func() {}}, newCtx.Err()
+ }
+ return LockContext{ctx: ctx, cancel: func() {}}, OperationTimedOut{}
+ }
+ timeout.LogSuccess(UTCNow().Sub(start))
+ return LockContext{ctx: newCtx, cancel: cancel}, nil
+}
+
+// RUnlock - block until read lock is released.
+func (di *distLockInstance) RUnlock(lc LockContext) {
+ if lc.cancel != nil {
+ lc.cancel()
+ }
+ di.rwMutex.RUnlock(lc.ctx)
+}
+
+// localLockInstance - frontend/top-level interface for namespace locks.
+type localLockInstance struct {
+ ns *nsLockMap
+ volume string
+ paths []string
+ opsID string
+}
+
+// NewNSLock - returns a lock instance for a given volume and
+// path. The returned lockInstance object encapsulates the nsLockMap,
+// volume, path and operation ID.
+func (n *nsLockMap) NewNSLock(lockers func() ([]dsync.NetLocker, string), volume string, paths ...string) RWLocker {
+ sort.Strings(paths)
+ opsID := mustGetUUID()
+ if n.isDistErasure {
+ drwmutex := dsync.NewDRWMutex(&dsync.Dsync{
+ GetLockers: lockers,
+ Timeouts: dsync.DefaultTimeouts,
+ }, pathsJoinPrefix(volume, paths...)...)
+ return &distLockInstance{drwmutex, opsID}
+ }
+ return &localLockInstance{n, volume, paths, opsID}
+}
+
+// Lock - block until write lock is taken or timeout has occurred.
+func (li *localLockInstance) GetLock(ctx context.Context, timeout *dynamicTimeout) (_ LockContext, timedOutErr error) {
+ lockSource := getSource(2)
+ start := UTCNow()
+ const readLock = false
+ success := make([]int, len(li.paths))
+ for i, path := range li.paths {
+ if !li.ns.lock(ctx, li.volume, path, lockSource, li.opsID, readLock, timeout.Timeout()) {
+ timeout.LogFailure()
+ for si, sint := range success {
+ if sint == 1 {
+ li.ns.unlock(li.volume, li.paths[si], readLock)
+ }
+ }
+ if errors.Is(ctx.Err(), context.Canceled) {
+ return LockContext{}, ctx.Err()
+ }
+ return LockContext{}, OperationTimedOut{}
+ }
+ success[i] = 1
+ }
+ timeout.LogSuccess(UTCNow().Sub(start))
+ return LockContext{ctx: ctx, cancel: func() {}}, nil
+}
+
+// Unlock - block until write lock is released.
+func (li *localLockInstance) Unlock(lc LockContext) {
+ if lc.cancel != nil {
+ lc.cancel()
+ }
+ const readLock = false
+ for _, path := range li.paths {
+ li.ns.unlock(li.volume, path, readLock)
+ }
+}
+
+// RLock - block until read lock is taken or timeout has occurred.
+func (li *localLockInstance) GetRLock(ctx context.Context, timeout *dynamicTimeout) (_ LockContext, timedOutErr error) {
+ lockSource := getSource(2)
+ start := UTCNow()
+ const readLock = true
+ success := make([]int, len(li.paths))
+ for i, path := range li.paths {
+ if !li.ns.lock(ctx, li.volume, path, lockSource, li.opsID, readLock, timeout.Timeout()) {
+ timeout.LogFailure()
+ for si, sint := range success {
+ if sint == 1 {
+ li.ns.unlock(li.volume, li.paths[si], readLock)
+ }
+ }
+ if err := ctx.Err(); err == context.Canceled {
+ return LockContext{}, err
+ }
+ return LockContext{}, OperationTimedOut{}
+ }
+ success[i] = 1
+ }
+ timeout.LogSuccess(UTCNow().Sub(start))
+ return LockContext{ctx: ctx, cancel: func() {}}, nil
+}
+
+// RUnlock - block until read lock is released.
+func (li *localLockInstance) RUnlock(lc LockContext) {
+ if lc.cancel != nil {
+ lc.cancel()
+ }
+ const readLock = true
+ for _, path := range li.paths {
+ li.ns.unlock(li.volume, path, readLock)
+ }
+}
+
+func getSource(n int) string {
+ var funcName string
+ pc, filename, lineNum, ok := runtime.Caller(n)
+ if ok {
+ filename = pathutil.Base(filename)
+ funcName = strings.TrimPrefix(runtime.FuncForPC(pc).Name(),
+ "github.com/minio/minio/cmd.")
+ } else {
+ filename = ""
+ lineNum = 0
+ }
+
+ return fmt.Sprintf("[%s:%d:%s()]", filename, lineNum, funcName)
+}
diff --git a/cmd/namespace-lock_test.go b/cmd/namespace-lock_test.go
new file mode 100644
index 00000000..3f44e5f9
--- /dev/null
+++ b/cmd/namespace-lock_test.go
@@ -0,0 +1,102 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "runtime"
+ "testing"
+ "time"
+)
+
+// WARNING:
+//
+// Expected source line number is hard coded, 35, in the
+// following test. Adding new code before this test or changing its
+// position will cause the line number to change and the test to FAIL
+// Tests getSource().
+func TestGetSource(t *testing.T) {
+ currentSource := func() string { return getSource(2) }
+ gotSource := currentSource()
+ // Hard coded line number, 34, in the "expectedSource" value
+ expectedSource := "[namespace-lock_test.go:34:TestGetSource()]"
+ if gotSource != expectedSource {
+ t.Errorf("expected : %s, got : %s", expectedSource, gotSource)
+ }
+}
+
+// Test lock race
+func TestNSLockRace(t *testing.T) {
+ t.Skip("long test skip it")
+
+ ctx := t.Context()
+
+ for i := range 10000 {
+ nsLk := newNSLock(false)
+
+ // lk1; ref=1
+ if !nsLk.lock(ctx, "volume", "path", "source", "opsID", false, time.Second) {
+ t.Fatal("failed to acquire lock")
+ }
+
+ // lk2
+ lk2ch := make(chan struct{})
+ go func() {
+ defer close(lk2ch)
+ nsLk.lock(ctx, "volume", "path", "source", "opsID", false, 1*time.Millisecond)
+ }()
+ time.Sleep(1 * time.Millisecond) // wait for goroutine to advance; ref=2
+
+ // Unlock the 1st lock; ref=1 after this line
+ nsLk.unlock("volume", "path", false)
+
+ // Taking another lockMapMutex here allows queuing up additional lockers. This should
+ // not be required but makes reproduction much easier.
+ nsLk.lockMapMutex.Lock()
+
+ // lk3 blocks.
+ lk3ch := make(chan bool)
+ go func() {
+ lk3ch <- nsLk.lock(ctx, "volume", "path", "source", "opsID", false, 0)
+ }()
+
+ // lk4, blocks.
+ lk4ch := make(chan bool)
+ go func() {
+ lk4ch <- nsLk.lock(ctx, "volume", "path", "source", "opsID", false, 0)
+ }()
+ runtime.Gosched()
+
+ // unlock the manual lock
+ nsLk.lockMapMutex.Unlock()
+
+ // To trigger the race:
+ // 1) lk3 or lk4 need to advance and increment the ref on the existing resource,
+ // successfully acquiring the lock.
+ // 2) lk2 then needs to advance and remove the resource from lockMap.
+ // 3) lk3 or lk4 (whichever didn't execute in step 1) then executes and creates
+ // a new entry in lockMap and acquires a lock for the same resource.
+
+ <-lk2ch
+ lk3ok := <-lk3ch
+ lk4ok := <-lk4ch
+
+ if lk3ok && lk4ok {
+ t.Fatalf("multiple locks acquired; iteration=%d, lk3=%t, lk4=%t", i, lk3ok, lk4ok)
+ }
+ }
+}
diff --git a/cmd/naughty-disk_test.go b/cmd/naughty-disk_test.go
new file mode 100644
index 00000000..33162253
--- /dev/null
+++ b/cmd/naughty-disk_test.go
@@ -0,0 +1,340 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "io"
+ "sync"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+)
+
+// naughtyDisk wraps a POSIX disk and returns programmed errors
+// specified by the developer. The purpose is to simulate errors
+// that are hard to simulate in practice like DiskNotFound.
+// Programmed errors are stored in errors field.
+type naughtyDisk struct {
+ // The real disk
+ disk StorageAPI
+ // Programmed errors: API call number => error to return
+ errors map[int]error
+ // The error to return when no error value is programmed
+ defaultErr error
+ // The current API call number
+ callNR int
+ // Data protection
+ mu sync.Mutex
+}
+
+func newNaughtyDisk(d StorageAPI, errs map[int]error, defaultErr error) *naughtyDisk {
+ return &naughtyDisk{disk: d, errors: errs, defaultErr: defaultErr}
+}
+
+func (d *naughtyDisk) String() string {
+ return d.disk.String()
+}
+
+func (d *naughtyDisk) IsOnline() bool {
+ if err := d.calcError(); err != nil {
+ return err == errDiskNotFound
+ }
+ return d.disk.IsOnline()
+}
+
+func (d *naughtyDisk) LastConn() time.Time {
+ return d.disk.LastConn()
+}
+
+func (d *naughtyDisk) IsLocal() bool {
+ return d.disk.IsLocal()
+}
+
+func (d *naughtyDisk) Endpoint() Endpoint {
+ return d.disk.Endpoint()
+}
+
+func (d *naughtyDisk) Hostname() string {
+ return d.disk.Hostname()
+}
+
+func (d *naughtyDisk) Healing() *healingTracker {
+ return d.disk.Healing()
+}
+
+func (d *naughtyDisk) Close() (err error) {
+ if err = d.calcError(); err != nil {
+ return err
+ }
+ return d.disk.Close()
+}
+
+func (d *naughtyDisk) calcError() (err error) {
+ d.mu.Lock()
+ defer d.mu.Unlock()
+ d.callNR++
+ if err, ok := d.errors[d.callNR]; ok {
+ return err
+ }
+ if d.defaultErr != nil {
+ return d.defaultErr
+ }
+ return nil
+}
+
+func (d *naughtyDisk) GetDiskLoc() (poolIdx, setIdx, diskIdx int) {
+ return -1, -1, -1
+}
+
+func (d *naughtyDisk) GetDiskID() (string, error) {
+ return d.disk.GetDiskID()
+}
+
+func (d *naughtyDisk) SetDiskID(id string) {
+ d.disk.SetDiskID(id)
+}
+
+func (d *naughtyDisk) NSScanner(ctx context.Context, cache dataUsageCache, updates chan<- dataUsageEntry, scanMode madmin.HealScanMode, weSleep func() bool) (info dataUsageCache, err error) {
+ if err := d.calcError(); err != nil {
+ return info, err
+ }
+ return d.disk.NSScanner(ctx, cache, updates, scanMode, weSleep)
+}
+
+func (d *naughtyDisk) DiskInfo(ctx context.Context, opts DiskInfoOptions) (info DiskInfo, err error) {
+ if err := d.calcError(); err != nil {
+ return info, err
+ }
+ return d.disk.DiskInfo(ctx, opts)
+}
+
+func (d *naughtyDisk) MakeVolBulk(ctx context.Context, volumes ...string) (err error) {
+ if err := d.calcError(); err != nil {
+ return err
+ }
+ return d.disk.MakeVolBulk(ctx, volumes...)
+}
+
+func (d *naughtyDisk) MakeVol(ctx context.Context, volume string) (err error) {
+ if err := d.calcError(); err != nil {
+ return err
+ }
+ return d.disk.MakeVol(ctx, volume)
+}
+
+func (d *naughtyDisk) ListVols(ctx context.Context) (vols []VolInfo, err error) {
+ if err := d.calcError(); err != nil {
+ return nil, err
+ }
+ return d.disk.ListVols(ctx)
+}
+
+func (d *naughtyDisk) StatVol(ctx context.Context, volume string) (vol VolInfo, err error) {
+ if err := d.calcError(); err != nil {
+ return VolInfo{}, err
+ }
+ return d.disk.StatVol(ctx, volume)
+}
+
+func (d *naughtyDisk) DeleteVol(ctx context.Context, volume string, forceDelete bool) (err error) {
+ if err := d.calcError(); err != nil {
+ return err
+ }
+ return d.disk.DeleteVol(ctx, volume, forceDelete)
+}
+
+func (d *naughtyDisk) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writer) error {
+ if err := d.calcError(); err != nil {
+ return err
+ }
+ return d.disk.WalkDir(ctx, opts, wr)
+}
+
+func (d *naughtyDisk) ListDir(ctx context.Context, origvolume, volume, dirPath string, count int) (entries []string, err error) {
+ if err := d.calcError(); err != nil {
+ return []string{}, err
+ }
+ return d.disk.ListDir(ctx, origvolume, volume, dirPath, count)
+}
+
+func (d *naughtyDisk) ReadFile(ctx context.Context, volume string, path string, offset int64, buf []byte, verifier *BitrotVerifier) (n int64, err error) {
+ if err := d.calcError(); err != nil {
+ return 0, err
+ }
+ return d.disk.ReadFile(ctx, volume, path, offset, buf, verifier)
+}
+
+func (d *naughtyDisk) ReadFileStream(ctx context.Context, volume, path string, offset, length int64) (io.ReadCloser, error) {
+ if err := d.calcError(); err != nil {
+ return nil, err
+ }
+ return d.disk.ReadFileStream(ctx, volume, path, offset, length)
+}
+
+func (d *naughtyDisk) CreateFile(ctx context.Context, origvolume, volume, path string, size int64, reader io.Reader) error {
+ if err := d.calcError(); err != nil {
+ return err
+ }
+ return d.disk.CreateFile(ctx, origvolume, volume, path, size, reader)
+}
+
+func (d *naughtyDisk) AppendFile(ctx context.Context, volume string, path string, buf []byte) error {
+ if err := d.calcError(); err != nil {
+ return err
+ }
+ return d.disk.AppendFile(ctx, volume, path, buf)
+}
+
+func (d *naughtyDisk) RenameData(ctx context.Context, srcVolume, srcPath string, fi FileInfo, dstVolume, dstPath string, opts RenameOptions) (RenameDataResp, error) {
+ if err := d.calcError(); err != nil {
+ return RenameDataResp{}, err
+ }
+ return d.disk.RenameData(ctx, srcVolume, srcPath, fi, dstVolume, dstPath, opts)
+}
+
+func (d *naughtyDisk) RenamePart(ctx context.Context, srcVolume, srcPath, dstVolume, dstPath string, meta []byte, skipParent string) error {
+ if err := d.calcError(); err != nil {
+ return err
+ }
+ return d.disk.RenamePart(ctx, srcVolume, srcPath, dstVolume, dstPath, meta, skipParent)
+}
+
+func (d *naughtyDisk) ReadParts(ctx context.Context, bucket string, partMetaPaths ...string) ([]*ObjectPartInfo, error) {
+ if err := d.calcError(); err != nil {
+ return nil, err
+ }
+ return d.disk.ReadParts(ctx, bucket, partMetaPaths...)
+}
+
+func (d *naughtyDisk) RenameFile(ctx context.Context, srcVolume, srcPath, dstVolume, dstPath string) error {
+ if err := d.calcError(); err != nil {
+ return err
+ }
+ return d.disk.RenameFile(ctx, srcVolume, srcPath, dstVolume, dstPath)
+}
+
+func (d *naughtyDisk) CheckParts(ctx context.Context, volume string, path string, fi FileInfo) (*CheckPartsResp, error) {
+ if err := d.calcError(); err != nil {
+ return nil, err
+ }
+ return d.disk.CheckParts(ctx, volume, path, fi)
+}
+
+func (d *naughtyDisk) DeleteBulk(ctx context.Context, volume string, paths ...string) (err error) {
+ if err := d.calcError(); err != nil {
+ return err
+ }
+ return d.disk.DeleteBulk(ctx, volume, paths...)
+}
+
+func (d *naughtyDisk) Delete(ctx context.Context, volume string, path string, deleteOpts DeleteOptions) (err error) {
+ if err := d.calcError(); err != nil {
+ return err
+ }
+ return d.disk.Delete(ctx, volume, path, deleteOpts)
+}
+
+func (d *naughtyDisk) DeleteVersions(ctx context.Context, volume string, versions []FileInfoVersions, opts DeleteOptions) []error {
+ if err := d.calcError(); err != nil {
+ errs := make([]error, len(versions))
+ for i := range errs {
+ errs[i] = err
+ }
+ return errs
+ }
+ return d.disk.DeleteVersions(ctx, volume, versions, opts)
+}
+
+func (d *naughtyDisk) WriteMetadata(ctx context.Context, origvolume, volume, path string, fi FileInfo) (err error) {
+ if err := d.calcError(); err != nil {
+ return err
+ }
+ return d.disk.WriteMetadata(ctx, origvolume, volume, path, fi)
+}
+
+func (d *naughtyDisk) UpdateMetadata(ctx context.Context, volume, path string, fi FileInfo, opts UpdateMetadataOpts) (err error) {
+ if err := d.calcError(); err != nil {
+ return err
+ }
+ return d.disk.UpdateMetadata(ctx, volume, path, fi, opts)
+}
+
+func (d *naughtyDisk) DeleteVersion(ctx context.Context, volume, path string, fi FileInfo, forceDelMarker bool, opts DeleteOptions) (err error) {
+ if err := d.calcError(); err != nil {
+ return err
+ }
+ return d.disk.DeleteVersion(ctx, volume, path, fi, forceDelMarker, opts)
+}
+
+func (d *naughtyDisk) ReadVersion(ctx context.Context, origvolume, volume, path, versionID string, opts ReadOptions) (fi FileInfo, err error) {
+ if err := d.calcError(); err != nil {
+ return FileInfo{}, err
+ }
+ return d.disk.ReadVersion(ctx, origvolume, volume, path, versionID, opts)
+}
+
+func (d *naughtyDisk) WriteAll(ctx context.Context, volume string, path string, b []byte) (err error) {
+ if err := d.calcError(); err != nil {
+ return err
+ }
+ return d.disk.WriteAll(ctx, volume, path, b)
+}
+
+func (d *naughtyDisk) ReadAll(ctx context.Context, volume string, path string) (buf []byte, err error) {
+ if err := d.calcError(); err != nil {
+ return nil, err
+ }
+ return d.disk.ReadAll(ctx, volume, path)
+}
+
+func (d *naughtyDisk) ReadXL(ctx context.Context, volume string, path string, readData bool) (rf RawFileInfo, err error) {
+ if err := d.calcError(); err != nil {
+ return rf, err
+ }
+ return d.disk.ReadXL(ctx, volume, path, readData)
+}
+
+func (d *naughtyDisk) VerifyFile(ctx context.Context, volume, path string, fi FileInfo) (*CheckPartsResp, error) {
+ if err := d.calcError(); err != nil {
+ return nil, err
+ }
+ return d.disk.VerifyFile(ctx, volume, path, fi)
+}
+
+func (d *naughtyDisk) StatInfoFile(ctx context.Context, volume, path string, glob bool) (stat []StatInfo, err error) {
+ if err := d.calcError(); err != nil {
+ return stat, err
+ }
+ return d.disk.StatInfoFile(ctx, volume, path, glob)
+}
+
+func (d *naughtyDisk) ReadMultiple(ctx context.Context, req ReadMultipleReq, resp chan<- ReadMultipleResp) error {
+ if err := d.calcError(); err != nil {
+ close(resp)
+ return err
+ }
+ return d.disk.ReadMultiple(ctx, req, resp)
+}
+
+func (d *naughtyDisk) CleanAbandonedData(ctx context.Context, volume string, path string) error {
+ if err := d.calcError(); err != nil {
+ return err
+ }
+ return d.disk.CleanAbandonedData(ctx, volume, path)
+}
diff --git a/cmd/net.go b/cmd/net.go
new file mode 100644
index 00000000..f0462851
--- /dev/null
+++ b/cmd/net.go
@@ -0,0 +1,379 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "errors"
+ "net"
+ "net/url"
+ "runtime"
+ "sort"
+ "strings"
+
+ "github.com/minio/minio-go/v7/pkg/set"
+ "github.com/minio/minio/internal/config"
+ "github.com/minio/minio/internal/logger"
+ xnet "github.com/minio/pkg/v3/net"
+)
+
+var (
+ // IPv4 addresses of localhost.
+ localIP4 = mustGetLocalIP4()
+
+ // IPv6 addresses of localhost.
+ localIP6 = mustGetLocalIP6()
+
+ // List of all local loopback addresses.
+ localLoopbacks = mustGetLocalLoopbacks()
+)
+
+// mustSplitHostPort is a wrapper to net.SplitHostPort() where error is assumed to be a fatal.
+func mustSplitHostPort(hostPort string) (host, port string) {
+ xh, err := xnet.ParseHost(hostPort)
+ if err != nil {
+ logger.FatalIf(err, "Unable to split host port %s", hostPort)
+ }
+ return xh.Name, xh.Port.String()
+}
+
+// mustGetLocalIPs returns IPs of local interface
+func mustGetLocalIPs() (ipList []net.IP) {
+ ifs, err := net.Interfaces()
+ logger.FatalIf(err, "Unable to get IP addresses of this host")
+
+ for _, interf := range ifs {
+ addrs, err := interf.Addrs()
+ if err != nil {
+ continue
+ }
+ if runtime.GOOS == "windows" && interf.Flags&net.FlagUp == 0 {
+ continue
+ }
+
+ for _, addr := range addrs {
+ var ip net.IP
+ switch v := addr.(type) {
+ case *net.IPNet:
+ ip = v.IP
+ case *net.IPAddr:
+ ip = v.IP
+ }
+
+ ipList = append(ipList, ip)
+ }
+ }
+
+ return ipList
+}
+
+func mustGetLocalLoopbacks() (ipList set.StringSet) {
+ ipList = set.NewStringSet()
+ for _, ip := range mustGetLocalIPs() {
+ if ip != nil && ip.IsLoopback() {
+ ipList.Add(ip.String())
+ }
+ }
+ return ipList
+}
+
+// mustGetLocalIP4 returns IPv4 addresses of localhost. It panics on error.
+func mustGetLocalIP4() (ipList set.StringSet) {
+ ipList = set.NewStringSet()
+ for _, ip := range mustGetLocalIPs() {
+ if ip.To4() != nil {
+ ipList.Add(ip.String())
+ }
+ }
+ return ipList
+}
+
+// mustGetLocalIP6 returns IPv6 addresses of localhost. It panics on error.
+func mustGetLocalIP6() (ipList set.StringSet) {
+ ipList = set.NewStringSet()
+ for _, ip := range mustGetLocalIPs() {
+ if ip.To4() == nil {
+ ipList.Add(ip.String())
+ }
+ }
+ return ipList
+}
+
+// getHostIP returns IP address of given host.
+func getHostIP(host string) (ipList set.StringSet, err error) {
+ addrs, err := globalDNSCache.LookupHost(GlobalContext, host)
+ if err != nil {
+ return ipList, err
+ }
+
+ ipList = set.NewStringSet()
+ for _, addr := range addrs {
+ ipList.Add(addr)
+ }
+
+ return ipList, err
+}
+
+// sortIPs - sort ips based on higher octets.
+// The logic to sort by last octet is implemented to
+// prefer CIDRs with higher octets, this in-turn skips the
+// localhost/loopback address to be not preferred as the
+// first ip on the list. Subsequently this list helps us print
+// a user friendly message with appropriate values.
+func sortIPs(ipList []string) []string {
+ if len(ipList) == 1 {
+ return ipList
+ }
+
+ var ipV4s []net.IP
+ var nonIPs []string
+ for _, ip := range ipList {
+ nip := net.ParseIP(ip)
+ if nip != nil {
+ ipV4s = append(ipV4s, nip)
+ } else {
+ nonIPs = append(nonIPs, ip)
+ }
+ }
+
+ sort.Slice(ipV4s, func(i, j int) bool {
+ // This case is needed when all ips in the list
+ // have same last octets, Following just ensures that
+ // 127.0.0.1 is moved to the end of the list.
+ if ipV4s[i].IsLoopback() {
+ return false
+ }
+ if ipV4s[j].IsLoopback() {
+ return true
+ }
+ return []byte(ipV4s[i].To4())[3] > []byte(ipV4s[j].To4())[3]
+ })
+
+ var ips []string
+ for _, ip := range ipV4s {
+ ips = append(ips, ip.String())
+ }
+
+ return append(nonIPs, ips...)
+}
+
+func getConsoleEndpoints() (consoleEndpoints []string) {
+ if globalBrowserRedirectURL != nil {
+ return []string{globalBrowserRedirectURL.String()}
+ }
+ var ipList []string
+ if globalMinioConsoleHost == "" {
+ ipList = sortIPs(localIP4.ToSlice())
+ ipList = append(ipList, localIP6.ToSlice()...)
+ } else {
+ ipList = []string{globalMinioConsoleHost}
+ }
+
+ consoleEndpoints = make([]string, 0, len(ipList))
+ for _, ip := range ipList {
+ consoleEndpoints = append(consoleEndpoints, getURLScheme(globalIsTLS)+"://"+net.JoinHostPort(ip, globalMinioConsolePort))
+ }
+
+ return consoleEndpoints
+}
+
+func getAPIEndpoints() (apiEndpoints []string) {
+ if globalMinioEndpoint != "" {
+ return []string{globalMinioEndpoint}
+ }
+ var ipList []string
+ if globalMinioHost == "" {
+ ipList = sortIPs(localIP4.ToSlice())
+ ipList = append(ipList, localIP6.ToSlice()...)
+ } else {
+ ipList = []string{globalMinioHost}
+ }
+
+ apiEndpoints = make([]string, 0, len(ipList))
+ for _, ip := range ipList {
+ apiEndpoints = append(apiEndpoints, getURLScheme(globalIsTLS)+"://"+net.JoinHostPort(ip, globalMinioPort))
+ }
+
+ return apiEndpoints
+}
+
+// isHostIP - helper for validating if the provided arg is an ip address.
+func isHostIP(ipAddress string) bool {
+ host, _, err := net.SplitHostPort(ipAddress)
+ if err != nil {
+ host = ipAddress
+ }
+ // Strip off IPv6 zone information.
+ if i := strings.Index(host, "%"); i > -1 {
+ host = host[:i]
+ }
+ return net.ParseIP(host) != nil
+}
+
+// extractHostPort - extracts host/port from many address formats
+// such as, ":9000", "localhost:9000", "http://localhost:9000/"
+func extractHostPort(hostAddr string) (string, string, error) {
+ var addr, scheme string
+
+ if hostAddr == "" {
+ return "", "", errors.New("unable to process empty address")
+ }
+
+ // Simplify the work of url.Parse() and always send a url with
+ if !strings.HasPrefix(hostAddr, "http://") && !strings.HasPrefix(hostAddr, "https://") {
+ hostAddr = "//" + hostAddr
+ }
+
+ // Parse address to extract host and scheme field
+ u, err := url.Parse(hostAddr)
+ if err != nil {
+ return "", "", err
+ }
+
+ addr = u.Host
+ scheme = u.Scheme
+
+ // Use the given parameter again if url.Parse()
+ // didn't return any useful result.
+ if addr == "" {
+ addr = hostAddr
+ scheme = "http"
+ }
+
+ // At this point, addr can be one of the following form:
+ // ":9000"
+ // "localhost:9000"
+ // "localhost" <- in this case, we check for scheme
+
+ host, port, err := net.SplitHostPort(addr)
+ if err != nil {
+ if !strings.Contains(err.Error(), "missing port in address") {
+ return "", "", err
+ }
+
+ host = addr
+
+ switch scheme {
+ case "https":
+ port = "443"
+ case "http":
+ port = "80"
+ default:
+ return "", "", errors.New("unable to guess port from scheme")
+ }
+ }
+
+ return host, port, nil
+}
+
+// isLocalHost - checks if the given parameter
+// correspond to one of the local IP of the
+// current machine
+func isLocalHost(host string, port string, localPort string) (bool, error) {
+ hostIPs, err := getHostIP(host)
+ if err != nil {
+ return false, err
+ }
+
+ nonInterIPV4s := mustGetLocalIP4().Intersection(hostIPs)
+ if nonInterIPV4s.IsEmpty() {
+ hostIPs = hostIPs.ApplyFunc(func(ip string) string {
+ if net.ParseIP(ip).IsLoopback() {
+ // Any loopback IP which is not 127.0.0.1
+ // convert it to check for intersections.
+ return "127.0.0.1"
+ }
+ return ip
+ })
+ nonInterIPV4s = mustGetLocalIP4().Intersection(hostIPs)
+ }
+ nonInterIPV6s := mustGetLocalIP6().Intersection(hostIPs)
+
+ // If intersection of two IP sets is not empty, then the host is localhost.
+ isLocalv4 := !nonInterIPV4s.IsEmpty()
+ isLocalv6 := !nonInterIPV6s.IsEmpty()
+ if port != "" {
+ return (isLocalv4 || isLocalv6) && (port == localPort), nil
+ }
+ return isLocalv4 || isLocalv6, nil
+}
+
+// sameLocalAddrs - returns true if two addresses, even with different
+// formats, point to the same machine, e.g:
+//
+// ':9000' and 'http://localhost:9000/' will return true
+func sameLocalAddrs(addr1, addr2 string) (bool, error) {
+ // Extract host & port from given parameters
+ host1, port1, err := extractHostPort(addr1)
+ if err != nil {
+ return false, err
+ }
+ host2, port2, err := extractHostPort(addr2)
+ if err != nil {
+ return false, err
+ }
+
+ var addr1Local, addr2Local bool
+
+ if host1 == "" {
+ // If empty host means it is localhost
+ addr1Local = true
+ } else if addr1Local, err = isLocalHost(host1, port1, port1); err != nil {
+ // Host not empty, check if it is local
+ return false, err
+ }
+
+ if host2 == "" {
+ // If empty host means it is localhost
+ addr2Local = true
+ } else if addr2Local, err = isLocalHost(host2, port2, port2); err != nil {
+ // Host not empty, check if it is local
+ return false, err
+ }
+
+ // If both of addresses point to the same machine, check if
+ // have the same port
+ if addr1Local && addr2Local {
+ if port1 == port2 {
+ return true, nil
+ }
+ }
+ return false, nil
+}
+
+// CheckLocalServerAddr - checks if serverAddr is valid and local host.
+func CheckLocalServerAddr(serverAddr string) error {
+ host, err := xnet.ParseHost(serverAddr)
+ if err != nil {
+ return config.ErrInvalidAddressFlag(err)
+ }
+
+ // 0.0.0.0 is a wildcard address and refers to local network
+ // addresses. I.e, 0.0.0.0:9000 like ":9000" refers to port
+ // 9000 on localhost.
+ if host.Name != "" && host.Name != net.IPv4zero.String() && host.Name != net.IPv6zero.String() {
+ localHost, err := isLocalHost(host.Name, host.Port.String(), host.Port.String())
+ if err != nil {
+ return err
+ }
+ if !localHost {
+ return config.ErrInvalidAddressFlag(nil).Msg("host in server address should be this server")
+ }
+ }
+
+ return nil
+}
diff --git a/cmd/net_test.go b/cmd/net_test.go
new file mode 100644
index 00000000..d482cad2
--- /dev/null
+++ b/cmd/net_test.go
@@ -0,0 +1,317 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "errors"
+ "fmt"
+ "reflect"
+ "testing"
+
+ "github.com/minio/minio-go/v7/pkg/set"
+)
+
+func TestMustSplitHostPort(t *testing.T) {
+ testCases := []struct {
+ hostPort string
+ expectedHost string
+ expectedPort string
+ }{
+ {":54321", "", "54321"},
+ {"server:54321", "server", "54321"},
+ {":0", "", "0"},
+ {"server:https", "server", "443"},
+ {"server:http", "server", "80"},
+ }
+
+ for _, testCase := range testCases {
+ host, port := mustSplitHostPort(testCase.hostPort)
+ if testCase.expectedHost != host {
+ t.Fatalf("host: expected = %v, got = %v", testCase.expectedHost, host)
+ }
+
+ if testCase.expectedPort != port {
+ t.Fatalf("port: expected = %v, got = %v", testCase.expectedPort, port)
+ }
+ }
+}
+
+func TestSortIPs(t *testing.T) {
+ testCases := []struct {
+ ipList []string
+ sortedIPList []string
+ }{
+ // Default case of two ips one with higher octet moves
+ // to the beginning of the list.
+ {
+ ipList: []string{"127.0.0.1", "10.0.0.13"},
+ sortedIPList: []string{"10.0.0.13", "127.0.0.1"},
+ },
+ // With multiple types of octet, chooses a higher octet.
+ {
+ ipList: []string{"127.0.0.1", "172.0.21.1", "192.168.1.106"},
+ sortedIPList: []string{"192.168.1.106", "172.0.21.1", "127.0.0.1"},
+ },
+ // With different ip along with localhost.
+ {
+ ipList: []string{"127.0.0.1", "192.168.1.106"},
+ sortedIPList: []string{"192.168.1.106", "127.0.0.1"},
+ },
+ // With a list of only one element nothing to sort.
+ {
+ ipList: []string{"hostname"},
+ sortedIPList: []string{"hostname"},
+ },
+ // With a list of only one element nothing to sort.
+ {
+ ipList: []string{"127.0.0.1"},
+ sortedIPList: []string{"127.0.0.1"},
+ },
+ // Non parsable ip is assumed to be hostname and gets preserved
+ // as the left most elements, regardless of IP based sorting.
+ {
+ ipList: []string{"hostname", "127.0.0.1", "192.168.1.106"},
+ sortedIPList: []string{"hostname", "192.168.1.106", "127.0.0.1"},
+ },
+ // Non parsable ip is assumed to be hostname, with a mixed input of ip and hostname.
+ // gets preserved and moved into left most elements, regardless of
+ // IP based sorting.
+ {
+ ipList: []string{"hostname1", "10.0.0.13", "hostname2", "127.0.0.1", "192.168.1.106"},
+ sortedIPList: []string{"hostname1", "hostname2", "192.168.1.106", "10.0.0.13", "127.0.0.1"},
+ },
+ // With same higher octets, preferentially move the localhost.
+ {
+ ipList: []string{"127.0.0.1", "10.0.0.1", "192.168.0.1"},
+ sortedIPList: []string{"10.0.0.1", "192.168.0.1", "127.0.0.1"},
+ },
+ }
+ for i, testCase := range testCases {
+ gotIPList := sortIPs(testCase.ipList)
+ if !reflect.DeepEqual(testCase.sortedIPList, gotIPList) {
+ t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.sortedIPList, gotIPList)
+ }
+ }
+}
+
+func TestMustGetLocalIP4(t *testing.T) {
+ testCases := []struct {
+ expectedIPList set.StringSet
+ }{
+ {set.CreateStringSet("127.0.0.1")},
+ }
+
+ for _, testCase := range testCases {
+ ipList := mustGetLocalIP4()
+ if testCase.expectedIPList != nil && testCase.expectedIPList.Intersection(ipList).IsEmpty() {
+ t.Fatalf("host: expected = %v, got = %v", testCase.expectedIPList, ipList)
+ }
+ }
+}
+
+func TestGetHostIP(t *testing.T) {
+ testCases := []struct {
+ host string
+ expectedIPList set.StringSet
+ expectedErr error
+ }{
+ {"localhost", set.CreateStringSet("127.0.0.1"), nil},
+ }
+
+ for _, testCase := range testCases {
+ ipList, err := getHostIP(testCase.host)
+ switch {
+ case testCase.expectedErr == nil:
+ if err != nil {
+ t.Fatalf("error: expected = , got = %v", err)
+ }
+ case err == nil:
+ t.Fatalf("error: expected = %v, got = ", testCase.expectedErr)
+ case testCase.expectedErr.Error() != err.Error():
+ t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err)
+ }
+
+ if testCase.expectedIPList != nil {
+ var found bool
+ for _, ip := range ipList.ToSlice() {
+ if testCase.expectedIPList.Contains(ip) {
+ found = true
+ }
+ }
+ if !found {
+ t.Fatalf("host: expected = %v, got = %v", testCase.expectedIPList, ipList)
+ }
+ }
+ }
+}
+
+// Tests finalize api endpoints.
+func TestGetAPIEndpoints(t *testing.T) {
+ host, port := globalMinioHost, globalMinioPort
+ defer func() {
+ globalMinioHost, globalMinioPort = host, port
+ }()
+ testCases := []struct {
+ host, port string
+ expectedResult string
+ }{
+ {"", "80", "http://127.0.0.1:80"},
+ {"127.0.0.1", "80", "http://127.0.0.1:80"},
+ {"localhost", "80", "http://localhost:80"},
+ }
+
+ for i, testCase := range testCases {
+ globalMinioHost, globalMinioPort = testCase.host, testCase.port
+ apiEndpoints := getAPIEndpoints()
+ apiEndpointSet := set.CreateStringSet(apiEndpoints...)
+ if !apiEndpointSet.Contains(testCase.expectedResult) {
+ t.Fatalf("test %d: expected: Found, got: Not Found", i+1)
+ }
+ }
+}
+
+func TestCheckLocalServerAddr(t *testing.T) {
+ testCases := []struct {
+ serverAddr string
+ expectedErr error
+ }{
+ {":54321", nil},
+ {"localhost:54321", nil},
+ {"0.0.0.0:9000", nil},
+ {":0", nil},
+ {"localhost", nil},
+ {"", fmt.Errorf("invalid argument")},
+ {"example.org:54321", fmt.Errorf("host in server address should be this server")},
+ {":-10", fmt.Errorf("port must be between 0 to 65535")},
+ }
+
+ for _, testCase := range testCases {
+ t.Run("", func(t *testing.T) {
+ err := CheckLocalServerAddr(testCase.serverAddr)
+ switch {
+ case testCase.expectedErr == nil:
+ if err != nil {
+ t.Errorf("error: expected = , got = %v", err)
+ }
+ case err == nil:
+ t.Errorf("error: expected = %v, got = ", testCase.expectedErr)
+ case testCase.expectedErr.Error() != err.Error():
+ t.Errorf("error: expected = %v, got = %v", testCase.expectedErr, err)
+ }
+ })
+ }
+}
+
+func TestExtractHostPort(t *testing.T) {
+ testCases := []struct {
+ addr string
+ host string
+ port string
+ expectedErr error
+ }{
+ {"", "", "", errors.New("unable to process empty address")},
+ {"localhost:9000", "localhost", "9000", nil},
+ {"http://:9000/", "", "9000", nil},
+ {"http://8.8.8.8:9000/", "8.8.8.8", "9000", nil},
+ {"https://facebook.com:9000/", "facebook.com", "9000", nil},
+ }
+
+ for i, testCase := range testCases {
+ host, port, err := extractHostPort(testCase.addr)
+ if testCase.expectedErr == nil && err != nil {
+ t.Fatalf("Test %d: should succeed but failed with err: %v", i+1, err)
+ }
+ if testCase.expectedErr != nil && err == nil {
+ t.Fatalf("Test %d:, should fail but succeeded.", i+1)
+ }
+ if err == nil {
+ if host != testCase.host {
+ t.Fatalf("Test %d: expected: %v, found: %v", i+1, testCase.host, host)
+ }
+ if port != testCase.port {
+ t.Fatalf("Test %d: expected: %v, found: %v", i+1, testCase.port, port)
+ }
+ }
+ if testCase.expectedErr != nil && err != nil {
+ if testCase.expectedErr.Error() != err.Error() {
+ t.Fatalf("Test %d: failed with different error, expected: '%v', found:'%v'.", i+1, testCase.expectedErr, err)
+ }
+ }
+ }
+}
+
+func TestSameLocalAddrs(t *testing.T) {
+ testCases := []struct {
+ addr1 string
+ addr2 string
+ sameAddr bool
+ expectedErr error
+ }{
+ {"", "", false, errors.New("unable to process empty address")},
+ {":9000", ":9000", true, nil},
+ {"localhost:9000", ":9000", true, nil},
+ {"localhost:9000", "http://localhost:9000", true, nil},
+ {"http://localhost:9000", ":9000", true, nil},
+ {"http://localhost:9000", "http://localhost:9000", true, nil},
+ {"http://8.8.8.8:9000", "http://localhost:9000", false, nil},
+ }
+
+ for _, testCase := range testCases {
+ t.Run("", func(t *testing.T) {
+ sameAddr, err := sameLocalAddrs(testCase.addr1, testCase.addr2)
+ if testCase.expectedErr != nil && err == nil {
+ t.Errorf("should fail but succeeded")
+ }
+ if testCase.expectedErr == nil && err != nil {
+ t.Errorf("should succeed but failed with %v", err)
+ }
+ if err == nil {
+ if sameAddr != testCase.sameAddr {
+ t.Errorf("expected: %v, found: %v", testCase.sameAddr, sameAddr)
+ }
+ } else {
+ if err.Error() != testCase.expectedErr.Error() {
+ t.Errorf("failed with different error, expected: '%v', found:'%v'.",
+ testCase.expectedErr, err)
+ }
+ }
+ })
+ }
+}
+
+func TestIsHostIP(t *testing.T) {
+ testCases := []struct {
+ args string
+ expectedResult bool
+ }{
+ {"localhost", false},
+ {"localhost:9000", false},
+ {"example.com", false},
+ {"http://192.168.1.0", false},
+ {"http://192.168.1.0:9000", false},
+ {"192.168.1.0", true},
+ {"[2001:3984:3989::20%eth0]:9000", true},
+ }
+
+ for _, testCase := range testCases {
+ ret := isHostIP(testCase.args)
+ if testCase.expectedResult != ret {
+ t.Fatalf("expected: %v , got: %v", testCase.expectedResult, ret)
+ }
+ }
+}
diff --git a/cmd/notification-summary.go b/cmd/notification-summary.go
new file mode 100644
index 00000000..28d07c7e
--- /dev/null
+++ b/cmd/notification-summary.go
@@ -0,0 +1,70 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "github.com/minio/madmin-go/v3"
+)
+
+// GetTotalCapacity gets the total capacity in the cluster.
+func GetTotalCapacity(diskInfo []madmin.Disk) (capacity uint64) {
+ for _, disk := range diskInfo {
+ capacity += disk.TotalSpace
+ }
+ return capacity
+}
+
+// GetTotalUsableCapacity gets the total usable capacity in the cluster.
+func GetTotalUsableCapacity(diskInfo []madmin.Disk, s StorageInfo) (capacity uint64) {
+ for _, disk := range diskInfo {
+ // Ignore invalid.
+ if disk.PoolIndex < 0 || len(s.Backend.StandardSCData) <= disk.PoolIndex {
+ // https://github.com/minio/minio/issues/16500
+ continue
+ }
+ // Ignore parity disks
+ if disk.DiskIndex < s.Backend.StandardSCData[disk.PoolIndex] {
+ capacity += disk.TotalSpace
+ }
+ }
+ return capacity
+}
+
+// GetTotalCapacityFree gets the total capacity free in the cluster.
+func GetTotalCapacityFree(diskInfo []madmin.Disk) (capacity uint64) {
+ for _, d := range diskInfo {
+ capacity += d.AvailableSpace
+ }
+ return capacity
+}
+
+// GetTotalUsableCapacityFree gets the total usable capacity free in the cluster.
+func GetTotalUsableCapacityFree(diskInfo []madmin.Disk, s StorageInfo) (capacity uint64) {
+ for _, disk := range diskInfo {
+ // Ignore invalid.
+ if disk.PoolIndex < 0 || len(s.Backend.StandardSCData) <= disk.PoolIndex {
+ // https://github.com/minio/minio/issues/16500
+ continue
+ }
+ // Ignore parity disks
+ if disk.DiskIndex < s.Backend.StandardSCData[disk.PoolIndex] {
+ capacity += disk.AvailableSpace
+ }
+ }
+ return capacity
+}
diff --git a/cmd/notification.go b/cmd/notification.go
new file mode 100644
index 00000000..152856ac
--- /dev/null
+++ b/cmd/notification.go
@@ -0,0 +1,1632 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "math/rand"
+ "net/http"
+ "net/url"
+ "runtime"
+ "sync"
+ "time"
+
+ "github.com/cespare/xxhash/v2"
+ "github.com/klauspost/compress/zip"
+ "github.com/minio/madmin-go/v3"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ xnet "github.com/minio/pkg/v3/net"
+ "github.com/minio/pkg/v3/sync/errgroup"
+ "github.com/minio/pkg/v3/workers"
+
+ "github.com/minio/minio/internal/bucket/bandwidth"
+ "github.com/minio/minio/internal/logger"
+)
+
+// This file contains peer related notifications. For sending notifications to
+// external systems, see event-notification.go
+
+// NotificationSys - notification system.
+type NotificationSys struct {
+ peerClients []*peerRESTClient // Excludes self
+ allPeerClients []*peerRESTClient // Includes nil client for self
+}
+
+// NotificationPeerErr returns error associated for a remote peer.
+type NotificationPeerErr struct {
+ Host xnet.Host // Remote host on which the rpc call was initiated
+ Err error // Error returned by the remote peer for an rpc call
+}
+
+// A NotificationGroup is a collection of goroutines working on subtasks that are part of
+// the same overall task.
+//
+// A zero NotificationGroup is valid and does not cancel on error.
+type NotificationGroup struct {
+ workers *workers.Workers
+ errs []NotificationPeerErr
+ retryCount int
+}
+
+// WithNPeers returns a new NotificationGroup with length of errs slice upto nerrs,
+// upon Wait() errors are returned collected from all tasks.
+func WithNPeers(nerrs int) *NotificationGroup {
+ if nerrs <= 0 {
+ nerrs = 1
+ }
+ wk, _ := workers.New(nerrs)
+ return &NotificationGroup{errs: make([]NotificationPeerErr, nerrs), workers: wk, retryCount: 3}
+}
+
+// WithNPeersThrottled returns a new NotificationGroup with length of errs slice upto nerrs,
+// upon Wait() errors are returned collected from all tasks, optionally allows for X workers
+// only "per" parallel task.
+func WithNPeersThrottled(nerrs, wks int) *NotificationGroup {
+ if nerrs <= 0 {
+ nerrs = 1
+ }
+ if wks > nerrs {
+ wks = nerrs
+ }
+ wk, _ := workers.New(wks)
+ return &NotificationGroup{errs: make([]NotificationPeerErr, nerrs), workers: wk, retryCount: 3}
+}
+
+// WithRetries sets the retry count for all function calls from the Go method.
+func (g *NotificationGroup) WithRetries(retryCount int) *NotificationGroup {
+ if g != nil {
+ g.retryCount = retryCount
+ }
+ return g
+}
+
+// Wait blocks until all function calls from the Go method have returned, then
+// returns the slice of errors from all function calls.
+func (g *NotificationGroup) Wait() []NotificationPeerErr {
+ g.workers.Wait()
+ return g.errs
+}
+
+// Go calls the given function in a new goroutine.
+//
+// The first call to return a non-nil error will be
+// collected in errs slice and returned by Wait().
+func (g *NotificationGroup) Go(ctx context.Context, f func() error, index int, addr xnet.Host) {
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+
+ g.workers.Take()
+
+ go func() {
+ defer g.workers.Give()
+
+ g.errs[index] = NotificationPeerErr{
+ Host: addr,
+ }
+
+ retryCount := g.retryCount
+ for i := range retryCount {
+ g.errs[index].Err = nil
+ if err := f(); err != nil {
+ g.errs[index].Err = err
+
+ if contextCanceled(ctx) {
+ // context already canceled no retries.
+ retryCount = 0
+ }
+
+ // Last iteration log the error.
+ if i == retryCount-1 {
+ reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", addr.String())
+ ctx := logger.SetReqInfo(ctx, reqInfo)
+ peersLogOnceIf(ctx, err, addr.String())
+ }
+
+ // Wait for a minimum of 100ms and dynamically increase this based on number of attempts.
+ if i < retryCount-1 {
+ time.Sleep(100*time.Millisecond + time.Duration(r.Float64()*float64(time.Second)))
+ continue
+ }
+ }
+ break
+ }
+ }()
+}
+
+// DeletePolicy - deletes policy across all peers.
+func (sys *NotificationSys) DeletePolicy(ctx context.Context, policyName string) []NotificationPeerErr {
+ ng := WithNPeers(len(sys.peerClients)).WithRetries(1)
+ for idx, client := range sys.peerClients {
+ ng.Go(ctx, func() error {
+ if client == nil {
+ return errPeerNotReachable
+ }
+ return client.DeletePolicy(ctx, policyName)
+ }, idx, *client.host)
+ }
+ return ng.Wait()
+}
+
+// LoadPolicy - reloads a specific modified policy across all peers
+func (sys *NotificationSys) LoadPolicy(ctx context.Context, policyName string) []NotificationPeerErr {
+ ng := WithNPeers(len(sys.peerClients)).WithRetries(1)
+ for idx, client := range sys.peerClients {
+ ng.Go(ctx, func() error {
+ if client == nil {
+ return errPeerNotReachable
+ }
+ return client.LoadPolicy(ctx, policyName)
+ }, idx, *client.host)
+ }
+ return ng.Wait()
+}
+
+// LoadPolicyMapping - reloads a policy mapping across all peers
+func (sys *NotificationSys) LoadPolicyMapping(ctx context.Context, userOrGroup string, userType IAMUserType, isGroup bool) []NotificationPeerErr {
+ ng := WithNPeers(len(sys.peerClients)).WithRetries(1)
+ for idx, client := range sys.peerClients {
+ ng.Go(ctx, func() error {
+ if client == nil {
+ return errPeerNotReachable
+ }
+ return client.LoadPolicyMapping(ctx, userOrGroup, userType, isGroup)
+ }, idx, *client.host)
+ }
+ return ng.Wait()
+}
+
+// DeleteUser - deletes a specific user across all peers
+func (sys *NotificationSys) DeleteUser(ctx context.Context, accessKey string) []NotificationPeerErr {
+ ng := WithNPeers(len(sys.peerClients)).WithRetries(1)
+ for idx, client := range sys.peerClients {
+ ng.Go(ctx, func() error {
+ if client == nil {
+ return errPeerNotReachable
+ }
+ return client.DeleteUser(ctx, accessKey)
+ }, idx, *client.host)
+ }
+ return ng.Wait()
+}
+
+// LoadUser - reloads a specific user across all peers
+func (sys *NotificationSys) LoadUser(ctx context.Context, accessKey string, temp bool) []NotificationPeerErr {
+ ng := WithNPeers(len(sys.peerClients)).WithRetries(1)
+ for idx, client := range sys.peerClients {
+ ng.Go(ctx, func() error {
+ if client == nil {
+ return errPeerNotReachable
+ }
+ return client.LoadUser(ctx, accessKey, temp)
+ }, idx, *client.host)
+ }
+ return ng.Wait()
+}
+
+// LoadGroup - loads a specific group on all peers.
+func (sys *NotificationSys) LoadGroup(ctx context.Context, group string) []NotificationPeerErr {
+ ng := WithNPeers(len(sys.peerClients)).WithRetries(1)
+ for idx, client := range sys.peerClients {
+ ng.Go(ctx, func() error {
+ if client == nil {
+ return errPeerNotReachable
+ }
+ return client.LoadGroup(ctx, group)
+ }, idx, *client.host)
+ }
+ return ng.Wait()
+}
+
+// DeleteServiceAccount - deletes a specific service account across all peers
+func (sys *NotificationSys) DeleteServiceAccount(ctx context.Context, accessKey string) []NotificationPeerErr {
+ ng := WithNPeers(len(sys.peerClients)).WithRetries(1)
+ for idx, client := range sys.peerClients {
+ ng.Go(ctx, func() error {
+ if client == nil {
+ return errPeerNotReachable
+ }
+ return client.DeleteServiceAccount(ctx, accessKey)
+ }, idx, *client.host)
+ }
+ return ng.Wait()
+}
+
+// LoadServiceAccount - reloads a specific service account across all peers
+func (sys *NotificationSys) LoadServiceAccount(ctx context.Context, accessKey string) []NotificationPeerErr {
+ ng := WithNPeers(len(sys.peerClients)).WithRetries(1)
+ for idx, client := range sys.peerClients {
+ ng.Go(ctx, func() error {
+ if client == nil {
+ return errPeerNotReachable
+ }
+ return client.LoadServiceAccount(ctx, accessKey)
+ }, idx, *client.host)
+ }
+ return ng.Wait()
+}
+
+// BackgroundHealStatus - returns background heal status of all peers
+func (sys *NotificationSys) BackgroundHealStatus(ctx context.Context) ([]madmin.BgHealState, []NotificationPeerErr) {
+ ng := WithNPeers(len(sys.peerClients))
+ states := make([]madmin.BgHealState, len(sys.peerClients))
+ for idx, client := range sys.peerClients {
+ client := client
+ ng.Go(ctx, func() error {
+ if client == nil {
+ return errPeerNotReachable
+ }
+ st, err := client.BackgroundHealStatus(ctx)
+ if err != nil {
+ return err
+ }
+ states[idx] = st
+ return nil
+ }, idx, *client.host)
+ }
+
+ return states, ng.Wait()
+}
+
+// StartProfiling - start profiling on remote peers, by initiating a remote RPC.
+func (sys *NotificationSys) StartProfiling(ctx context.Context, profiler string) []NotificationPeerErr {
+ ng := WithNPeers(len(sys.peerClients))
+ for idx, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ client := client
+ ng.Go(ctx, func() error {
+ return client.StartProfiling(ctx, profiler)
+ }, idx, *client.host)
+ }
+ return ng.Wait()
+}
+
+// DownloadProfilingData - download profiling data from all remote peers.
+func (sys *NotificationSys) DownloadProfilingData(ctx context.Context, writer io.Writer) (profilingDataFound bool) {
+ // Initialize a zip writer which will provide a zipped content
+ // of profiling data of all nodes
+ zipWriter := zip.NewWriter(writer)
+ defer zipWriter.Close()
+
+ // Start by embedding cluster info.
+ if b := getClusterMetaInfo(ctx); len(b) > 0 {
+ internalLogIf(ctx, embedFileInZip(zipWriter, "cluster.info", b, 0o600))
+ }
+
+ // Profiles can be quite big, so we limit to max 16 concurrent downloads.
+ ng := WithNPeersThrottled(len(sys.peerClients), 16)
+ var writeMu sync.Mutex
+ for i, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ ng.Go(ctx, func() error {
+ // Give 15 seconds to each remote call.
+ // Errors are logged but not returned.
+ ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
+ defer cancel()
+ data, err := client.DownloadProfileData(ctx)
+ if err != nil {
+ reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", client.host.String())
+ ctx := logger.SetReqInfo(ctx, reqInfo)
+ peersLogOnceIf(ctx, err, client.host.String())
+ return nil
+ }
+
+ for typ, data := range data {
+ // zip writer only handles one concurrent write
+ writeMu.Lock()
+ profilingDataFound = true
+ err := embedFileInZip(zipWriter, fmt.Sprintf("profile-%s-%s", client.host.String(), typ), data, 0o600)
+ writeMu.Unlock()
+ if err != nil {
+ reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", client.host.String())
+ ctx := logger.SetReqInfo(ctx, reqInfo)
+ peersLogOnceIf(ctx, err, client.host.String())
+ }
+ }
+ return nil
+ }, i, *client.host)
+ }
+ ng.Wait()
+ if ctx.Err() != nil {
+ return false
+ }
+
+ // Local host
+ thisAddr, err := xnet.ParseHost(globalLocalNodeName)
+ if err != nil {
+ bugLogIf(ctx, err)
+ return profilingDataFound
+ }
+
+ data, err := getProfileData()
+ if err != nil {
+ reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", thisAddr.String())
+ ctx := logger.SetReqInfo(ctx, reqInfo)
+ bugLogIf(ctx, err)
+ return profilingDataFound
+ }
+
+ profilingDataFound = true
+
+ // Send profiling data to zip as file
+ for typ, data := range data {
+ err := embedFileInZip(zipWriter, fmt.Sprintf("profile-%s-%s", thisAddr, typ), data, 0o600)
+ internalLogIf(ctx, err)
+ }
+
+ return profilingDataFound
+}
+
+// VerifyBinary - asks remote peers to verify the checksum
+func (sys *NotificationSys) VerifyBinary(ctx context.Context, u *url.URL, sha256Sum []byte, releaseInfo string, bin []byte) []NotificationPeerErr {
+ // FIXME: network calls made in this manner such as one goroutine per node,
+ // can easily eat into the internode bandwidth. This function would be mostly
+ // TX saturating, however there are situations where a RX might also saturate.
+ // To avoid these problems we must split the work at scale. With 1000 node
+ // setup becoming a reality we must try to shard the work properly such as
+ // pick 10 nodes that precisely can send those 100 requests the first node
+ // in the 10 node shard would coordinate between other 9 shards to get the
+ // rest of the `99*9` requests.
+ //
+ // This essentially splits the workload properly and also allows for network
+ // utilization to be optimal, instead of blindly throttling the way we are
+ // doing below. However the changes that are needed here are a bit involved,
+ // further discussion advised. Remove this comment and remove the worker model
+ // for this function in future.
+ maxWorkers := runtime.GOMAXPROCS(0) / 2
+ ng := WithNPeersThrottled(len(sys.peerClients), maxWorkers)
+ for idx, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ client := client
+ ng.Go(ctx, func() error {
+ return client.VerifyBinary(ctx, u, sha256Sum, releaseInfo, bytes.NewReader(bin))
+ }, idx, *client.host)
+ }
+ return ng.Wait()
+}
+
+// CommitBinary - asks remote peers to overwrite the old binary with the new one
+func (sys *NotificationSys) CommitBinary(ctx context.Context) []NotificationPeerErr {
+ ng := WithNPeers(len(sys.peerClients))
+ for idx, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ client := client
+ ng.Go(ctx, func() error {
+ return client.CommitBinary(ctx)
+ }, idx, *client.host)
+ }
+ return ng.Wait()
+}
+
+// SignalConfigReload reloads requested sub-system on a remote peer dynamically.
+func (sys *NotificationSys) SignalConfigReload(subSys string) []NotificationPeerErr {
+ ng := WithNPeers(len(sys.peerClients))
+ for idx, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ client := client
+ ng.Go(GlobalContext, func() error {
+ return client.SignalService(serviceReloadDynamic, subSys, false, nil)
+ }, idx, *client.host)
+ }
+ return ng.Wait()
+}
+
+// SignalService - calls signal service RPC call on all peers.
+func (sys *NotificationSys) SignalService(sig serviceSignal) []NotificationPeerErr {
+ ng := WithNPeers(len(sys.peerClients))
+ for idx, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ client := client
+ ng.Go(GlobalContext, func() error {
+ // force == true preserves the current behavior
+ return client.SignalService(sig, "", false, nil)
+ }, idx, *client.host)
+ }
+ return ng.Wait()
+}
+
+// SignalServiceV2 - calls signal service RPC call on all peers with v2 API
+func (sys *NotificationSys) SignalServiceV2(sig serviceSignal, dryRun bool, execAt *time.Time) []NotificationPeerErr {
+ ng := WithNPeers(len(sys.peerClients))
+ for idx, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ client := client
+ ng.Go(GlobalContext, func() error {
+ return client.SignalService(sig, "", dryRun, execAt)
+ }, idx, *client.host)
+ }
+ return ng.Wait()
+}
+
+var errPeerNotReachable = errors.New("peer is not reachable")
+
+// GetLocks - makes GetLocks RPC call on all peers.
+func (sys *NotificationSys) GetLocks(ctx context.Context, r *http.Request) []*PeerLocks {
+ locksResp := make([]*PeerLocks, len(sys.peerClients))
+ g := errgroup.WithNErrs(len(sys.peerClients))
+ for index, client := range sys.peerClients {
+ client := client
+ g.Go(func() error {
+ if client == nil {
+ return errPeerNotReachable
+ }
+ serverLocksResp, err := sys.peerClients[index].GetLocks(ctx)
+ if err != nil {
+ return err
+ }
+ locksResp[index] = &PeerLocks{
+ Addr: sys.peerClients[index].host.String(),
+ Locks: serverLocksResp,
+ }
+ return nil
+ }, index)
+ }
+ for index, err := range g.Wait() {
+ reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress",
+ sys.peerClients[index].host.String())
+ ctx := logger.SetReqInfo(ctx, reqInfo)
+ peersLogOnceIf(ctx, err, sys.peerClients[index].host.String())
+ }
+ locksResp = append(locksResp, &PeerLocks{
+ Addr: getHostName(r),
+ Locks: globalLockServer.DupLockMap(),
+ })
+ return locksResp
+}
+
+// LoadBucketMetadata - calls LoadBucketMetadata call on all peers
+func (sys *NotificationSys) LoadBucketMetadata(ctx context.Context, bucketName string) {
+ ng := WithNPeers(len(sys.peerClients))
+ for idx, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ client := client
+ ng.Go(ctx, func() error {
+ return client.LoadBucketMetadata(ctx, bucketName)
+ }, idx, *client.host)
+ }
+ for _, nErr := range ng.Wait() {
+ reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", nErr.Host.String())
+ if nErr.Err != nil {
+ peersLogOnceIf(logger.SetReqInfo(ctx, reqInfo), nErr.Err, nErr.Host.String())
+ }
+ }
+}
+
+// DeleteBucketMetadata - calls DeleteBucketMetadata call on all peers
+func (sys *NotificationSys) DeleteBucketMetadata(ctx context.Context, bucketName string) {
+ globalReplicationStats.Load().Delete(bucketName)
+ globalBucketMetadataSys.Remove(bucketName)
+ globalBucketTargetSys.Delete(bucketName)
+ globalEventNotifier.RemoveNotification(bucketName)
+ globalBucketConnStats.delete(bucketName)
+ globalBucketHTTPStats.delete(bucketName)
+ if localMetacacheMgr != nil {
+ localMetacacheMgr.deleteBucketCache(bucketName)
+ }
+
+ ng := WithNPeers(len(sys.peerClients))
+ for idx, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ client := client
+ ng.Go(ctx, func() error {
+ return client.DeleteBucketMetadata(ctx, bucketName)
+ }, idx, *client.host)
+ }
+ for _, nErr := range ng.Wait() {
+ reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", nErr.Host.String())
+ if nErr.Err != nil {
+ peersLogOnceIf(logger.SetReqInfo(ctx, reqInfo), nErr.Err, nErr.Host.String())
+ }
+ }
+}
+
+// GetClusterAllBucketStats - returns bucket stats for all buckets from all remote peers.
+func (sys *NotificationSys) GetClusterAllBucketStats(ctx context.Context) []BucketStatsMap {
+ ng := WithNPeers(len(sys.peerClients)).WithRetries(1)
+ replicationStats := make([]BucketStatsMap, len(sys.peerClients))
+ for index, client := range sys.peerClients {
+ client := client
+ ng.Go(ctx, func() error {
+ if client == nil {
+ return errPeerNotReachable
+ }
+ bsMap, err := client.GetAllBucketStats(ctx)
+ if err != nil {
+ return err
+ }
+ replicationStats[index] = bsMap
+ return nil
+ }, index, *client.host)
+ }
+ for _, nErr := range ng.Wait() {
+ reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", nErr.Host.String())
+ if nErr.Err != nil {
+ peersLogOnceIf(logger.SetReqInfo(ctx, reqInfo), nErr.Err, nErr.Host.String())
+ }
+ }
+
+ replicationStatsList := globalReplicationStats.Load().GetAll()
+ bucketStatsMap := BucketStatsMap{
+ Stats: make(map[string]BucketStats, len(replicationStatsList)),
+ Timestamp: UTCNow(),
+ }
+ for k, replicationStats := range replicationStatsList {
+ bucketStatsMap.Stats[k] = BucketStats{
+ ReplicationStats: replicationStats,
+ ProxyStats: globalReplicationStats.Load().getProxyStats(k),
+ }
+ }
+
+ replicationStats = append(replicationStats, bucketStatsMap)
+ return replicationStats
+}
+
+// GetClusterBucketStats - calls GetClusterBucketStats call on all peers for a cluster statistics view.
+func (sys *NotificationSys) GetClusterBucketStats(ctx context.Context, bucketName string) []BucketStats {
+ ng := WithNPeers(len(sys.peerClients)).WithRetries(1)
+ bucketStats := make([]BucketStats, len(sys.peerClients))
+ for index, client := range sys.peerClients {
+ client := client
+ ng.Go(ctx, func() error {
+ if client == nil {
+ return errPeerNotReachable
+ }
+ bs, err := client.GetBucketStats(ctx, bucketName)
+ if err != nil {
+ return err
+ }
+ bucketStats[index] = bs
+ return nil
+ }, index, *client.host)
+ }
+ for _, nErr := range ng.Wait() {
+ reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", nErr.Host.String())
+ if nErr.Err != nil {
+ peersLogOnceIf(logger.SetReqInfo(ctx, reqInfo), nErr.Err, nErr.Host.String())
+ }
+ }
+ if st := globalReplicationStats.Load(); st != nil {
+ bucketStats = append(bucketStats, BucketStats{
+ ReplicationStats: st.Get(bucketName),
+ QueueStats: ReplicationQueueStats{Nodes: []ReplQNodeStats{st.getNodeQueueStats(bucketName)}},
+ ProxyStats: st.getProxyStats(bucketName),
+ })
+ }
+ return bucketStats
+}
+
+// GetClusterSiteMetrics - calls GetClusterSiteMetrics call on all peers for a cluster statistics view.
+func (sys *NotificationSys) GetClusterSiteMetrics(ctx context.Context) []SRMetricsSummary {
+ ng := WithNPeers(len(sys.peerClients)).WithRetries(1)
+ siteStats := make([]SRMetricsSummary, len(sys.peerClients))
+ for index, client := range sys.peerClients {
+ client := client
+ ng.Go(ctx, func() error {
+ if client == nil {
+ return errPeerNotReachable
+ }
+ sm, err := client.GetSRMetrics(ctx)
+ if err != nil {
+ return err
+ }
+ siteStats[index] = sm
+ return nil
+ }, index, *client.host)
+ }
+ for _, nErr := range ng.Wait() {
+ reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", nErr.Host.String())
+ if nErr.Err != nil {
+ peersLogOnceIf(logger.SetReqInfo(ctx, reqInfo), nErr.Err, nErr.Host.String())
+ }
+ }
+ siteStats = append(siteStats, globalReplicationStats.Load().getSRMetricsForNode())
+ return siteStats
+}
+
+// ReloadPoolMeta reloads on disk updates on pool metadata
+func (sys *NotificationSys) ReloadPoolMeta(ctx context.Context) {
+ ng := WithNPeers(len(sys.peerClients))
+ for idx, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ client := client
+ ng.Go(ctx, func() error {
+ return client.ReloadPoolMeta(ctx)
+ }, idx, *client.host)
+ }
+ for _, nErr := range ng.Wait() {
+ reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", nErr.Host.String())
+ if nErr.Err != nil {
+ peersLogOnceIf(logger.SetReqInfo(ctx, reqInfo), nErr.Err, nErr.Host.String())
+ }
+ }
+}
+
+// DeleteUploadID notifies all the MinIO nodes to remove the
+// given uploadID from cache
+func (sys *NotificationSys) DeleteUploadID(ctx context.Context, uploadID string) {
+ ng := WithNPeers(len(sys.peerClients))
+ for idx, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ client := client
+ ng.Go(ctx, func() error {
+ return client.DeleteUploadID(ctx, uploadID)
+ }, idx, *client.host)
+ }
+ for _, nErr := range ng.Wait() {
+ reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", nErr.Host.String())
+ if nErr.Err != nil {
+ peersLogOnceIf(logger.SetReqInfo(ctx, reqInfo), nErr.Err, nErr.Host.String())
+ }
+ }
+}
+
+// StopRebalance notifies all MinIO nodes to signal any ongoing rebalance
+// goroutine to stop.
+func (sys *NotificationSys) StopRebalance(ctx context.Context) {
+ objAPI := newObjectLayerFn()
+ if objAPI == nil {
+ internalLogIf(ctx, errServerNotInitialized)
+ return
+ }
+
+ ng := WithNPeers(len(sys.peerClients))
+ for idx, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ client := client
+ ng.Go(ctx, func() error {
+ return client.StopRebalance(ctx)
+ }, idx, *client.host)
+ }
+ for _, nErr := range ng.Wait() {
+ reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", nErr.Host.String())
+ if nErr.Err != nil {
+ peersLogOnceIf(logger.SetReqInfo(ctx, reqInfo), nErr.Err, nErr.Host.String())
+ }
+ }
+
+ if pools, ok := objAPI.(*erasureServerPools); ok {
+ pools.StopRebalance()
+ }
+}
+
+// LoadRebalanceMeta notifies all peers to load rebalance.bin from object layer.
+// Note: Only peers participating in rebalance operation, namely the first node
+// in each pool will load rebalance.bin.
+func (sys *NotificationSys) LoadRebalanceMeta(ctx context.Context, startRebalance bool) {
+ ng := WithNPeers(len(sys.peerClients))
+ for idx, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ client := client
+ ng.Go(ctx, func() error {
+ return client.LoadRebalanceMeta(ctx, startRebalance)
+ }, idx, *client.host)
+ }
+ for _, nErr := range ng.Wait() {
+ reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", nErr.Host.String())
+ if nErr.Err != nil {
+ peersLogOnceIf(logger.SetReqInfo(ctx, reqInfo), nErr.Err, nErr.Host.String())
+ }
+ }
+}
+
+// LoadTransitionTierConfig notifies remote peers to load their remote tier
+// configs from config store.
+func (sys *NotificationSys) LoadTransitionTierConfig(ctx context.Context) {
+ ng := WithNPeers(len(sys.peerClients))
+ for idx, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ client := client
+ ng.Go(ctx, func() error {
+ return client.LoadTransitionTierConfig(ctx)
+ }, idx, *client.host)
+ }
+ for _, nErr := range ng.Wait() {
+ reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", nErr.Host.String())
+ if nErr.Err != nil {
+ peersLogOnceIf(logger.SetReqInfo(ctx, reqInfo), nErr.Err, nErr.Host.String())
+ }
+ }
+}
+
+// GetCPUs - Get all CPU information.
+func (sys *NotificationSys) GetCPUs(ctx context.Context) []madmin.CPUs {
+ reply := make([]madmin.CPUs, len(sys.peerClients))
+
+ g := errgroup.WithNErrs(len(sys.peerClients))
+ for index, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ index := index
+ g.Go(func() error {
+ var err error
+ reply[index], err = sys.peerClients[index].GetCPUs(ctx)
+ return err
+ }, index)
+ }
+
+ for index, err := range g.Wait() {
+ if err != nil {
+ sys.addNodeErr(&reply[index], sys.peerClients[index], err)
+ }
+ }
+ return reply
+}
+
+// GetNetInfo - Network information
+func (sys *NotificationSys) GetNetInfo(ctx context.Context) []madmin.NetInfo {
+ reply := make([]madmin.NetInfo, len(sys.peerClients))
+
+ g := errgroup.WithNErrs(len(sys.peerClients))
+ for index, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ index := index
+ g.Go(func() error {
+ var err error
+ reply[index], err = sys.peerClients[index].GetNetInfo(ctx)
+ return err
+ }, index)
+ }
+
+ for index, err := range g.Wait() {
+ if err != nil {
+ sys.addNodeErr(&reply[index], sys.peerClients[index], err)
+ }
+ }
+ return reply
+}
+
+// GetPartitions - Disk partition information
+func (sys *NotificationSys) GetPartitions(ctx context.Context) []madmin.Partitions {
+ reply := make([]madmin.Partitions, len(sys.peerClients))
+
+ g := errgroup.WithNErrs(len(sys.peerClients))
+ for index, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ index := index
+ g.Go(func() error {
+ var err error
+ reply[index], err = sys.peerClients[index].GetPartitions(ctx)
+ return err
+ }, index)
+ }
+
+ for index, err := range g.Wait() {
+ if err != nil {
+ sys.addNodeErr(&reply[index], sys.peerClients[index], err)
+ }
+ }
+ return reply
+}
+
+// GetOSInfo - Get operating system's information
+func (sys *NotificationSys) GetOSInfo(ctx context.Context) []madmin.OSInfo {
+ reply := make([]madmin.OSInfo, len(sys.peerClients))
+
+ g := errgroup.WithNErrs(len(sys.peerClients))
+ for index, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ index := index
+ g.Go(func() error {
+ var err error
+ reply[index], err = sys.peerClients[index].GetOSInfo(ctx)
+ return err
+ }, index)
+ }
+
+ for index, err := range g.Wait() {
+ if err != nil {
+ sys.addNodeErr(&reply[index], sys.peerClients[index], err)
+ }
+ }
+ return reply
+}
+
+// GetMetrics - Get metrics from all peers.
+func (sys *NotificationSys) GetMetrics(ctx context.Context, t madmin.MetricType, opts collectMetricsOpts) []madmin.RealtimeMetrics {
+ reply := make([]madmin.RealtimeMetrics, len(sys.peerClients))
+
+ g := errgroup.WithNErrs(len(sys.peerClients))
+ for index, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ host := client.host.String()
+ if len(opts.hosts) > 0 {
+ if _, ok := opts.hosts[host]; !ok {
+ continue
+ }
+ }
+
+ index := index
+ g.Go(func() error {
+ var err error
+ reply[index], err = sys.peerClients[index].GetMetrics(ctx, t, opts)
+ return err
+ }, index)
+ }
+
+ for index, err := range g.Wait() {
+ if err != nil {
+ reply[index].Errors = []string{fmt.Sprintf("%s: %s (rpc)", sys.peerClients[index].String(), err.Error())}
+ }
+ }
+ return reply
+}
+
+// GetResourceMetrics - gets the resource metrics from all nodes excluding self.
+func (sys *NotificationSys) GetResourceMetrics(ctx context.Context) <-chan MetricV2 {
+ if sys == nil {
+ return nil
+ }
+ g := errgroup.WithNErrs(len(sys.peerClients))
+ peerChannels := make([]<-chan MetricV2, len(sys.peerClients))
+ for index := range sys.peerClients {
+ g.Go(func() error {
+ if sys.peerClients[index] == nil {
+ return errPeerNotReachable
+ }
+ var err error
+ peerChannels[index], err = sys.peerClients[index].GetResourceMetrics(ctx)
+ return err
+ }, index)
+ }
+ return sys.collectPeerMetrics(ctx, peerChannels, g)
+}
+
+// GetSysConfig - Get information about system config
+// (only the config that are of concern to minio)
+func (sys *NotificationSys) GetSysConfig(ctx context.Context) []madmin.SysConfig {
+ reply := make([]madmin.SysConfig, len(sys.peerClients))
+
+ g := errgroup.WithNErrs(len(sys.peerClients))
+ for index, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ index := index
+ g.Go(func() error {
+ var err error
+ reply[index], err = sys.peerClients[index].GetSysConfig(ctx)
+ return err
+ }, index)
+ }
+
+ for index, err := range g.Wait() {
+ if err != nil {
+ sys.addNodeErr(&reply[index], sys.peerClients[index], err)
+ }
+ }
+ return reply
+}
+
+// GetSysServices - Get information about system services
+// (only the services that are of concern to minio)
+func (sys *NotificationSys) GetSysServices(ctx context.Context) []madmin.SysServices {
+ reply := make([]madmin.SysServices, len(sys.peerClients))
+
+ g := errgroup.WithNErrs(len(sys.peerClients))
+ for index, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ index := index
+ g.Go(func() error {
+ var err error
+ reply[index], err = sys.peerClients[index].GetSELinuxInfo(ctx)
+ return err
+ }, index)
+ }
+
+ for index, err := range g.Wait() {
+ if err != nil {
+ sys.addNodeErr(&reply[index], sys.peerClients[index], err)
+ }
+ }
+ return reply
+}
+
+func (sys *NotificationSys) addNodeErr(nodeInfo madmin.NodeInfo, peerClient *peerRESTClient, err error) {
+ addr := peerClient.host.String()
+ reqInfo := (&logger.ReqInfo{}).AppendTags("remotePeer", addr)
+ ctx := logger.SetReqInfo(GlobalContext, reqInfo)
+ peersLogOnceIf(ctx, err, "add-node-err-"+addr)
+ nodeInfo.SetAddr(addr)
+ nodeInfo.SetError(err.Error())
+}
+
+// GetSysErrors - Memory information
+func (sys *NotificationSys) GetSysErrors(ctx context.Context) []madmin.SysErrors {
+ reply := make([]madmin.SysErrors, len(sys.peerClients))
+
+ g := errgroup.WithNErrs(len(sys.peerClients))
+ for index, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ index := index
+ g.Go(func() error {
+ var err error
+ reply[index], err = sys.peerClients[index].GetSysErrors(ctx)
+ return err
+ }, index)
+ }
+
+ for index, err := range g.Wait() {
+ if err != nil {
+ sys.addNodeErr(&reply[index], sys.peerClients[index], err)
+ }
+ }
+ return reply
+}
+
+// GetMemInfo - Memory information
+func (sys *NotificationSys) GetMemInfo(ctx context.Context) []madmin.MemInfo {
+ reply := make([]madmin.MemInfo, len(sys.peerClients))
+
+ g := errgroup.WithNErrs(len(sys.peerClients))
+ for index, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ index := index
+ g.Go(func() error {
+ var err error
+ reply[index], err = sys.peerClients[index].GetMemInfo(ctx)
+ return err
+ }, index)
+ }
+
+ for index, err := range g.Wait() {
+ if err != nil {
+ sys.addNodeErr(&reply[index], sys.peerClients[index], err)
+ }
+ }
+ return reply
+}
+
+// GetProcInfo - Process information
+func (sys *NotificationSys) GetProcInfo(ctx context.Context) []madmin.ProcInfo {
+ reply := make([]madmin.ProcInfo, len(sys.peerClients))
+
+ g := errgroup.WithNErrs(len(sys.peerClients))
+ for index, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ index := index
+ g.Go(func() error {
+ var err error
+ reply[index], err = sys.peerClients[index].GetProcInfo(ctx)
+ return err
+ }, index)
+ }
+
+ for index, err := range g.Wait() {
+ if err != nil {
+ sys.addNodeErr(&reply[index], sys.peerClients[index], err)
+ }
+ }
+ return reply
+}
+
+// Construct a list of offline disks information for a given node.
+// If offlineHost is empty, do it for the local disks.
+func getOfflineDisks(offlineHost string, endpoints EndpointServerPools) []madmin.Disk {
+ var offlineDisks []madmin.Disk
+ for _, pool := range endpoints {
+ for _, ep := range pool.Endpoints {
+ if offlineHost == "" && ep.IsLocal || offlineHost == ep.Host {
+ offlineDisks = append(offlineDisks, madmin.Disk{
+ Endpoint: ep.String(),
+ State: string(madmin.ItemOffline),
+ PoolIndex: ep.PoolIdx,
+ SetIndex: ep.SetIdx,
+ DiskIndex: ep.DiskIdx,
+ })
+ }
+ }
+ }
+ return offlineDisks
+}
+
+// StorageInfo returns disk information across all peers
+func (sys *NotificationSys) StorageInfo(ctx context.Context, objLayer ObjectLayer, metrics bool) StorageInfo {
+ var storageInfo StorageInfo
+ replies := make([]StorageInfo, len(sys.peerClients))
+
+ var wg sync.WaitGroup
+ for i, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ wg.Add(1)
+ go func(client *peerRESTClient, idx int) {
+ defer wg.Done()
+ info, err := client.LocalStorageInfo(ctx, metrics)
+ if err != nil {
+ info.Disks = getOfflineDisks(client.host.String(), globalEndpoints)
+ }
+ replies[idx] = info
+ }(client, i)
+ }
+ wg.Wait()
+
+ // Add local to this server.
+ replies = append(replies, objLayer.LocalStorageInfo(ctx, metrics))
+
+ storageInfo.Backend = objLayer.BackendInfo()
+ for _, sinfo := range replies {
+ storageInfo.Disks = append(storageInfo.Disks, sinfo.Disks...)
+ }
+
+ return storageInfo
+}
+
+// ServerInfo - calls ServerInfo RPC call on all peers.
+func (sys *NotificationSys) ServerInfo(ctx context.Context, metrics bool) []madmin.ServerProperties {
+ reply := make([]madmin.ServerProperties, len(sys.peerClients))
+ var wg sync.WaitGroup
+ for i, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ wg.Add(1)
+ go func(client *peerRESTClient, idx int) {
+ defer wg.Done()
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+ info, err := client.ServerInfo(ctx, metrics)
+ if err != nil {
+ info.Endpoint = client.host.String()
+ info.State = string(madmin.ItemOffline)
+ info.Disks = getOfflineDisks(info.Endpoint, globalEndpoints)
+ }
+ reply[idx] = info
+ }(client, i)
+ }
+ wg.Wait()
+
+ return reply
+}
+
+// restClientFromHash will return a deterministic peerRESTClient based on s.
+// Will return nil if client is local.
+func (sys *NotificationSys) restClientFromHash(s string) (client *peerRESTClient) {
+ if len(sys.peerClients) == 0 {
+ return nil
+ }
+ peerClients := sys.allPeerClients
+ if len(peerClients) == 0 {
+ return nil
+ }
+ idx := xxhash.Sum64String(s) % uint64(len(peerClients))
+ return peerClients[idx]
+}
+
+// GetPeerOnlineCount gets the count of online and offline nodes.
+func (sys *NotificationSys) GetPeerOnlineCount() (nodesOnline, nodesOffline int) {
+ nodesOnline = 1 // Self is always online.
+ nodesOffline = 0
+ nodesOnlineIndex := make([]bool, len(sys.peerClients))
+ var wg sync.WaitGroup
+ for idx, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ wg.Add(1)
+ go func(idx int, client *peerRESTClient) {
+ defer wg.Done()
+ nodesOnlineIndex[idx] = client.restClient.HealthCheckFn()
+ }(idx, client)
+ }
+ wg.Wait()
+
+ for _, online := range nodesOnlineIndex {
+ if online {
+ nodesOnline++
+ } else {
+ nodesOffline++
+ }
+ }
+ return nodesOnline, nodesOffline
+}
+
+// NewNotificationSys - creates new notification system object.
+func NewNotificationSys(endpoints EndpointServerPools) *NotificationSys {
+ remote, all := newPeerRestClients(endpoints)
+ return &NotificationSys{
+ peerClients: remote,
+ allPeerClients: all,
+ }
+}
+
+// GetBandwidthReports - gets the bandwidth report from all nodes including self.
+func (sys *NotificationSys) GetBandwidthReports(ctx context.Context, buckets ...string) bandwidth.BucketBandwidthReport {
+ reports := make([]*bandwidth.BucketBandwidthReport, len(sys.peerClients))
+ g := errgroup.WithNErrs(len(sys.peerClients))
+ for index := range sys.peerClients {
+ if sys.peerClients[index] == nil {
+ continue
+ }
+ index := index
+ g.Go(func() error {
+ var err error
+ reports[index], err = sys.peerClients[index].MonitorBandwidth(ctx, buckets)
+ return err
+ }, index)
+ }
+
+ for index, err := range g.Wait() {
+ reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress",
+ sys.peerClients[index].host.String())
+ ctx := logger.SetReqInfo(ctx, reqInfo)
+ peersLogOnceIf(ctx, err, sys.peerClients[index].host.String())
+ }
+ reports = append(reports, globalBucketMonitor.GetReport(bandwidth.SelectBuckets(buckets...)))
+ consolidatedReport := bandwidth.BucketBandwidthReport{
+ BucketStats: make(map[bandwidth.BucketOptions]bandwidth.Details),
+ }
+ for _, report := range reports {
+ if report == nil || report.BucketStats == nil {
+ continue
+ }
+ for opts := range report.BucketStats {
+ d, ok := consolidatedReport.BucketStats[opts]
+ if !ok {
+ d = bandwidth.Details{
+ LimitInBytesPerSecond: report.BucketStats[opts].LimitInBytesPerSecond,
+ }
+ }
+ dt, ok := report.BucketStats[opts]
+ if ok {
+ d.CurrentBandwidthInBytesPerSecond += dt.CurrentBandwidthInBytesPerSecond
+ }
+ consolidatedReport.BucketStats[opts] = d
+ }
+ }
+ return consolidatedReport
+}
+
+func (sys *NotificationSys) collectPeerMetrics(ctx context.Context, peerChannels []<-chan MetricV2, g *errgroup.Group) <-chan MetricV2 {
+ ch := make(chan MetricV2)
+ var wg sync.WaitGroup
+ for index, err := range g.Wait() {
+ if err != nil {
+ if sys.peerClients[index] != nil {
+ reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress",
+ sys.peerClients[index].host.String())
+ peersLogOnceIf(logger.SetReqInfo(ctx, reqInfo), err, sys.peerClients[index].host.String())
+ } else {
+ peersLogOnceIf(ctx, err, "peer-offline")
+ }
+ continue
+ }
+ wg.Add(1)
+ go func(ctx context.Context, peerChannel <-chan MetricV2, wg *sync.WaitGroup) {
+ defer wg.Done()
+ for {
+ select {
+ case m, ok := <-peerChannel:
+ if !ok {
+ return
+ }
+ select {
+ case ch <- m:
+ case <-ctx.Done():
+ return
+ }
+ case <-ctx.Done():
+ return
+ }
+ }
+ }(ctx, peerChannels[index], &wg)
+ }
+ go func(wg *sync.WaitGroup, ch chan MetricV2) {
+ wg.Wait()
+ xioutil.SafeClose(ch)
+ }(&wg, ch)
+ return ch
+}
+
+// GetBucketMetrics - gets the cluster level bucket metrics from all nodes excluding self.
+func (sys *NotificationSys) GetBucketMetrics(ctx context.Context) <-chan MetricV2 {
+ if sys == nil {
+ return nil
+ }
+ g := errgroup.WithNErrs(len(sys.peerClients))
+ peerChannels := make([]<-chan MetricV2, len(sys.peerClients))
+ for index := range sys.peerClients {
+ g.Go(func() error {
+ if sys.peerClients[index] == nil {
+ return errPeerNotReachable
+ }
+ var err error
+ peerChannels[index], err = sys.peerClients[index].GetPeerBucketMetrics(ctx)
+ return err
+ }, index)
+ }
+ return sys.collectPeerMetrics(ctx, peerChannels, g)
+}
+
+// GetClusterMetrics - gets the cluster metrics from all nodes excluding self.
+func (sys *NotificationSys) GetClusterMetrics(ctx context.Context) <-chan MetricV2 {
+ if sys == nil {
+ return nil
+ }
+ g := errgroup.WithNErrs(len(sys.peerClients))
+ peerChannels := make([]<-chan MetricV2, len(sys.peerClients))
+ for index := range sys.peerClients {
+ g.Go(func() error {
+ if sys.peerClients[index] == nil {
+ return errPeerNotReachable
+ }
+ var err error
+ peerChannels[index], err = sys.peerClients[index].GetPeerMetrics(ctx)
+ return err
+ }, index)
+ }
+ return sys.collectPeerMetrics(ctx, peerChannels, g)
+}
+
+// ServiceFreeze freezes all S3 API calls when 'freeze' is true,
+// 'freeze' is 'false' would resume all S3 API calls again.
+// NOTE: once a tenant is frozen either two things needs to
+// happen before resuming normal operations.
+// - Server needs to be restarted 'mc admin service restart'
+// - 'freeze' should be set to 'false' for this call
+// to resume normal operations.
+func (sys *NotificationSys) ServiceFreeze(ctx context.Context, freeze bool) []NotificationPeerErr {
+ serviceSig := serviceUnFreeze
+ if freeze {
+ serviceSig = serviceFreeze
+ }
+ ng := WithNPeers(len(sys.peerClients))
+ for idx, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ client := client
+ ng.Go(GlobalContext, func() error {
+ return client.SignalService(serviceSig, "", false, nil)
+ }, idx, *client.host)
+ }
+ nerrs := ng.Wait()
+ if freeze {
+ freezeServices()
+ } else {
+ unfreezeServices()
+ }
+ return nerrs
+}
+
+// Netperf - perform mesh style network throughput test
+func (sys *NotificationSys) Netperf(ctx context.Context, duration time.Duration) []madmin.NetperfNodeResult {
+ length := len(sys.allPeerClients)
+ if length == 0 {
+ // For single node erasure setup.
+ return nil
+ }
+ results := make([]madmin.NetperfNodeResult, length)
+
+ scheme := "http"
+ if globalIsTLS {
+ scheme = "https"
+ }
+
+ var wg sync.WaitGroup
+ for index := range sys.peerClients {
+ if sys.peerClients[index] == nil {
+ continue
+ }
+ wg.Add(1)
+ go func(index int) {
+ defer wg.Done()
+ r, err := sys.peerClients[index].Netperf(ctx, duration)
+ u := &url.URL{
+ Scheme: scheme,
+ Host: sys.peerClients[index].host.String(),
+ }
+ if err != nil {
+ results[index].Error = err.Error()
+ } else {
+ results[index] = r
+ }
+ results[index].Endpoint = u.String()
+ }(index)
+ }
+
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ r := netperf(ctx, duration)
+ u := &url.URL{
+ Scheme: scheme,
+ Host: globalLocalNodeName,
+ }
+ results[len(results)-1] = r
+ results[len(results)-1].Endpoint = u.String()
+ }()
+ wg.Wait()
+
+ return results
+}
+
+// SpeedTest run GET/PUT tests at input concurrency for requested object size,
+// optionally you can extend the tests longer with time.Duration.
+func (sys *NotificationSys) SpeedTest(ctx context.Context, sopts speedTestOpts) []SpeedTestResult {
+ length := len(sys.allPeerClients)
+ if length == 0 {
+ // For single node erasure setup.
+ length = 1
+ }
+ results := make([]SpeedTestResult, length)
+
+ scheme := "http"
+ if globalIsTLS {
+ scheme = "https"
+ }
+
+ var wg sync.WaitGroup
+ for index := range sys.peerClients {
+ if sys.peerClients[index] == nil {
+ continue
+ }
+ wg.Add(1)
+ go func(index int) {
+ defer wg.Done()
+ r, err := sys.peerClients[index].SpeedTest(ctx, sopts)
+ u := &url.URL{
+ Scheme: scheme,
+ Host: sys.peerClients[index].host.String(),
+ }
+ if err != nil {
+ results[index].Error = err.Error()
+ } else {
+ results[index] = r
+ }
+ results[index].Endpoint = u.String()
+ }(index)
+ }
+
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ r, err := selfSpeedTest(ctx, sopts)
+ u := &url.URL{
+ Scheme: scheme,
+ Host: globalLocalNodeName,
+ }
+ if err != nil {
+ results[len(results)-1].Error = err.Error()
+ } else {
+ results[len(results)-1] = r
+ }
+ results[len(results)-1].Endpoint = u.String()
+ }()
+ wg.Wait()
+
+ return results
+}
+
+// DriveSpeedTest - Drive performance information
+func (sys *NotificationSys) DriveSpeedTest(ctx context.Context, opts madmin.DriveSpeedTestOpts) chan madmin.DriveSpeedTestResult {
+ ch := make(chan madmin.DriveSpeedTestResult)
+ var wg sync.WaitGroup
+ for _, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ wg.Add(1)
+ go func(client *peerRESTClient) {
+ defer wg.Done()
+ resp, err := client.DriveSpeedTest(ctx, opts)
+ if err != nil {
+ resp.Error = err.Error()
+ }
+
+ select {
+ case <-ctx.Done():
+ case ch <- resp:
+ }
+
+ reqInfo := (&logger.ReqInfo{}).AppendTags("remotePeer", client.host.String())
+ ctx := logger.SetReqInfo(GlobalContext, reqInfo)
+ peersLogOnceIf(ctx, err, client.host.String())
+ }(client)
+ }
+
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ select {
+ case <-ctx.Done():
+ case ch <- driveSpeedTest(ctx, opts):
+ }
+ }()
+
+ go func(wg *sync.WaitGroup, ch chan madmin.DriveSpeedTestResult) {
+ wg.Wait()
+ xioutil.SafeClose(ch)
+ }(&wg, ch)
+
+ return ch
+}
+
+// ReloadSiteReplicationConfig - tells all peer minio nodes to reload the
+// site-replication configuration.
+func (sys *NotificationSys) ReloadSiteReplicationConfig(ctx context.Context) []error {
+ errs := make([]error, len(sys.allPeerClients))
+ var wg sync.WaitGroup
+ for index := range sys.peerClients {
+ if sys.peerClients[index] == nil {
+ continue
+ }
+ wg.Add(1)
+ go func(index int) {
+ defer wg.Done()
+ errs[index] = sys.peerClients[index].ReloadSiteReplicationConfig(ctx)
+ }(index)
+ }
+
+ wg.Wait()
+ return errs
+}
+
+// GetLastDayTierStats fetches per-tier stats of the last 24hrs from all peers
+func (sys *NotificationSys) GetLastDayTierStats(ctx context.Context) DailyAllTierStats {
+ errs := make([]error, len(sys.allPeerClients))
+ lastDayStats := make([]DailyAllTierStats, len(sys.allPeerClients))
+ var wg sync.WaitGroup
+ for index := range sys.peerClients {
+ if sys.peerClients[index] == nil {
+ continue
+ }
+ wg.Add(1)
+ go func(index int) {
+ defer wg.Done()
+ lastDayStats[index], errs[index] = sys.peerClients[index].GetLastDayTierStats(ctx)
+ }(index)
+ }
+
+ wg.Wait()
+ merged := globalTransitionState.getDailyAllTierStats()
+ for i, stat := range lastDayStats {
+ if errs[i] != nil {
+ peersLogOnceIf(ctx, fmt.Errorf("failed to fetch last day tier stats: %w", errs[i]), sys.peerClients[i].host.String())
+ continue
+ }
+ merged.merge(stat)
+ }
+ return merged
+}
+
+// GetReplicationMRF - Get replication MRF from all peers.
+func (sys *NotificationSys) GetReplicationMRF(ctx context.Context, bucket, node string) (mrfCh chan madmin.ReplicationMRF, err error) {
+ g := errgroup.WithNErrs(len(sys.peerClients))
+ peerChannels := make([]<-chan madmin.ReplicationMRF, len(sys.peerClients))
+ for index, client := range sys.peerClients {
+ if client == nil {
+ continue
+ }
+ host := client.host.String()
+ if host != node && node != "all" {
+ continue
+ }
+ index := index
+ g.Go(func() error {
+ var err error
+ peerChannels[index], err = sys.peerClients[index].GetReplicationMRF(ctx, bucket)
+ return err
+ }, index)
+ }
+ mrfCh = make(chan madmin.ReplicationMRF, 4000)
+ var wg sync.WaitGroup
+
+ for index, err := range g.Wait() {
+ if err != nil {
+ if sys.peerClients[index] != nil {
+ reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress",
+ sys.peerClients[index].host.String())
+ peersLogOnceIf(logger.SetReqInfo(ctx, reqInfo), err, sys.peerClients[index].host.String())
+ } else {
+ peersLogOnceIf(ctx, err, "peer-offline")
+ }
+ continue
+ }
+ wg.Add(1)
+ go func(ctx context.Context, peerChannel <-chan madmin.ReplicationMRF, wg *sync.WaitGroup) {
+ defer wg.Done()
+ for {
+ select {
+ case m, ok := <-peerChannel:
+ if !ok {
+ return
+ }
+ select {
+ case <-ctx.Done():
+ return
+ case mrfCh <- m:
+ }
+ case <-ctx.Done():
+ return
+ }
+ }
+ }(ctx, peerChannels[index], &wg)
+ }
+ wg.Add(1)
+ go func(ch chan madmin.ReplicationMRF) error {
+ defer wg.Done()
+ if node != "all" && node != globalLocalNodeName {
+ return nil
+ }
+ mCh, err := globalReplicationPool.Get().getMRF(ctx, bucket)
+ if err != nil {
+ return err
+ }
+ for e := range mCh {
+ select {
+ case <-ctx.Done():
+ return err
+ case mrfCh <- e:
+ }
+ }
+ return nil
+ }(mrfCh)
+ go func(wg *sync.WaitGroup) {
+ wg.Wait()
+ xioutil.SafeClose(mrfCh)
+ }(&wg)
+ return mrfCh, nil
+}
diff --git a/cmd/object-api-common.go b/cmd/object-api-common.go
new file mode 100644
index 00000000..a4fe6a97
--- /dev/null
+++ b/cmd/object-api-common.go
@@ -0,0 +1,71 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "sync"
+
+ "github.com/dustin/go-humanize"
+)
+
+const (
+ // Block size used for all internal operations version 1.
+
+ // TLDR..
+ // Not used anymore xl.meta captures the right blockSize
+ // so blockSizeV2 should be used for all future purposes.
+ // this value is kept here to calculate the max API
+ // requests based on RAM size for existing content.
+ blockSizeV1 = 10 * humanize.MiByte
+
+ // Block size used in erasure coding version 2.
+ blockSizeV2 = 1 * humanize.MiByte
+
+ // Buckets meta prefix.
+ bucketMetaPrefix = "buckets"
+
+ // Deleted Buckets prefix.
+ deletedBucketsPrefix = ".deleted"
+
+ // ETag (hex encoded md5sum) of empty string.
+ emptyETag = "d41d8cd98f00b204e9800998ecf8427e"
+)
+
+// Global object layer mutex, used for safely updating object layer.
+var globalObjLayerMutex sync.RWMutex
+
+// Global object layer, only accessed by globalObjectAPI.
+var globalObjectAPI ObjectLayer
+
+type storageOpts struct {
+ cleanUp bool
+ healthCheck bool
+}
+
+// Depending on the disk type network or local, initialize storage API.
+func newStorageAPI(endpoint Endpoint, opts storageOpts) (storage StorageAPI, err error) {
+ if endpoint.IsLocal {
+ storage, err := newXLStorage(endpoint, opts.cleanUp)
+ if err != nil {
+ return nil, err
+ }
+ return newXLStorageDiskIDCheck(storage, opts.healthCheck), nil
+ }
+
+ return newStorageRESTClient(endpoint, opts.healthCheck, globalGrid.Load())
+}
diff --git a/cmd/object-api-datatypes.go b/cmd/object-api-datatypes.go
new file mode 100644
index 00000000..d00e2be1
--- /dev/null
+++ b/cmd/object-api-datatypes.go
@@ -0,0 +1,682 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "io"
+ "maps"
+ "math"
+ "net/http"
+ "time"
+
+ "github.com/dustin/go-humanize"
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio/internal/bucket/replication"
+ "github.com/minio/minio/internal/hash"
+)
+
+//go:generate msgp -file $GOFILE -io=false -tests=false -unexported=false
+
+// BackendType - represents different backend types.
+type BackendType int
+
+// Enum for different backend types.
+const (
+ Unknown = BackendType(madmin.Unknown)
+ // Filesystem backend.
+ BackendFS = BackendType(madmin.FS)
+ // Multi disk BackendErasure (single, distributed) backend.
+ BackendErasure = BackendType(madmin.Erasure)
+ // Add your own backend.
+)
+
+// StorageInfo - represents total capacity of underlying storage.
+type StorageInfo = madmin.StorageInfo
+
+// objectHistogramInterval is an interval that will be
+// used to report the histogram of objects data sizes
+type objectHistogramInterval struct {
+ name string
+ start, end int64
+}
+
+const (
+ // dataUsageBucketLenV1 must be length of ObjectsHistogramIntervalsV1
+ dataUsageBucketLenV1 = 7
+ // dataUsageBucketLen must be length of ObjectsHistogramIntervals
+ dataUsageBucketLen = 11
+ dataUsageVersionLen = 7
+)
+
+// ObjectsHistogramIntervalsV1 is the list of all intervals
+// of object sizes to be included in objects histogram(V1).
+var ObjectsHistogramIntervalsV1 = [dataUsageBucketLenV1]objectHistogramInterval{
+ {"LESS_THAN_1024_B", 0, humanize.KiByte - 1},
+ {"BETWEEN_1024B_AND_1_MB", humanize.KiByte, humanize.MiByte - 1},
+ {"BETWEEN_1_MB_AND_10_MB", humanize.MiByte, humanize.MiByte*10 - 1},
+ {"BETWEEN_10_MB_AND_64_MB", humanize.MiByte * 10, humanize.MiByte*64 - 1},
+ {"BETWEEN_64_MB_AND_128_MB", humanize.MiByte * 64, humanize.MiByte*128 - 1},
+ {"BETWEEN_128_MB_AND_512_MB", humanize.MiByte * 128, humanize.MiByte*512 - 1},
+ {"GREATER_THAN_512_MB", humanize.MiByte * 512, math.MaxInt64},
+}
+
+// ObjectsHistogramIntervals is the list of all intervals
+// of object sizes to be included in objects histogram.
+// Note: this histogram expands 1024B-1MB to incl. 1024B-64KB, 64KB-256KB, 256KB-512KB and 512KB-1MiB
+var ObjectsHistogramIntervals = [dataUsageBucketLen]objectHistogramInterval{
+ {"LESS_THAN_1024_B", 0, humanize.KiByte - 1},
+ {"BETWEEN_1024_B_AND_64_KB", humanize.KiByte, 64*humanize.KiByte - 1}, // not exported, for support use only
+ {"BETWEEN_64_KB_AND_256_KB", 64 * humanize.KiByte, 256*humanize.KiByte - 1}, // not exported, for support use only
+ {"BETWEEN_256_KB_AND_512_KB", 256 * humanize.KiByte, 512*humanize.KiByte - 1}, // not exported, for support use only
+ {"BETWEEN_512_KB_AND_1_MB", 512 * humanize.KiByte, humanize.MiByte - 1}, // not exported, for support use only
+ {"BETWEEN_1024B_AND_1_MB", humanize.KiByte, humanize.MiByte - 1},
+ {"BETWEEN_1_MB_AND_10_MB", humanize.MiByte, humanize.MiByte*10 - 1},
+ {"BETWEEN_10_MB_AND_64_MB", humanize.MiByte * 10, humanize.MiByte*64 - 1},
+ {"BETWEEN_64_MB_AND_128_MB", humanize.MiByte * 64, humanize.MiByte*128 - 1},
+ {"BETWEEN_128_MB_AND_512_MB", humanize.MiByte * 128, humanize.MiByte*512 - 1},
+ {"GREATER_THAN_512_MB", humanize.MiByte * 512, math.MaxInt64},
+}
+
+// ObjectsVersionCountIntervals is the list of all intervals
+// of object version count to be included in objects histogram.
+var ObjectsVersionCountIntervals = [dataUsageVersionLen]objectHistogramInterval{
+ {"UNVERSIONED", 0, 0},
+ {"SINGLE_VERSION", 1, 1},
+ {"BETWEEN_2_AND_10", 2, 9},
+ {"BETWEEN_10_AND_100", 10, 99},
+ {"BETWEEN_100_AND_1000", 100, 999},
+ {"BETWEEN_1000_AND_10000", 1000, 9999},
+ {"GREATER_THAN_10000", 10000, math.MaxInt64},
+}
+
+// BucketInfo - represents bucket metadata.
+type BucketInfo struct {
+ // Name of the bucket.
+ Name string
+
+ // Date and time when the bucket was created.
+ Created time.Time
+ Deleted time.Time
+
+ // Bucket features enabled
+ Versioning, ObjectLocking bool
+}
+
+// ObjectInfo - represents object metadata.
+type ObjectInfo struct {
+ // Name of the bucket.
+ Bucket string
+
+ // Name of the object.
+ Name string
+
+ // Date and time when the object was last modified.
+ ModTime time.Time
+
+ // Total object size.
+ Size int64
+
+ // Actual size is the real size of the object uploaded by client.
+ ActualSize *int64
+
+ // IsDir indicates if the object is prefix.
+ IsDir bool
+
+ // Hex encoded unique entity tag of the object.
+ ETag string
+
+ // Version ID of this object.
+ VersionID string
+
+ // IsLatest indicates if this is the latest current version
+ // latest can be true for delete marker or a version.
+ IsLatest bool
+
+ // DeleteMarker indicates if the versionId corresponds
+ // to a delete marker on an object.
+ DeleteMarker bool
+
+ // Transitioned object information
+ TransitionedObject TransitionedObject
+
+ // RestoreExpires indicates date a restored object expires
+ RestoreExpires time.Time
+
+ // RestoreOngoing indicates if a restore is in progress
+ RestoreOngoing bool
+
+ // A standard MIME type describing the format of the object.
+ ContentType string
+
+ // Specifies what content encodings have been applied to the object and thus
+ // what decoding mechanisms must be applied to obtain the object referenced
+ // by the Content-Type header field.
+ ContentEncoding string
+
+ // Date and time at which the object is no longer able to be cached
+ Expires time.Time
+
+ // Cache-Control - Specifies caching behavior along the request/reply chain
+ CacheControl string
+
+ // Specify object storage class
+ StorageClass string
+
+ ReplicationStatusInternal string
+ ReplicationStatus replication.StatusType
+ // User-Defined metadata
+ UserDefined map[string]string
+
+ // User-Defined object tags
+ UserTags string
+
+ // List of individual parts, maximum size of upto 10,000
+ Parts []ObjectPartInfo `json:"-"`
+
+ // Implements writer and reader used by CopyObject API
+ Writer io.WriteCloser `json:"-" msg:"-"`
+ Reader *hash.Reader `json:"-" msg:"-"`
+ PutObjReader *PutObjReader `json:"-" msg:"-"`
+
+ metadataOnly bool
+ versionOnly bool // adds a new version, only used by CopyObject
+ keyRotation bool
+
+ // Date and time when the object was last accessed.
+ AccTime time.Time
+
+ Legacy bool // indicates object on disk is in legacy data format
+
+ // internal representation of version purge status
+ VersionPurgeStatusInternal string
+ VersionPurgeStatus VersionPurgeStatusType
+
+ replicationDecision string // internal representation of replication decision for use by DeleteObject handler
+ // The total count of all versions of this object
+ NumVersions int
+ // The modtime of the successor object version if any
+ SuccessorModTime time.Time
+
+ // Checksums added on upload.
+ // Encoded, maybe encrypted.
+ Checksum []byte
+
+ // Inlined
+ Inlined bool
+
+ DataBlocks int
+ ParityBlocks int
+}
+
+// ExpiresStr returns a stringified version of Expires header in http.TimeFormat
+func (o ObjectInfo) ExpiresStr() string {
+ var expires string
+ if !o.Expires.IsZero() {
+ expires = o.Expires.UTC().Format(http.TimeFormat)
+ }
+ return expires
+}
+
+// ArchiveInfo returns any saved zip archive meta information.
+// It will be decrypted if needed.
+func (o *ObjectInfo) ArchiveInfo(h http.Header) []byte {
+ if len(o.UserDefined) == 0 {
+ return nil
+ }
+ z, ok := o.UserDefined[archiveInfoMetadataKey]
+ if !ok {
+ return nil
+ }
+ data := []byte(z)
+ if v, ok := o.UserDefined[archiveTypeMetadataKey]; ok && v == archiveTypeEnc {
+ decrypted, err := o.metadataDecrypter(h)(archiveTypeEnc, data)
+ if err != nil {
+ encLogIf(GlobalContext, err)
+ return nil
+ }
+ data = decrypted
+ }
+ return data
+}
+
+// Clone - Returns a cloned copy of current objectInfo
+func (o *ObjectInfo) Clone() (cinfo ObjectInfo) {
+ cinfo = ObjectInfo{
+ Bucket: o.Bucket,
+ Name: o.Name,
+ ModTime: o.ModTime,
+ Size: o.Size,
+ IsDir: o.IsDir,
+ ETag: o.ETag,
+ VersionID: o.VersionID,
+ IsLatest: o.IsLatest,
+ DeleteMarker: o.DeleteMarker,
+ TransitionedObject: o.TransitionedObject,
+ RestoreExpires: o.RestoreExpires,
+ RestoreOngoing: o.RestoreOngoing,
+ ContentType: o.ContentType,
+ ContentEncoding: o.ContentEncoding,
+ Expires: o.Expires,
+ StorageClass: o.StorageClass,
+ ReplicationStatus: o.ReplicationStatus,
+ UserTags: o.UserTags,
+ Parts: o.Parts,
+ Writer: o.Writer,
+ Reader: o.Reader,
+ PutObjReader: o.PutObjReader,
+ metadataOnly: o.metadataOnly,
+ versionOnly: o.versionOnly,
+ keyRotation: o.keyRotation,
+ AccTime: o.AccTime,
+ Legacy: o.Legacy,
+ VersionPurgeStatus: o.VersionPurgeStatus,
+ NumVersions: o.NumVersions,
+ SuccessorModTime: o.SuccessorModTime,
+ ReplicationStatusInternal: o.ReplicationStatusInternal,
+ VersionPurgeStatusInternal: o.VersionPurgeStatusInternal,
+ }
+ cinfo.UserDefined = make(map[string]string, len(o.UserDefined))
+ maps.Copy(cinfo.UserDefined, o.UserDefined)
+ return cinfo
+}
+
+func (o ObjectInfo) tierStats() tierStats {
+ ts := tierStats{
+ TotalSize: uint64(o.Size),
+ NumVersions: 1,
+ }
+ // the current version of an object is accounted towards objects count
+ if o.IsLatest {
+ ts.NumObjects = 1
+ }
+ return ts
+}
+
+// ToObjectInfo converts a replication object info to a partial ObjectInfo
+// do not rely on this function to give you correct ObjectInfo, this
+// function is merely and optimization.
+func (ri ReplicateObjectInfo) ToObjectInfo() ObjectInfo {
+ return ObjectInfo{
+ Name: ri.Name,
+ Bucket: ri.Bucket,
+ VersionID: ri.VersionID,
+ ModTime: ri.ModTime,
+ UserTags: ri.UserTags,
+ Size: ri.Size,
+ ActualSize: &ri.ActualSize,
+ ReplicationStatus: ri.ReplicationStatus,
+ ReplicationStatusInternal: ri.ReplicationStatusInternal,
+ VersionPurgeStatus: ri.VersionPurgeStatus,
+ VersionPurgeStatusInternal: ri.VersionPurgeStatusInternal,
+ DeleteMarker: true,
+ UserDefined: map[string]string{},
+ Checksum: ri.Checksum,
+ }
+}
+
+// ReplicateObjectInfo represents object info to be replicated
+type ReplicateObjectInfo struct {
+ Name string
+ Bucket string
+ VersionID string
+ ETag string
+ Size int64
+ ActualSize int64
+ ModTime time.Time
+ UserTags string
+ SSEC bool
+ ReplicationStatus replication.StatusType
+ ReplicationStatusInternal string
+ VersionPurgeStatusInternal string
+ VersionPurgeStatus VersionPurgeStatusType
+ ReplicationState ReplicationState
+ DeleteMarker bool
+
+ OpType replication.Type
+ EventType string
+ RetryCount uint32
+ ResetID string
+ Dsc ReplicateDecision
+ ExistingObjResync ResyncDecision
+ TargetArn string
+ TargetStatuses map[string]replication.StatusType
+ TargetPurgeStatuses map[string]VersionPurgeStatusType
+ ReplicationTimestamp time.Time
+ Checksum []byte
+}
+
+// MultipartInfo captures metadata information about the uploadId
+// this data structure is used primarily for some internal purposes
+// for verifying upload type such as was the upload
+// - encrypted
+// - compressed
+type MultipartInfo struct {
+ // Name of the bucket.
+ Bucket string
+
+ // Name of the object.
+ Object string
+
+ // Upload ID identifying the multipart upload whose parts are being listed.
+ UploadID string
+
+ // Date and time at which the multipart upload was initiated.
+ Initiated time.Time
+
+ // Any metadata set during InitMultipartUpload, including encryption headers.
+ UserDefined map[string]string
+}
+
+// ListPartsInfo - represents list of all parts.
+type ListPartsInfo struct {
+ // Name of the bucket.
+ Bucket string
+
+ // Name of the object.
+ Object string
+
+ // Upload ID identifying the multipart upload whose parts are being listed.
+ UploadID string
+
+ // The class of storage used to store the object.
+ StorageClass string
+
+ // Part number after which listing begins.
+ PartNumberMarker int
+
+ // When a list is truncated, this element specifies the last part in the list,
+ // as well as the value to use for the part-number-marker request parameter
+ // in a subsequent request.
+ NextPartNumberMarker int
+
+ // Maximum number of parts that were allowed in the response.
+ MaxParts int
+
+ // Indicates whether the returned list of parts is truncated.
+ IsTruncated bool
+
+ // List of all parts.
+ Parts []PartInfo
+
+ // Any metadata set during InitMultipartUpload, including encryption headers.
+ UserDefined map[string]string
+
+ // ChecksumAlgorithm if set
+ ChecksumAlgorithm string
+
+ // ChecksumType if set
+ ChecksumType string
+}
+
+// Lookup - returns if uploadID is valid
+func (lm ListMultipartsInfo) Lookup(uploadID string) bool {
+ for _, upload := range lm.Uploads {
+ if upload.UploadID == uploadID {
+ return true
+ }
+ }
+ return false
+}
+
+// ListMultipartsInfo - represents bucket resources for incomplete multipart uploads.
+type ListMultipartsInfo struct {
+ // Together with upload-id-marker, this parameter specifies the multipart upload
+ // after which listing should begin.
+ KeyMarker string
+
+ // Together with key-marker, specifies the multipart upload after which listing
+ // should begin. If key-marker is not specified, the upload-id-marker parameter
+ // is ignored.
+ UploadIDMarker string
+
+ // When a list is truncated, this element specifies the value that should be
+ // used for the key-marker request parameter in a subsequent request.
+ NextKeyMarker string
+
+ // When a list is truncated, this element specifies the value that should be
+ // used for the upload-id-marker request parameter in a subsequent request.
+ NextUploadIDMarker string
+
+ // Maximum number of multipart uploads that could have been included in the
+ // response.
+ MaxUploads int
+
+ // Indicates whether the returned list of multipart uploads is truncated. A
+ // value of true indicates that the list was truncated. The list can be truncated
+ // if the number of multipart uploads exceeds the limit allowed or specified
+ // by max uploads.
+ IsTruncated bool
+
+ // List of all pending uploads.
+ Uploads []MultipartInfo
+
+ // When a prefix is provided in the request, The result contains only keys
+ // starting with the specified prefix.
+ Prefix string
+
+ // A character used to truncate the object prefixes.
+ // NOTE: only supported delimiter is '/'.
+ Delimiter string
+
+ // CommonPrefixes contains all (if there are any) keys between Prefix and the
+ // next occurrence of the string specified by delimiter.
+ CommonPrefixes []string
+
+ EncodingType string // Not supported yet.
+}
+
+// TransitionedObject transitioned object tier and status.
+type TransitionedObject struct {
+ Name string
+ VersionID string
+ Tier string
+ FreeVersion bool
+ Status string
+}
+
+// DeletedObjectInfo - container for list objects versions deleted objects.
+type DeletedObjectInfo struct {
+ // Name of the bucket.
+ Bucket string
+
+ // Name of the object.
+ Name string
+
+ // Date and time when the object was last modified.
+ ModTime time.Time
+
+ // Version ID of this object.
+ VersionID string
+
+ // Indicates the deleted marker is latest
+ IsLatest bool
+}
+
+// ListObjectVersionsInfo - container for list objects versions.
+type ListObjectVersionsInfo struct {
+ // Indicates whether the returned list objects response is truncated. A
+ // value of true indicates that the list was truncated. The list can be truncated
+ // if the number of objects exceeds the limit allowed or specified
+ // by max keys.
+ IsTruncated bool
+
+ // When response is truncated (the IsTruncated element value in the response is true),
+ // you can use the key name in this field as marker in the subsequent
+ // request to get next set of objects.
+ //
+ // NOTE: AWS S3 returns NextMarker only if you have delimiter request parameter specified,
+ // MinIO always returns NextMarker.
+ NextMarker string
+
+ // NextVersionIDMarker may be set of IsTruncated is true
+ NextVersionIDMarker string
+
+ // List of objects info for this request.
+ Objects []ObjectInfo
+
+ // List of prefixes for this request.
+ Prefixes []string
+}
+
+// ListObjectsInfo - container for list objects.
+type ListObjectsInfo struct {
+ // Indicates whether the returned list objects response is truncated. A
+ // value of true indicates that the list was truncated. The list can be truncated
+ // if the number of objects exceeds the limit allowed or specified
+ // by max keys.
+ IsTruncated bool
+
+ // When response is truncated (the IsTruncated element value in the response is true),
+ // you can use the key name in this field as marker in the subsequent
+ // request to get next set of objects.
+ //
+ // NOTE: AWS S3 returns NextMarker only if you have delimiter request parameter specified,
+ // MinIO always returns NextMarker.
+ NextMarker string
+
+ // List of objects info for this request.
+ Objects []ObjectInfo
+
+ // List of prefixes for this request.
+ Prefixes []string
+}
+
+// ListObjectsV2Info - container for list objects version 2.
+type ListObjectsV2Info struct {
+ // Indicates whether the returned list objects response is truncated. A
+ // value of true indicates that the list was truncated. The list can be truncated
+ // if the number of objects exceeds the limit allowed or specified
+ // by max keys.
+ IsTruncated bool
+
+ // When response is truncated (the IsTruncated element value in the response
+ // is true), you can use the key name in this field as marker in the subsequent
+ // request to get next set of objects.
+ //
+ // NOTE: This element is returned only if you have delimiter request parameter
+ // specified.
+ ContinuationToken string
+ NextContinuationToken string
+
+ // List of objects info for this request.
+ Objects []ObjectInfo
+
+ // List of prefixes for this request.
+ Prefixes []string
+}
+
+// PartInfo - represents individual part metadata.
+type PartInfo struct {
+ // Part number that identifies the part. This is a positive integer between
+ // 1 and 10,000.
+ PartNumber int
+
+ // Date and time at which the part was uploaded.
+ LastModified time.Time
+
+ // Entity tag returned when the part was initially uploaded.
+ ETag string
+
+ // Size in bytes of the part.
+ Size int64
+
+ // Real size of the object uploaded by client.
+ ActualSize int64
+
+ // Checksum values
+ ChecksumCRC32 string
+ ChecksumCRC32C string
+ ChecksumSHA1 string
+ ChecksumSHA256 string
+ ChecksumCRC64NVME string
+}
+
+// CompletePart - represents the part that was completed, this is sent by the client
+// during CompleteMultipartUpload request.
+type CompletePart struct {
+ // Part number identifying the part. This is a positive integer between 1 and
+ // 10,000
+ PartNumber int
+
+ // Entity tag returned when the part was uploaded.
+ ETag string
+
+ Size int64
+
+ // Checksum values. Optional.
+ ChecksumCRC32 string
+ ChecksumCRC32C string
+ ChecksumSHA1 string
+ ChecksumSHA256 string
+ ChecksumCRC64NVME string
+}
+
+// CompleteMultipartUpload - represents list of parts which are completed, this is sent by the
+// client during CompleteMultipartUpload request.
+type CompleteMultipartUpload struct {
+ Parts []CompletePart `xml:"Part"`
+}
+
+// NewMultipartUploadResult contains information about a newly created multipart upload.
+type NewMultipartUploadResult struct {
+ UploadID string
+ ChecksumAlgo string
+ ChecksumType string
+}
+
+type getObjectAttributesResponse struct {
+ ETag string `xml:",omitempty"`
+ Checksum *objectAttributesChecksum `xml:",omitempty"`
+ ObjectParts *objectAttributesParts `xml:",omitempty"`
+ StorageClass string `xml:",omitempty"`
+ ObjectSize int64 `xml:",omitempty"`
+}
+
+type objectAttributesChecksum struct {
+ ChecksumCRC32 string `xml:",omitempty"`
+ ChecksumCRC32C string `xml:",omitempty"`
+ ChecksumSHA1 string `xml:",omitempty"`
+ ChecksumSHA256 string `xml:",omitempty"`
+ ChecksumCRC64NVME string `xml:",omitempty"`
+ ChecksumType string `xml:",omitempty"`
+}
+
+type objectAttributesParts struct {
+ IsTruncated bool
+ MaxParts int
+ NextPartNumberMarker int
+ PartNumberMarker int
+ PartsCount int
+ Parts []*objectAttributesPart `xml:"Part"`
+}
+
+type objectAttributesPart struct {
+ PartNumber int
+ Size int64
+ ChecksumCRC32 string `xml:",omitempty"`
+ ChecksumCRC32C string `xml:",omitempty"`
+ ChecksumSHA1 string `xml:",omitempty"`
+ ChecksumSHA256 string `xml:",omitempty"`
+ ChecksumCRC64NVME string `xml:",omitempty"`
+}
+
+type objectAttributesErrorResponse struct {
+ ArgumentValue *string `xml:"ArgumentValue,omitempty"`
+ ArgumentName *string `xml:"ArgumentName"`
+ APIErrorResponse
+}
diff --git a/cmd/object-api-datatypes_gen.go b/cmd/object-api-datatypes_gen.go
new file mode 100644
index 00000000..b15bb3ab
--- /dev/null
+++ b/cmd/object-api-datatypes_gen.go
@@ -0,0 +1,2396 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "github.com/minio/minio/internal/bucket/replication"
+ "github.com/tinylib/msgp/msgp"
+)
+
+// MarshalMsg implements msgp.Marshaler
+func (z BackendType) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ o = msgp.AppendInt(o, int(z))
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BackendType) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ {
+ var zb0001 int
+ zb0001, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ (*z) = BackendType(zb0001)
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z BackendType) Msgsize() (s int) {
+ s = msgp.IntSize
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *BucketInfo) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 5
+ // string "Name"
+ o = append(o, 0x85, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
+ o = msgp.AppendString(o, z.Name)
+ // string "Created"
+ o = append(o, 0xa7, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64)
+ o = msgp.AppendTime(o, z.Created)
+ // string "Deleted"
+ o = append(o, 0xa7, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64)
+ o = msgp.AppendTime(o, z.Deleted)
+ // string "Versioning"
+ o = append(o, 0xaa, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67)
+ o = msgp.AppendBool(o, z.Versioning)
+ // string "ObjectLocking"
+ o = append(o, 0xad, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4c, 0x6f, 0x63, 0x6b, 0x69, 0x6e, 0x67)
+ o = msgp.AppendBool(o, z.ObjectLocking)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BucketInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Name":
+ z.Name, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Name")
+ return
+ }
+ case "Created":
+ z.Created, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Created")
+ return
+ }
+ case "Deleted":
+ z.Deleted, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Deleted")
+ return
+ }
+ case "Versioning":
+ z.Versioning, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Versioning")
+ return
+ }
+ case "ObjectLocking":
+ z.ObjectLocking, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ObjectLocking")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *BucketInfo) Msgsize() (s int) {
+ s = 1 + 5 + msgp.StringPrefixSize + len(z.Name) + 8 + msgp.TimeSize + 8 + msgp.TimeSize + 11 + msgp.BoolSize + 14 + msgp.BoolSize
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *CompleteMultipartUpload) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 1
+ // string "Parts"
+ o = append(o, 0x81, 0xa5, 0x50, 0x61, 0x72, 0x74, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.Parts)))
+ for za0001 := range z.Parts {
+ o, err = z.Parts[za0001].MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Parts", za0001)
+ return
+ }
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *CompleteMultipartUpload) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Parts":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Parts")
+ return
+ }
+ if cap(z.Parts) >= int(zb0002) {
+ z.Parts = (z.Parts)[:zb0002]
+ } else {
+ z.Parts = make([]CompletePart, zb0002)
+ }
+ for za0001 := range z.Parts {
+ bts, err = z.Parts[za0001].UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Parts", za0001)
+ return
+ }
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *CompleteMultipartUpload) Msgsize() (s int) {
+ s = 1 + 6 + msgp.ArrayHeaderSize
+ for za0001 := range z.Parts {
+ s += z.Parts[za0001].Msgsize()
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *CompletePart) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 8
+ // string "PartNumber"
+ o = append(o, 0x88, 0xaa, 0x50, 0x61, 0x72, 0x74, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72)
+ o = msgp.AppendInt(o, z.PartNumber)
+ // string "ETag"
+ o = append(o, 0xa4, 0x45, 0x54, 0x61, 0x67)
+ o = msgp.AppendString(o, z.ETag)
+ // string "Size"
+ o = append(o, 0xa4, 0x53, 0x69, 0x7a, 0x65)
+ o = msgp.AppendInt64(o, z.Size)
+ // string "ChecksumCRC32"
+ o = append(o, 0xad, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x43, 0x52, 0x43, 0x33, 0x32)
+ o = msgp.AppendString(o, z.ChecksumCRC32)
+ // string "ChecksumCRC32C"
+ o = append(o, 0xae, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x43, 0x52, 0x43, 0x33, 0x32, 0x43)
+ o = msgp.AppendString(o, z.ChecksumCRC32C)
+ // string "ChecksumSHA1"
+ o = append(o, 0xac, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x53, 0x48, 0x41, 0x31)
+ o = msgp.AppendString(o, z.ChecksumSHA1)
+ // string "ChecksumSHA256"
+ o = append(o, 0xae, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x53, 0x48, 0x41, 0x32, 0x35, 0x36)
+ o = msgp.AppendString(o, z.ChecksumSHA256)
+ // string "ChecksumCRC64NVME"
+ o = append(o, 0xb1, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x43, 0x52, 0x43, 0x36, 0x34, 0x4e, 0x56, 0x4d, 0x45)
+ o = msgp.AppendString(o, z.ChecksumCRC64NVME)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *CompletePart) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "PartNumber":
+ z.PartNumber, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "PartNumber")
+ return
+ }
+ case "ETag":
+ z.ETag, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ETag")
+ return
+ }
+ case "Size":
+ z.Size, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ case "ChecksumCRC32":
+ z.ChecksumCRC32, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ChecksumCRC32")
+ return
+ }
+ case "ChecksumCRC32C":
+ z.ChecksumCRC32C, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ChecksumCRC32C")
+ return
+ }
+ case "ChecksumSHA1":
+ z.ChecksumSHA1, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ChecksumSHA1")
+ return
+ }
+ case "ChecksumSHA256":
+ z.ChecksumSHA256, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ChecksumSHA256")
+ return
+ }
+ case "ChecksumCRC64NVME":
+ z.ChecksumCRC64NVME, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ChecksumCRC64NVME")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *CompletePart) Msgsize() (s int) {
+ s = 1 + 11 + msgp.IntSize + 5 + msgp.StringPrefixSize + len(z.ETag) + 5 + msgp.Int64Size + 14 + msgp.StringPrefixSize + len(z.ChecksumCRC32) + 15 + msgp.StringPrefixSize + len(z.ChecksumCRC32C) + 13 + msgp.StringPrefixSize + len(z.ChecksumSHA1) + 15 + msgp.StringPrefixSize + len(z.ChecksumSHA256) + 18 + msgp.StringPrefixSize + len(z.ChecksumCRC64NVME)
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *DeletedObjectInfo) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 5
+ // string "Bucket"
+ o = append(o, 0x85, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ o = msgp.AppendString(o, z.Bucket)
+ // string "Name"
+ o = append(o, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
+ o = msgp.AppendString(o, z.Name)
+ // string "ModTime"
+ o = append(o, 0xa7, 0x4d, 0x6f, 0x64, 0x54, 0x69, 0x6d, 0x65)
+ o = msgp.AppendTime(o, z.ModTime)
+ // string "VersionID"
+ o = append(o, 0xa9, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x44)
+ o = msgp.AppendString(o, z.VersionID)
+ // string "IsLatest"
+ o = append(o, 0xa8, 0x49, 0x73, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74)
+ o = msgp.AppendBool(o, z.IsLatest)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *DeletedObjectInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Bucket":
+ z.Bucket, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "Name":
+ z.Name, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Name")
+ return
+ }
+ case "ModTime":
+ z.ModTime, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ModTime")
+ return
+ }
+ case "VersionID":
+ z.VersionID, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "VersionID")
+ return
+ }
+ case "IsLatest":
+ z.IsLatest, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "IsLatest")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *DeletedObjectInfo) Msgsize() (s int) {
+ s = 1 + 7 + msgp.StringPrefixSize + len(z.Bucket) + 5 + msgp.StringPrefixSize + len(z.Name) + 8 + msgp.TimeSize + 10 + msgp.StringPrefixSize + len(z.VersionID) + 9 + msgp.BoolSize
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *ListMultipartsInfo) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 11
+ // string "KeyMarker"
+ o = append(o, 0x8b, 0xa9, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72)
+ o = msgp.AppendString(o, z.KeyMarker)
+ // string "UploadIDMarker"
+ o = append(o, 0xae, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x49, 0x44, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72)
+ o = msgp.AppendString(o, z.UploadIDMarker)
+ // string "NextKeyMarker"
+ o = append(o, 0xad, 0x4e, 0x65, 0x78, 0x74, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72)
+ o = msgp.AppendString(o, z.NextKeyMarker)
+ // string "NextUploadIDMarker"
+ o = append(o, 0xb2, 0x4e, 0x65, 0x78, 0x74, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x49, 0x44, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72)
+ o = msgp.AppendString(o, z.NextUploadIDMarker)
+ // string "MaxUploads"
+ o = append(o, 0xaa, 0x4d, 0x61, 0x78, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x73)
+ o = msgp.AppendInt(o, z.MaxUploads)
+ // string "IsTruncated"
+ o = append(o, 0xab, 0x49, 0x73, 0x54, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64)
+ o = msgp.AppendBool(o, z.IsTruncated)
+ // string "Uploads"
+ o = append(o, 0xa7, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.Uploads)))
+ for za0001 := range z.Uploads {
+ o, err = z.Uploads[za0001].MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Uploads", za0001)
+ return
+ }
+ }
+ // string "Prefix"
+ o = append(o, 0xa6, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78)
+ o = msgp.AppendString(o, z.Prefix)
+ // string "Delimiter"
+ o = append(o, 0xa9, 0x44, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x65, 0x72)
+ o = msgp.AppendString(o, z.Delimiter)
+ // string "CommonPrefixes"
+ o = append(o, 0xae, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.CommonPrefixes)))
+ for za0002 := range z.CommonPrefixes {
+ o = msgp.AppendString(o, z.CommonPrefixes[za0002])
+ }
+ // string "EncodingType"
+ o = append(o, 0xac, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65)
+ o = msgp.AppendString(o, z.EncodingType)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ListMultipartsInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "KeyMarker":
+ z.KeyMarker, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "KeyMarker")
+ return
+ }
+ case "UploadIDMarker":
+ z.UploadIDMarker, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "UploadIDMarker")
+ return
+ }
+ case "NextKeyMarker":
+ z.NextKeyMarker, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "NextKeyMarker")
+ return
+ }
+ case "NextUploadIDMarker":
+ z.NextUploadIDMarker, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "NextUploadIDMarker")
+ return
+ }
+ case "MaxUploads":
+ z.MaxUploads, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "MaxUploads")
+ return
+ }
+ case "IsTruncated":
+ z.IsTruncated, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "IsTruncated")
+ return
+ }
+ case "Uploads":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Uploads")
+ return
+ }
+ if cap(z.Uploads) >= int(zb0002) {
+ z.Uploads = (z.Uploads)[:zb0002]
+ } else {
+ z.Uploads = make([]MultipartInfo, zb0002)
+ }
+ for za0001 := range z.Uploads {
+ bts, err = z.Uploads[za0001].UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Uploads", za0001)
+ return
+ }
+ }
+ case "Prefix":
+ z.Prefix, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefix")
+ return
+ }
+ case "Delimiter":
+ z.Delimiter, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Delimiter")
+ return
+ }
+ case "CommonPrefixes":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "CommonPrefixes")
+ return
+ }
+ if cap(z.CommonPrefixes) >= int(zb0003) {
+ z.CommonPrefixes = (z.CommonPrefixes)[:zb0003]
+ } else {
+ z.CommonPrefixes = make([]string, zb0003)
+ }
+ for za0002 := range z.CommonPrefixes {
+ z.CommonPrefixes[za0002], bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "CommonPrefixes", za0002)
+ return
+ }
+ }
+ case "EncodingType":
+ z.EncodingType, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "EncodingType")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *ListMultipartsInfo) Msgsize() (s int) {
+ s = 1 + 10 + msgp.StringPrefixSize + len(z.KeyMarker) + 15 + msgp.StringPrefixSize + len(z.UploadIDMarker) + 14 + msgp.StringPrefixSize + len(z.NextKeyMarker) + 19 + msgp.StringPrefixSize + len(z.NextUploadIDMarker) + 11 + msgp.IntSize + 12 + msgp.BoolSize + 8 + msgp.ArrayHeaderSize
+ for za0001 := range z.Uploads {
+ s += z.Uploads[za0001].Msgsize()
+ }
+ s += 7 + msgp.StringPrefixSize + len(z.Prefix) + 10 + msgp.StringPrefixSize + len(z.Delimiter) + 15 + msgp.ArrayHeaderSize
+ for za0002 := range z.CommonPrefixes {
+ s += msgp.StringPrefixSize + len(z.CommonPrefixes[za0002])
+ }
+ s += 13 + msgp.StringPrefixSize + len(z.EncodingType)
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *ListObjectVersionsInfo) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 5
+ // string "IsTruncated"
+ o = append(o, 0x85, 0xab, 0x49, 0x73, 0x54, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64)
+ o = msgp.AppendBool(o, z.IsTruncated)
+ // string "NextMarker"
+ o = append(o, 0xaa, 0x4e, 0x65, 0x78, 0x74, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72)
+ o = msgp.AppendString(o, z.NextMarker)
+ // string "NextVersionIDMarker"
+ o = append(o, 0xb3, 0x4e, 0x65, 0x78, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72)
+ o = msgp.AppendString(o, z.NextVersionIDMarker)
+ // string "Objects"
+ o = append(o, 0xa7, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.Objects)))
+ for za0001 := range z.Objects {
+ o, err = z.Objects[za0001].MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Objects", za0001)
+ return
+ }
+ }
+ // string "Prefixes"
+ o = append(o, 0xa8, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.Prefixes)))
+ for za0002 := range z.Prefixes {
+ o = msgp.AppendString(o, z.Prefixes[za0002])
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ListObjectVersionsInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "IsTruncated":
+ z.IsTruncated, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "IsTruncated")
+ return
+ }
+ case "NextMarker":
+ z.NextMarker, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "NextMarker")
+ return
+ }
+ case "NextVersionIDMarker":
+ z.NextVersionIDMarker, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "NextVersionIDMarker")
+ return
+ }
+ case "Objects":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Objects")
+ return
+ }
+ if cap(z.Objects) >= int(zb0002) {
+ z.Objects = (z.Objects)[:zb0002]
+ } else {
+ z.Objects = make([]ObjectInfo, zb0002)
+ }
+ for za0001 := range z.Objects {
+ bts, err = z.Objects[za0001].UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Objects", za0001)
+ return
+ }
+ }
+ case "Prefixes":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefixes")
+ return
+ }
+ if cap(z.Prefixes) >= int(zb0003) {
+ z.Prefixes = (z.Prefixes)[:zb0003]
+ } else {
+ z.Prefixes = make([]string, zb0003)
+ }
+ for za0002 := range z.Prefixes {
+ z.Prefixes[za0002], bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefixes", za0002)
+ return
+ }
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *ListObjectVersionsInfo) Msgsize() (s int) {
+ s = 1 + 12 + msgp.BoolSize + 11 + msgp.StringPrefixSize + len(z.NextMarker) + 20 + msgp.StringPrefixSize + len(z.NextVersionIDMarker) + 8 + msgp.ArrayHeaderSize
+ for za0001 := range z.Objects {
+ s += z.Objects[za0001].Msgsize()
+ }
+ s += 9 + msgp.ArrayHeaderSize
+ for za0002 := range z.Prefixes {
+ s += msgp.StringPrefixSize + len(z.Prefixes[za0002])
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *ListObjectsInfo) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 4
+ // string "IsTruncated"
+ o = append(o, 0x84, 0xab, 0x49, 0x73, 0x54, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64)
+ o = msgp.AppendBool(o, z.IsTruncated)
+ // string "NextMarker"
+ o = append(o, 0xaa, 0x4e, 0x65, 0x78, 0x74, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72)
+ o = msgp.AppendString(o, z.NextMarker)
+ // string "Objects"
+ o = append(o, 0xa7, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.Objects)))
+ for za0001 := range z.Objects {
+ o, err = z.Objects[za0001].MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Objects", za0001)
+ return
+ }
+ }
+ // string "Prefixes"
+ o = append(o, 0xa8, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.Prefixes)))
+ for za0002 := range z.Prefixes {
+ o = msgp.AppendString(o, z.Prefixes[za0002])
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ListObjectsInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "IsTruncated":
+ z.IsTruncated, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "IsTruncated")
+ return
+ }
+ case "NextMarker":
+ z.NextMarker, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "NextMarker")
+ return
+ }
+ case "Objects":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Objects")
+ return
+ }
+ if cap(z.Objects) >= int(zb0002) {
+ z.Objects = (z.Objects)[:zb0002]
+ } else {
+ z.Objects = make([]ObjectInfo, zb0002)
+ }
+ for za0001 := range z.Objects {
+ bts, err = z.Objects[za0001].UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Objects", za0001)
+ return
+ }
+ }
+ case "Prefixes":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefixes")
+ return
+ }
+ if cap(z.Prefixes) >= int(zb0003) {
+ z.Prefixes = (z.Prefixes)[:zb0003]
+ } else {
+ z.Prefixes = make([]string, zb0003)
+ }
+ for za0002 := range z.Prefixes {
+ z.Prefixes[za0002], bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefixes", za0002)
+ return
+ }
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *ListObjectsInfo) Msgsize() (s int) {
+ s = 1 + 12 + msgp.BoolSize + 11 + msgp.StringPrefixSize + len(z.NextMarker) + 8 + msgp.ArrayHeaderSize
+ for za0001 := range z.Objects {
+ s += z.Objects[za0001].Msgsize()
+ }
+ s += 9 + msgp.ArrayHeaderSize
+ for za0002 := range z.Prefixes {
+ s += msgp.StringPrefixSize + len(z.Prefixes[za0002])
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *ListObjectsV2Info) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 5
+ // string "IsTruncated"
+ o = append(o, 0x85, 0xab, 0x49, 0x73, 0x54, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64)
+ o = msgp.AppendBool(o, z.IsTruncated)
+ // string "ContinuationToken"
+ o = append(o, 0xb1, 0x43, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e)
+ o = msgp.AppendString(o, z.ContinuationToken)
+ // string "NextContinuationToken"
+ o = append(o, 0xb5, 0x4e, 0x65, 0x78, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e)
+ o = msgp.AppendString(o, z.NextContinuationToken)
+ // string "Objects"
+ o = append(o, 0xa7, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.Objects)))
+ for za0001 := range z.Objects {
+ o, err = z.Objects[za0001].MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Objects", za0001)
+ return
+ }
+ }
+ // string "Prefixes"
+ o = append(o, 0xa8, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.Prefixes)))
+ for za0002 := range z.Prefixes {
+ o = msgp.AppendString(o, z.Prefixes[za0002])
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ListObjectsV2Info) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "IsTruncated":
+ z.IsTruncated, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "IsTruncated")
+ return
+ }
+ case "ContinuationToken":
+ z.ContinuationToken, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ContinuationToken")
+ return
+ }
+ case "NextContinuationToken":
+ z.NextContinuationToken, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "NextContinuationToken")
+ return
+ }
+ case "Objects":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Objects")
+ return
+ }
+ if cap(z.Objects) >= int(zb0002) {
+ z.Objects = (z.Objects)[:zb0002]
+ } else {
+ z.Objects = make([]ObjectInfo, zb0002)
+ }
+ for za0001 := range z.Objects {
+ bts, err = z.Objects[za0001].UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Objects", za0001)
+ return
+ }
+ }
+ case "Prefixes":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefixes")
+ return
+ }
+ if cap(z.Prefixes) >= int(zb0003) {
+ z.Prefixes = (z.Prefixes)[:zb0003]
+ } else {
+ z.Prefixes = make([]string, zb0003)
+ }
+ for za0002 := range z.Prefixes {
+ z.Prefixes[za0002], bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Prefixes", za0002)
+ return
+ }
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *ListObjectsV2Info) Msgsize() (s int) {
+ s = 1 + 12 + msgp.BoolSize + 18 + msgp.StringPrefixSize + len(z.ContinuationToken) + 22 + msgp.StringPrefixSize + len(z.NextContinuationToken) + 8 + msgp.ArrayHeaderSize
+ for za0001 := range z.Objects {
+ s += z.Objects[za0001].Msgsize()
+ }
+ s += 9 + msgp.ArrayHeaderSize
+ for za0002 := range z.Prefixes {
+ s += msgp.StringPrefixSize + len(z.Prefixes[za0002])
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *ListPartsInfo) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 12
+ // string "Bucket"
+ o = append(o, 0x8c, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ o = msgp.AppendString(o, z.Bucket)
+ // string "Object"
+ o = append(o, 0xa6, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74)
+ o = msgp.AppendString(o, z.Object)
+ // string "UploadID"
+ o = append(o, 0xa8, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x49, 0x44)
+ o = msgp.AppendString(o, z.UploadID)
+ // string "StorageClass"
+ o = append(o, 0xac, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73)
+ o = msgp.AppendString(o, z.StorageClass)
+ // string "PartNumberMarker"
+ o = append(o, 0xb0, 0x50, 0x61, 0x72, 0x74, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72)
+ o = msgp.AppendInt(o, z.PartNumberMarker)
+ // string "NextPartNumberMarker"
+ o = append(o, 0xb4, 0x4e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x72, 0x74, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72)
+ o = msgp.AppendInt(o, z.NextPartNumberMarker)
+ // string "MaxParts"
+ o = append(o, 0xa8, 0x4d, 0x61, 0x78, 0x50, 0x61, 0x72, 0x74, 0x73)
+ o = msgp.AppendInt(o, z.MaxParts)
+ // string "IsTruncated"
+ o = append(o, 0xab, 0x49, 0x73, 0x54, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64)
+ o = msgp.AppendBool(o, z.IsTruncated)
+ // string "Parts"
+ o = append(o, 0xa5, 0x50, 0x61, 0x72, 0x74, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.Parts)))
+ for za0001 := range z.Parts {
+ o, err = z.Parts[za0001].MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Parts", za0001)
+ return
+ }
+ }
+ // string "UserDefined"
+ o = append(o, 0xab, 0x55, 0x73, 0x65, 0x72, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64)
+ o = msgp.AppendMapHeader(o, uint32(len(z.UserDefined)))
+ for za0002, za0003 := range z.UserDefined {
+ o = msgp.AppendString(o, za0002)
+ o = msgp.AppendString(o, za0003)
+ }
+ // string "ChecksumAlgorithm"
+ o = append(o, 0xb1, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d)
+ o = msgp.AppendString(o, z.ChecksumAlgorithm)
+ // string "ChecksumType"
+ o = append(o, 0xac, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x54, 0x79, 0x70, 0x65)
+ o = msgp.AppendString(o, z.ChecksumType)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ListPartsInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Bucket":
+ z.Bucket, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "Object":
+ z.Object, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ case "UploadID":
+ z.UploadID, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "UploadID")
+ return
+ }
+ case "StorageClass":
+ z.StorageClass, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "StorageClass")
+ return
+ }
+ case "PartNumberMarker":
+ z.PartNumberMarker, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "PartNumberMarker")
+ return
+ }
+ case "NextPartNumberMarker":
+ z.NextPartNumberMarker, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "NextPartNumberMarker")
+ return
+ }
+ case "MaxParts":
+ z.MaxParts, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "MaxParts")
+ return
+ }
+ case "IsTruncated":
+ z.IsTruncated, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "IsTruncated")
+ return
+ }
+ case "Parts":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Parts")
+ return
+ }
+ if cap(z.Parts) >= int(zb0002) {
+ z.Parts = (z.Parts)[:zb0002]
+ } else {
+ z.Parts = make([]PartInfo, zb0002)
+ }
+ for za0001 := range z.Parts {
+ bts, err = z.Parts[za0001].UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Parts", za0001)
+ return
+ }
+ }
+ case "UserDefined":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "UserDefined")
+ return
+ }
+ if z.UserDefined == nil {
+ z.UserDefined = make(map[string]string, zb0003)
+ } else if len(z.UserDefined) > 0 {
+ clear(z.UserDefined)
+ }
+ for zb0003 > 0 {
+ var za0003 string
+ zb0003--
+ var za0002 string
+ za0002, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "UserDefined")
+ return
+ }
+ za0003, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "UserDefined", za0002)
+ return
+ }
+ z.UserDefined[za0002] = za0003
+ }
+ case "ChecksumAlgorithm":
+ z.ChecksumAlgorithm, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ChecksumAlgorithm")
+ return
+ }
+ case "ChecksumType":
+ z.ChecksumType, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ChecksumType")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *ListPartsInfo) Msgsize() (s int) {
+ s = 1 + 7 + msgp.StringPrefixSize + len(z.Bucket) + 7 + msgp.StringPrefixSize + len(z.Object) + 9 + msgp.StringPrefixSize + len(z.UploadID) + 13 + msgp.StringPrefixSize + len(z.StorageClass) + 17 + msgp.IntSize + 21 + msgp.IntSize + 9 + msgp.IntSize + 12 + msgp.BoolSize + 6 + msgp.ArrayHeaderSize
+ for za0001 := range z.Parts {
+ s += z.Parts[za0001].Msgsize()
+ }
+ s += 12 + msgp.MapHeaderSize
+ if z.UserDefined != nil {
+ for za0002, za0003 := range z.UserDefined {
+ _ = za0003
+ s += msgp.StringPrefixSize + len(za0002) + msgp.StringPrefixSize + len(za0003)
+ }
+ }
+ s += 18 + msgp.StringPrefixSize + len(z.ChecksumAlgorithm) + 13 + msgp.StringPrefixSize + len(z.ChecksumType)
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *MultipartInfo) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 5
+ // string "Bucket"
+ o = append(o, 0x85, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ o = msgp.AppendString(o, z.Bucket)
+ // string "Object"
+ o = append(o, 0xa6, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74)
+ o = msgp.AppendString(o, z.Object)
+ // string "UploadID"
+ o = append(o, 0xa8, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x49, 0x44)
+ o = msgp.AppendString(o, z.UploadID)
+ // string "Initiated"
+ o = append(o, 0xa9, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x65, 0x64)
+ o = msgp.AppendTime(o, z.Initiated)
+ // string "UserDefined"
+ o = append(o, 0xab, 0x55, 0x73, 0x65, 0x72, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64)
+ o = msgp.AppendMapHeader(o, uint32(len(z.UserDefined)))
+ for za0001, za0002 := range z.UserDefined {
+ o = msgp.AppendString(o, za0001)
+ o = msgp.AppendString(o, za0002)
+ }
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *MultipartInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Bucket":
+ z.Bucket, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "Object":
+ z.Object, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Object")
+ return
+ }
+ case "UploadID":
+ z.UploadID, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "UploadID")
+ return
+ }
+ case "Initiated":
+ z.Initiated, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Initiated")
+ return
+ }
+ case "UserDefined":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "UserDefined")
+ return
+ }
+ if z.UserDefined == nil {
+ z.UserDefined = make(map[string]string, zb0002)
+ } else if len(z.UserDefined) > 0 {
+ clear(z.UserDefined)
+ }
+ for zb0002 > 0 {
+ var za0002 string
+ zb0002--
+ var za0001 string
+ za0001, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "UserDefined")
+ return
+ }
+ za0002, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "UserDefined", za0001)
+ return
+ }
+ z.UserDefined[za0001] = za0002
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *MultipartInfo) Msgsize() (s int) {
+ s = 1 + 7 + msgp.StringPrefixSize + len(z.Bucket) + 7 + msgp.StringPrefixSize + len(z.Object) + 9 + msgp.StringPrefixSize + len(z.UploadID) + 10 + msgp.TimeSize + 12 + msgp.MapHeaderSize
+ if z.UserDefined != nil {
+ for za0001, za0002 := range z.UserDefined {
+ _ = za0002
+ s += msgp.StringPrefixSize + len(za0001) + msgp.StringPrefixSize + len(za0002)
+ }
+ }
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z NewMultipartUploadResult) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 3
+ // string "UploadID"
+ o = append(o, 0x83, 0xa8, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x49, 0x44)
+ o = msgp.AppendString(o, z.UploadID)
+ // string "ChecksumAlgo"
+ o = append(o, 0xac, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x41, 0x6c, 0x67, 0x6f)
+ o = msgp.AppendString(o, z.ChecksumAlgo)
+ // string "ChecksumType"
+ o = append(o, 0xac, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x54, 0x79, 0x70, 0x65)
+ o = msgp.AppendString(o, z.ChecksumType)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *NewMultipartUploadResult) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "UploadID":
+ z.UploadID, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "UploadID")
+ return
+ }
+ case "ChecksumAlgo":
+ z.ChecksumAlgo, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ChecksumAlgo")
+ return
+ }
+ case "ChecksumType":
+ z.ChecksumType, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ChecksumType")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z NewMultipartUploadResult) Msgsize() (s int) {
+ s = 1 + 9 + msgp.StringPrefixSize + len(z.UploadID) + 13 + msgp.StringPrefixSize + len(z.ChecksumAlgo) + 13 + msgp.StringPrefixSize + len(z.ChecksumType)
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *ObjectInfo) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 33
+ // string "Bucket"
+ o = append(o, 0xde, 0x0, 0x21, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ o = msgp.AppendString(o, z.Bucket)
+ // string "Name"
+ o = append(o, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
+ o = msgp.AppendString(o, z.Name)
+ // string "ModTime"
+ o = append(o, 0xa7, 0x4d, 0x6f, 0x64, 0x54, 0x69, 0x6d, 0x65)
+ o = msgp.AppendTime(o, z.ModTime)
+ // string "Size"
+ o = append(o, 0xa4, 0x53, 0x69, 0x7a, 0x65)
+ o = msgp.AppendInt64(o, z.Size)
+ // string "ActualSize"
+ o = append(o, 0xaa, 0x41, 0x63, 0x74, 0x75, 0x61, 0x6c, 0x53, 0x69, 0x7a, 0x65)
+ if z.ActualSize == nil {
+ o = msgp.AppendNil(o)
+ } else {
+ o = msgp.AppendInt64(o, *z.ActualSize)
+ }
+ // string "IsDir"
+ o = append(o, 0xa5, 0x49, 0x73, 0x44, 0x69, 0x72)
+ o = msgp.AppendBool(o, z.IsDir)
+ // string "ETag"
+ o = append(o, 0xa4, 0x45, 0x54, 0x61, 0x67)
+ o = msgp.AppendString(o, z.ETag)
+ // string "VersionID"
+ o = append(o, 0xa9, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x44)
+ o = msgp.AppendString(o, z.VersionID)
+ // string "IsLatest"
+ o = append(o, 0xa8, 0x49, 0x73, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74)
+ o = msgp.AppendBool(o, z.IsLatest)
+ // string "DeleteMarker"
+ o = append(o, 0xac, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72)
+ o = msgp.AppendBool(o, z.DeleteMarker)
+ // string "TransitionedObject"
+ o = append(o, 0xb2, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74)
+ o, err = z.TransitionedObject.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "TransitionedObject")
+ return
+ }
+ // string "RestoreExpires"
+ o = append(o, 0xae, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73)
+ o = msgp.AppendTime(o, z.RestoreExpires)
+ // string "RestoreOngoing"
+ o = append(o, 0xae, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x4f, 0x6e, 0x67, 0x6f, 0x69, 0x6e, 0x67)
+ o = msgp.AppendBool(o, z.RestoreOngoing)
+ // string "ContentType"
+ o = append(o, 0xab, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65)
+ o = msgp.AppendString(o, z.ContentType)
+ // string "ContentEncoding"
+ o = append(o, 0xaf, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67)
+ o = msgp.AppendString(o, z.ContentEncoding)
+ // string "Expires"
+ o = append(o, 0xa7, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73)
+ o = msgp.AppendTime(o, z.Expires)
+ // string "CacheControl"
+ o = append(o, 0xac, 0x43, 0x61, 0x63, 0x68, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c)
+ o = msgp.AppendString(o, z.CacheControl)
+ // string "StorageClass"
+ o = append(o, 0xac, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73)
+ o = msgp.AppendString(o, z.StorageClass)
+ // string "ReplicationStatusInternal"
+ o = append(o, 0xb9, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c)
+ o = msgp.AppendString(o, z.ReplicationStatusInternal)
+ // string "ReplicationStatus"
+ o = append(o, 0xb1, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73)
+ o, err = z.ReplicationStatus.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationStatus")
+ return
+ }
+ // string "UserDefined"
+ o = append(o, 0xab, 0x55, 0x73, 0x65, 0x72, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64)
+ o = msgp.AppendMapHeader(o, uint32(len(z.UserDefined)))
+ for za0001, za0002 := range z.UserDefined {
+ o = msgp.AppendString(o, za0001)
+ o = msgp.AppendString(o, za0002)
+ }
+ // string "UserTags"
+ o = append(o, 0xa8, 0x55, 0x73, 0x65, 0x72, 0x54, 0x61, 0x67, 0x73)
+ o = msgp.AppendString(o, z.UserTags)
+ // string "Parts"
+ o = append(o, 0xa5, 0x50, 0x61, 0x72, 0x74, 0x73)
+ o = msgp.AppendArrayHeader(o, uint32(len(z.Parts)))
+ for za0003 := range z.Parts {
+ o, err = z.Parts[za0003].MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Parts", za0003)
+ return
+ }
+ }
+ // string "AccTime"
+ o = append(o, 0xa7, 0x41, 0x63, 0x63, 0x54, 0x69, 0x6d, 0x65)
+ o = msgp.AppendTime(o, z.AccTime)
+ // string "Legacy"
+ o = append(o, 0xa6, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79)
+ o = msgp.AppendBool(o, z.Legacy)
+ // string "VersionPurgeStatusInternal"
+ o = append(o, 0xba, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x75, 0x72, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c)
+ o = msgp.AppendString(o, z.VersionPurgeStatusInternal)
+ // string "VersionPurgeStatus"
+ o = append(o, 0xb2, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x75, 0x72, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73)
+ o, err = z.VersionPurgeStatus.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "VersionPurgeStatus")
+ return
+ }
+ // string "NumVersions"
+ o = append(o, 0xab, 0x4e, 0x75, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73)
+ o = msgp.AppendInt(o, z.NumVersions)
+ // string "SuccessorModTime"
+ o = append(o, 0xb0, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x4d, 0x6f, 0x64, 0x54, 0x69, 0x6d, 0x65)
+ o = msgp.AppendTime(o, z.SuccessorModTime)
+ // string "Checksum"
+ o = append(o, 0xa8, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d)
+ o = msgp.AppendBytes(o, z.Checksum)
+ // string "Inlined"
+ o = append(o, 0xa7, 0x49, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x64)
+ o = msgp.AppendBool(o, z.Inlined)
+ // string "DataBlocks"
+ o = append(o, 0xaa, 0x44, 0x61, 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73)
+ o = msgp.AppendInt(o, z.DataBlocks)
+ // string "ParityBlocks"
+ o = append(o, 0xac, 0x50, 0x61, 0x72, 0x69, 0x74, 0x79, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73)
+ o = msgp.AppendInt(o, z.ParityBlocks)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ObjectInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Bucket":
+ z.Bucket, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "Name":
+ z.Name, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Name")
+ return
+ }
+ case "ModTime":
+ z.ModTime, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ModTime")
+ return
+ }
+ case "Size":
+ z.Size, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ case "ActualSize":
+ if msgp.IsNil(bts) {
+ bts, err = msgp.ReadNilBytes(bts)
+ if err != nil {
+ return
+ }
+ z.ActualSize = nil
+ } else {
+ if z.ActualSize == nil {
+ z.ActualSize = new(int64)
+ }
+ *z.ActualSize, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ActualSize")
+ return
+ }
+ }
+ case "IsDir":
+ z.IsDir, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "IsDir")
+ return
+ }
+ case "ETag":
+ z.ETag, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ETag")
+ return
+ }
+ case "VersionID":
+ z.VersionID, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "VersionID")
+ return
+ }
+ case "IsLatest":
+ z.IsLatest, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "IsLatest")
+ return
+ }
+ case "DeleteMarker":
+ z.DeleteMarker, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "DeleteMarker")
+ return
+ }
+ case "TransitionedObject":
+ bts, err = z.TransitionedObject.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "TransitionedObject")
+ return
+ }
+ case "RestoreExpires":
+ z.RestoreExpires, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "RestoreExpires")
+ return
+ }
+ case "RestoreOngoing":
+ z.RestoreOngoing, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "RestoreOngoing")
+ return
+ }
+ case "ContentType":
+ z.ContentType, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ContentType")
+ return
+ }
+ case "ContentEncoding":
+ z.ContentEncoding, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ContentEncoding")
+ return
+ }
+ case "Expires":
+ z.Expires, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Expires")
+ return
+ }
+ case "CacheControl":
+ z.CacheControl, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "CacheControl")
+ return
+ }
+ case "StorageClass":
+ z.StorageClass, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "StorageClass")
+ return
+ }
+ case "ReplicationStatusInternal":
+ z.ReplicationStatusInternal, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationStatusInternal")
+ return
+ }
+ case "ReplicationStatus":
+ bts, err = z.ReplicationStatus.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationStatus")
+ return
+ }
+ case "UserDefined":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "UserDefined")
+ return
+ }
+ if z.UserDefined == nil {
+ z.UserDefined = make(map[string]string, zb0002)
+ } else if len(z.UserDefined) > 0 {
+ clear(z.UserDefined)
+ }
+ for zb0002 > 0 {
+ var za0002 string
+ zb0002--
+ var za0001 string
+ za0001, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "UserDefined")
+ return
+ }
+ za0002, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "UserDefined", za0001)
+ return
+ }
+ z.UserDefined[za0001] = za0002
+ }
+ case "UserTags":
+ z.UserTags, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "UserTags")
+ return
+ }
+ case "Parts":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Parts")
+ return
+ }
+ if cap(z.Parts) >= int(zb0003) {
+ z.Parts = (z.Parts)[:zb0003]
+ } else {
+ z.Parts = make([]ObjectPartInfo, zb0003)
+ }
+ for za0003 := range z.Parts {
+ bts, err = z.Parts[za0003].UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Parts", za0003)
+ return
+ }
+ }
+ case "AccTime":
+ z.AccTime, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AccTime")
+ return
+ }
+ case "Legacy":
+ z.Legacy, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Legacy")
+ return
+ }
+ case "VersionPurgeStatusInternal":
+ z.VersionPurgeStatusInternal, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "VersionPurgeStatusInternal")
+ return
+ }
+ case "VersionPurgeStatus":
+ bts, err = z.VersionPurgeStatus.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "VersionPurgeStatus")
+ return
+ }
+ case "NumVersions":
+ z.NumVersions, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "NumVersions")
+ return
+ }
+ case "SuccessorModTime":
+ z.SuccessorModTime, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "SuccessorModTime")
+ return
+ }
+ case "Checksum":
+ z.Checksum, bts, err = msgp.ReadBytesBytes(bts, z.Checksum)
+ if err != nil {
+ err = msgp.WrapError(err, "Checksum")
+ return
+ }
+ case "Inlined":
+ z.Inlined, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Inlined")
+ return
+ }
+ case "DataBlocks":
+ z.DataBlocks, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "DataBlocks")
+ return
+ }
+ case "ParityBlocks":
+ z.ParityBlocks, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ParityBlocks")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *ObjectInfo) Msgsize() (s int) {
+ s = 3 + 7 + msgp.StringPrefixSize + len(z.Bucket) + 5 + msgp.StringPrefixSize + len(z.Name) + 8 + msgp.TimeSize + 5 + msgp.Int64Size + 11
+ if z.ActualSize == nil {
+ s += msgp.NilSize
+ } else {
+ s += msgp.Int64Size
+ }
+ s += 6 + msgp.BoolSize + 5 + msgp.StringPrefixSize + len(z.ETag) + 10 + msgp.StringPrefixSize + len(z.VersionID) + 9 + msgp.BoolSize + 13 + msgp.BoolSize + 19 + z.TransitionedObject.Msgsize() + 15 + msgp.TimeSize + 15 + msgp.BoolSize + 12 + msgp.StringPrefixSize + len(z.ContentType) + 16 + msgp.StringPrefixSize + len(z.ContentEncoding) + 8 + msgp.TimeSize + 13 + msgp.StringPrefixSize + len(z.CacheControl) + 13 + msgp.StringPrefixSize + len(z.StorageClass) + 26 + msgp.StringPrefixSize + len(z.ReplicationStatusInternal) + 18 + z.ReplicationStatus.Msgsize() + 12 + msgp.MapHeaderSize
+ if z.UserDefined != nil {
+ for za0001, za0002 := range z.UserDefined {
+ _ = za0002
+ s += msgp.StringPrefixSize + len(za0001) + msgp.StringPrefixSize + len(za0002)
+ }
+ }
+ s += 9 + msgp.StringPrefixSize + len(z.UserTags) + 6 + msgp.ArrayHeaderSize
+ for za0003 := range z.Parts {
+ s += z.Parts[za0003].Msgsize()
+ }
+ s += 8 + msgp.TimeSize + 7 + msgp.BoolSize + 27 + msgp.StringPrefixSize + len(z.VersionPurgeStatusInternal) + 19 + z.VersionPurgeStatus.Msgsize() + 12 + msgp.IntSize + 17 + msgp.TimeSize + 9 + msgp.BytesPrefixSize + len(z.Checksum) + 8 + msgp.BoolSize + 11 + msgp.IntSize + 13 + msgp.IntSize
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *PartInfo) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 10
+ // string "PartNumber"
+ o = append(o, 0x8a, 0xaa, 0x50, 0x61, 0x72, 0x74, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72)
+ o = msgp.AppendInt(o, z.PartNumber)
+ // string "LastModified"
+ o = append(o, 0xac, 0x4c, 0x61, 0x73, 0x74, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64)
+ o = msgp.AppendTime(o, z.LastModified)
+ // string "ETag"
+ o = append(o, 0xa4, 0x45, 0x54, 0x61, 0x67)
+ o = msgp.AppendString(o, z.ETag)
+ // string "Size"
+ o = append(o, 0xa4, 0x53, 0x69, 0x7a, 0x65)
+ o = msgp.AppendInt64(o, z.Size)
+ // string "ActualSize"
+ o = append(o, 0xaa, 0x41, 0x63, 0x74, 0x75, 0x61, 0x6c, 0x53, 0x69, 0x7a, 0x65)
+ o = msgp.AppendInt64(o, z.ActualSize)
+ // string "ChecksumCRC32"
+ o = append(o, 0xad, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x43, 0x52, 0x43, 0x33, 0x32)
+ o = msgp.AppendString(o, z.ChecksumCRC32)
+ // string "ChecksumCRC32C"
+ o = append(o, 0xae, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x43, 0x52, 0x43, 0x33, 0x32, 0x43)
+ o = msgp.AppendString(o, z.ChecksumCRC32C)
+ // string "ChecksumSHA1"
+ o = append(o, 0xac, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x53, 0x48, 0x41, 0x31)
+ o = msgp.AppendString(o, z.ChecksumSHA1)
+ // string "ChecksumSHA256"
+ o = append(o, 0xae, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x53, 0x48, 0x41, 0x32, 0x35, 0x36)
+ o = msgp.AppendString(o, z.ChecksumSHA256)
+ // string "ChecksumCRC64NVME"
+ o = append(o, 0xb1, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x43, 0x52, 0x43, 0x36, 0x34, 0x4e, 0x56, 0x4d, 0x45)
+ o = msgp.AppendString(o, z.ChecksumCRC64NVME)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *PartInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "PartNumber":
+ z.PartNumber, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "PartNumber")
+ return
+ }
+ case "LastModified":
+ z.LastModified, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "LastModified")
+ return
+ }
+ case "ETag":
+ z.ETag, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ETag")
+ return
+ }
+ case "Size":
+ z.Size, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ case "ActualSize":
+ z.ActualSize, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ActualSize")
+ return
+ }
+ case "ChecksumCRC32":
+ z.ChecksumCRC32, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ChecksumCRC32")
+ return
+ }
+ case "ChecksumCRC32C":
+ z.ChecksumCRC32C, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ChecksumCRC32C")
+ return
+ }
+ case "ChecksumSHA1":
+ z.ChecksumSHA1, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ChecksumSHA1")
+ return
+ }
+ case "ChecksumSHA256":
+ z.ChecksumSHA256, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ChecksumSHA256")
+ return
+ }
+ case "ChecksumCRC64NVME":
+ z.ChecksumCRC64NVME, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ChecksumCRC64NVME")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *PartInfo) Msgsize() (s int) {
+ s = 1 + 11 + msgp.IntSize + 13 + msgp.TimeSize + 5 + msgp.StringPrefixSize + len(z.ETag) + 5 + msgp.Int64Size + 11 + msgp.Int64Size + 14 + msgp.StringPrefixSize + len(z.ChecksumCRC32) + 15 + msgp.StringPrefixSize + len(z.ChecksumCRC32C) + 13 + msgp.StringPrefixSize + len(z.ChecksumSHA1) + 15 + msgp.StringPrefixSize + len(z.ChecksumSHA256) + 18 + msgp.StringPrefixSize + len(z.ChecksumCRC64NVME)
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *ReplicateObjectInfo) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 26
+ // string "Name"
+ o = append(o, 0xde, 0x0, 0x1a, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
+ o = msgp.AppendString(o, z.Name)
+ // string "Bucket"
+ o = append(o, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
+ o = msgp.AppendString(o, z.Bucket)
+ // string "VersionID"
+ o = append(o, 0xa9, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x44)
+ o = msgp.AppendString(o, z.VersionID)
+ // string "ETag"
+ o = append(o, 0xa4, 0x45, 0x54, 0x61, 0x67)
+ o = msgp.AppendString(o, z.ETag)
+ // string "Size"
+ o = append(o, 0xa4, 0x53, 0x69, 0x7a, 0x65)
+ o = msgp.AppendInt64(o, z.Size)
+ // string "ActualSize"
+ o = append(o, 0xaa, 0x41, 0x63, 0x74, 0x75, 0x61, 0x6c, 0x53, 0x69, 0x7a, 0x65)
+ o = msgp.AppendInt64(o, z.ActualSize)
+ // string "ModTime"
+ o = append(o, 0xa7, 0x4d, 0x6f, 0x64, 0x54, 0x69, 0x6d, 0x65)
+ o = msgp.AppendTime(o, z.ModTime)
+ // string "UserTags"
+ o = append(o, 0xa8, 0x55, 0x73, 0x65, 0x72, 0x54, 0x61, 0x67, 0x73)
+ o = msgp.AppendString(o, z.UserTags)
+ // string "SSEC"
+ o = append(o, 0xa4, 0x53, 0x53, 0x45, 0x43)
+ o = msgp.AppendBool(o, z.SSEC)
+ // string "ReplicationStatus"
+ o = append(o, 0xb1, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73)
+ o, err = z.ReplicationStatus.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationStatus")
+ return
+ }
+ // string "ReplicationStatusInternal"
+ o = append(o, 0xb9, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c)
+ o = msgp.AppendString(o, z.ReplicationStatusInternal)
+ // string "VersionPurgeStatusInternal"
+ o = append(o, 0xba, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x75, 0x72, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c)
+ o = msgp.AppendString(o, z.VersionPurgeStatusInternal)
+ // string "VersionPurgeStatus"
+ o = append(o, 0xb2, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x75, 0x72, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73)
+ o, err = z.VersionPurgeStatus.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "VersionPurgeStatus")
+ return
+ }
+ // string "ReplicationState"
+ o = append(o, 0xb0, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65)
+ o, err = z.ReplicationState.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationState")
+ return
+ }
+ // string "DeleteMarker"
+ o = append(o, 0xac, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72)
+ o = msgp.AppendBool(o, z.DeleteMarker)
+ // string "OpType"
+ o = append(o, 0xa6, 0x4f, 0x70, 0x54, 0x79, 0x70, 0x65)
+ o, err = z.OpType.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "OpType")
+ return
+ }
+ // string "EventType"
+ o = append(o, 0xa9, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65)
+ o = msgp.AppendString(o, z.EventType)
+ // string "RetryCount"
+ o = append(o, 0xaa, 0x52, 0x65, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x75, 0x6e, 0x74)
+ o = msgp.AppendUint32(o, z.RetryCount)
+ // string "ResetID"
+ o = append(o, 0xa7, 0x52, 0x65, 0x73, 0x65, 0x74, 0x49, 0x44)
+ o = msgp.AppendString(o, z.ResetID)
+ // string "Dsc"
+ o = append(o, 0xa3, 0x44, 0x73, 0x63)
+ o, err = z.Dsc.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "Dsc")
+ return
+ }
+ // string "ExistingObjResync"
+ o = append(o, 0xb1, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x4f, 0x62, 0x6a, 0x52, 0x65, 0x73, 0x79, 0x6e, 0x63)
+ o, err = z.ExistingObjResync.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "ExistingObjResync")
+ return
+ }
+ // string "TargetArn"
+ o = append(o, 0xa9, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x41, 0x72, 0x6e)
+ o = msgp.AppendString(o, z.TargetArn)
+ // string "TargetStatuses"
+ o = append(o, 0xae, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73)
+ o = msgp.AppendMapHeader(o, uint32(len(z.TargetStatuses)))
+ for za0001, za0002 := range z.TargetStatuses {
+ o = msgp.AppendString(o, za0001)
+ o, err = za0002.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "TargetStatuses", za0001)
+ return
+ }
+ }
+ // string "TargetPurgeStatuses"
+ o = append(o, 0xb3, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x50, 0x75, 0x72, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73)
+ o = msgp.AppendMapHeader(o, uint32(len(z.TargetPurgeStatuses)))
+ for za0003, za0004 := range z.TargetPurgeStatuses {
+ o = msgp.AppendString(o, za0003)
+ o, err = za0004.MarshalMsg(o)
+ if err != nil {
+ err = msgp.WrapError(err, "TargetPurgeStatuses", za0003)
+ return
+ }
+ }
+ // string "ReplicationTimestamp"
+ o = append(o, 0xb4, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70)
+ o = msgp.AppendTime(o, z.ReplicationTimestamp)
+ // string "Checksum"
+ o = append(o, 0xa8, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d)
+ o = msgp.AppendBytes(o, z.Checksum)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ReplicateObjectInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Name":
+ z.Name, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Name")
+ return
+ }
+ case "Bucket":
+ z.Bucket, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Bucket")
+ return
+ }
+ case "VersionID":
+ z.VersionID, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "VersionID")
+ return
+ }
+ case "ETag":
+ z.ETag, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ETag")
+ return
+ }
+ case "Size":
+ z.Size, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Size")
+ return
+ }
+ case "ActualSize":
+ z.ActualSize, bts, err = msgp.ReadInt64Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ActualSize")
+ return
+ }
+ case "ModTime":
+ z.ModTime, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ModTime")
+ return
+ }
+ case "UserTags":
+ z.UserTags, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "UserTags")
+ return
+ }
+ case "SSEC":
+ z.SSEC, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "SSEC")
+ return
+ }
+ case "ReplicationStatus":
+ bts, err = z.ReplicationStatus.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationStatus")
+ return
+ }
+ case "ReplicationStatusInternal":
+ z.ReplicationStatusInternal, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationStatusInternal")
+ return
+ }
+ case "VersionPurgeStatusInternal":
+ z.VersionPurgeStatusInternal, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "VersionPurgeStatusInternal")
+ return
+ }
+ case "VersionPurgeStatus":
+ bts, err = z.VersionPurgeStatus.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "VersionPurgeStatus")
+ return
+ }
+ case "ReplicationState":
+ bts, err = z.ReplicationState.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationState")
+ return
+ }
+ case "DeleteMarker":
+ z.DeleteMarker, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "DeleteMarker")
+ return
+ }
+ case "OpType":
+ bts, err = z.OpType.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "OpType")
+ return
+ }
+ case "EventType":
+ z.EventType, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "EventType")
+ return
+ }
+ case "RetryCount":
+ z.RetryCount, bts, err = msgp.ReadUint32Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "RetryCount")
+ return
+ }
+ case "ResetID":
+ z.ResetID, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ResetID")
+ return
+ }
+ case "Dsc":
+ bts, err = z.Dsc.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Dsc")
+ return
+ }
+ case "ExistingObjResync":
+ bts, err = z.ExistingObjResync.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ExistingObjResync")
+ return
+ }
+ case "TargetArn":
+ z.TargetArn, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "TargetArn")
+ return
+ }
+ case "TargetStatuses":
+ var zb0002 uint32
+ zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "TargetStatuses")
+ return
+ }
+ if z.TargetStatuses == nil {
+ z.TargetStatuses = make(map[string]replication.StatusType, zb0002)
+ } else if len(z.TargetStatuses) > 0 {
+ clear(z.TargetStatuses)
+ }
+ for zb0002 > 0 {
+ var za0002 replication.StatusType
+ zb0002--
+ var za0001 string
+ za0001, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "TargetStatuses")
+ return
+ }
+ bts, err = za0002.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "TargetStatuses", za0001)
+ return
+ }
+ z.TargetStatuses[za0001] = za0002
+ }
+ case "TargetPurgeStatuses":
+ var zb0003 uint32
+ zb0003, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "TargetPurgeStatuses")
+ return
+ }
+ if z.TargetPurgeStatuses == nil {
+ z.TargetPurgeStatuses = make(map[string]VersionPurgeStatusType, zb0003)
+ } else if len(z.TargetPurgeStatuses) > 0 {
+ clear(z.TargetPurgeStatuses)
+ }
+ for zb0003 > 0 {
+ var za0004 VersionPurgeStatusType
+ zb0003--
+ var za0003 string
+ za0003, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "TargetPurgeStatuses")
+ return
+ }
+ bts, err = za0004.UnmarshalMsg(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "TargetPurgeStatuses", za0003)
+ return
+ }
+ z.TargetPurgeStatuses[za0003] = za0004
+ }
+ case "ReplicationTimestamp":
+ z.ReplicationTimestamp, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ReplicationTimestamp")
+ return
+ }
+ case "Checksum":
+ z.Checksum, bts, err = msgp.ReadBytesBytes(bts, z.Checksum)
+ if err != nil {
+ err = msgp.WrapError(err, "Checksum")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *ReplicateObjectInfo) Msgsize() (s int) {
+ s = 3 + 5 + msgp.StringPrefixSize + len(z.Name) + 7 + msgp.StringPrefixSize + len(z.Bucket) + 10 + msgp.StringPrefixSize + len(z.VersionID) + 5 + msgp.StringPrefixSize + len(z.ETag) + 5 + msgp.Int64Size + 11 + msgp.Int64Size + 8 + msgp.TimeSize + 9 + msgp.StringPrefixSize + len(z.UserTags) + 5 + msgp.BoolSize + 18 + z.ReplicationStatus.Msgsize() + 26 + msgp.StringPrefixSize + len(z.ReplicationStatusInternal) + 27 + msgp.StringPrefixSize + len(z.VersionPurgeStatusInternal) + 19 + z.VersionPurgeStatus.Msgsize() + 17 + z.ReplicationState.Msgsize() + 13 + msgp.BoolSize + 7 + z.OpType.Msgsize() + 10 + msgp.StringPrefixSize + len(z.EventType) + 11 + msgp.Uint32Size + 8 + msgp.StringPrefixSize + len(z.ResetID) + 4 + z.Dsc.Msgsize() + 18 + z.ExistingObjResync.Msgsize() + 10 + msgp.StringPrefixSize + len(z.TargetArn) + 15 + msgp.MapHeaderSize
+ if z.TargetStatuses != nil {
+ for za0001, za0002 := range z.TargetStatuses {
+ _ = za0002
+ s += msgp.StringPrefixSize + len(za0001) + za0002.Msgsize()
+ }
+ }
+ s += 20 + msgp.MapHeaderSize
+ if z.TargetPurgeStatuses != nil {
+ for za0003, za0004 := range z.TargetPurgeStatuses {
+ _ = za0004
+ s += msgp.StringPrefixSize + len(za0003) + za0004.Msgsize()
+ }
+ }
+ s += 21 + msgp.TimeSize + 9 + msgp.BytesPrefixSize + len(z.Checksum)
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *TransitionedObject) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 5
+ // string "Name"
+ o = append(o, 0x85, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
+ o = msgp.AppendString(o, z.Name)
+ // string "VersionID"
+ o = append(o, 0xa9, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x44)
+ o = msgp.AppendString(o, z.VersionID)
+ // string "Tier"
+ o = append(o, 0xa4, 0x54, 0x69, 0x65, 0x72)
+ o = msgp.AppendString(o, z.Tier)
+ // string "FreeVersion"
+ o = append(o, 0xab, 0x46, 0x72, 0x65, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e)
+ o = msgp.AppendBool(o, z.FreeVersion)
+ // string "Status"
+ o = append(o, 0xa6, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73)
+ o = msgp.AppendString(o, z.Status)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *TransitionedObject) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Name":
+ z.Name, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Name")
+ return
+ }
+ case "VersionID":
+ z.VersionID, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "VersionID")
+ return
+ }
+ case "Tier":
+ z.Tier, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Tier")
+ return
+ }
+ case "FreeVersion":
+ z.FreeVersion, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "FreeVersion")
+ return
+ }
+ case "Status":
+ z.Status, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Status")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *TransitionedObject) Msgsize() (s int) {
+ s = 1 + 5 + msgp.StringPrefixSize + len(z.Name) + 10 + msgp.StringPrefixSize + len(z.VersionID) + 5 + msgp.StringPrefixSize + len(z.Tier) + 12 + msgp.BoolSize + 7 + msgp.StringPrefixSize + len(z.Status)
+ return
+}
diff --git a/cmd/object-api-deleteobject_test.go b/cmd/object-api-deleteobject_test.go
new file mode 100644
index 00000000..b7e888e4
--- /dev/null
+++ b/cmd/object-api-deleteobject_test.go
@@ -0,0 +1,126 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "crypto/md5"
+ "encoding/hex"
+ "strings"
+ "testing"
+)
+
+// Wrapper for calling DeleteObject tests for both Erasure multiple disks and single node setup.
+func TestDeleteObject(t *testing.T) {
+ ExecObjectLayerTest(t, testDeleteObject)
+}
+
+// Unit test for DeleteObject in general.
+func testDeleteObject(obj ObjectLayer, instanceType string, t TestErrHandler) {
+ type objectUpload struct {
+ name string
+ content string
+ }
+
+ testCases := []struct {
+ bucketName string
+ objectToUploads []objectUpload
+ pathToDelete string
+ objectsAfterDelete []string
+ }{
+ // Test 1: removes an object and checks it is the only object
+ // that has been deleted.
+ {
+ "bucket1",
+ []objectUpload{{"object0", "content"}, {"object1", "content"}},
+ "object0",
+ []string{"object1"},
+ },
+ // Test 2: remove an object inside a directory and checks it is deleted
+ // with its parent since this former becomes empty
+ {
+ "bucket2",
+ []objectUpload{{"object0", "content"}, {"dir/object1", "content"}},
+ "dir/object1",
+ []string{"object0"},
+ },
+ // Test 3: remove an object inside a directory and checks if it is deleted
+ // but other sibling object in the same directory still exists
+ {
+ "bucket3",
+ []objectUpload{{"dir/object1", "content"}, {"dir/object2", "content"}},
+ "dir/object1",
+ []string{"dir/object2"},
+ },
+ // Test 4: remove a non empty directory and checks it has no effect
+ {
+ "bucket4",
+ []objectUpload{{"object0", "content"}, {"dir/object1", "content"}},
+ "dir/",
+ []string{"dir/object1", "object0"},
+ },
+ // Test 5: Remove an empty directory and checks it is really removed
+ {
+ "bucket5",
+ []objectUpload{{"object0", "content"}, {"dir/", ""}},
+ "dir/",
+ []string{"object0"},
+ },
+ }
+
+ for i, testCase := range testCases {
+ err := obj.MakeBucket(context.Background(), testCase.bucketName, MakeBucketOptions{})
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ for _, object := range testCase.objectToUploads {
+ md5Bytes := md5.Sum([]byte(object.content))
+ oi, err := obj.PutObject(context.Background(), testCase.bucketName, object.name, mustGetPutObjReader(t, strings.NewReader(object.content),
+ int64(len(object.content)), hex.EncodeToString(md5Bytes[:]), ""), ObjectOptions{})
+ if err != nil {
+ t.Log(oi)
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ }
+
+ oi, err := obj.DeleteObject(context.Background(), testCase.bucketName, testCase.pathToDelete, ObjectOptions{})
+ if err != nil && !isErrObjectNotFound(err) {
+ t.Log(oi)
+ t.Errorf("Test %d: %s: Expected to pass, but failed with: %s", i+1, instanceType, err)
+ continue
+ }
+
+ result, err := obj.ListObjects(context.Background(), testCase.bucketName, "", "", "", 1000)
+ if err != nil {
+ t.Errorf("Test %d: %s: Expected to pass, but failed with: %s", i+1, instanceType, err.Error())
+ continue
+ }
+
+ if len(result.Objects) != len(testCase.objectsAfterDelete) {
+ t.Errorf("Test %d: %s: mismatch number of objects after delete, expected = %v, found = %v", i+1, instanceType, testCase.objectsAfterDelete, result.Objects)
+ continue
+ }
+
+ for idx := range result.Objects {
+ if result.Objects[idx].Name != testCase.objectsAfterDelete[idx] {
+ t.Errorf("Test %d: %s: Unexpected object found after delete, found = `%v`", i+1, instanceType, result.Objects[idx].Name)
+ }
+ }
+ }
+}
diff --git a/cmd/object-api-errors.go b/cmd/object-api-errors.go
new file mode 100644
index 00000000..ef372393
--- /dev/null
+++ b/cmd/object-api-errors.go
@@ -0,0 +1,807 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+)
+
+// Converts underlying storage error. Convenience function written to
+// handle all cases where we have known types of errors returned by
+// underlying storage layer.
+func toObjectErr(oerr error, params ...string) error {
+ if oerr == nil {
+ return nil
+ }
+
+ // Unwarp the error first
+ err := unwrapAll(oerr)
+
+ if err == context.Canceled {
+ return context.Canceled
+ }
+
+ switch err.Error() {
+ case errVolumeNotFound.Error():
+ apiErr := BucketNotFound{}
+ if len(params) >= 1 {
+ apiErr.Bucket = params[0]
+ }
+ return apiErr
+ case errVolumeNotEmpty.Error():
+ apiErr := BucketNotEmpty{}
+ if len(params) >= 1 {
+ apiErr.Bucket = params[0]
+ }
+ return apiErr
+ case errVolumeExists.Error():
+ apiErr := BucketExists{}
+ if len(params) >= 1 {
+ apiErr.Bucket = params[0]
+ }
+ return apiErr
+ case errDiskFull.Error():
+ return StorageFull{}
+ case errTooManyOpenFiles.Error():
+ return SlowDown{}
+ case errFileAccessDenied.Error():
+ apiErr := PrefixAccessDenied{}
+ if len(params) >= 1 {
+ apiErr.Bucket = params[0]
+ }
+ if len(params) >= 2 {
+ apiErr.Object = decodeDirObject(params[1])
+ }
+ return apiErr
+ case errIsNotRegular.Error():
+ apiErr := ObjectExistsAsDirectory{}
+ if len(params) >= 1 {
+ apiErr.Bucket = params[0]
+ }
+ if len(params) >= 2 {
+ apiErr.Object = decodeDirObject(params[1])
+ }
+ return apiErr
+ case errFileVersionNotFound.Error():
+ apiErr := VersionNotFound{}
+ if len(params) >= 1 {
+ apiErr.Bucket = params[0]
+ }
+ if len(params) >= 2 {
+ apiErr.Object = decodeDirObject(params[1])
+ }
+ if len(params) >= 3 {
+ apiErr.VersionID = params[2]
+ }
+ return apiErr
+ case errMethodNotAllowed.Error():
+ apiErr := MethodNotAllowed{}
+ if len(params) >= 1 {
+ apiErr.Bucket = params[0]
+ }
+ if len(params) >= 2 {
+ apiErr.Object = decodeDirObject(params[1])
+ }
+ return apiErr
+ case errFileNotFound.Error():
+ apiErr := ObjectNotFound{}
+ if len(params) >= 1 {
+ apiErr.Bucket = params[0]
+ }
+ if len(params) >= 2 {
+ apiErr.Object = decodeDirObject(params[1])
+ }
+ return apiErr
+ case errUploadIDNotFound.Error():
+ apiErr := InvalidUploadID{}
+ if len(params) >= 1 {
+ apiErr.Bucket = params[0]
+ }
+ if len(params) >= 2 {
+ apiErr.Object = decodeDirObject(params[1])
+ }
+ if len(params) >= 3 {
+ apiErr.UploadID = params[2]
+ }
+ return apiErr
+ case errFileNameTooLong.Error():
+ apiErr := ObjectNameInvalid{}
+ if len(params) >= 1 {
+ apiErr.Bucket = params[0]
+ }
+ if len(params) >= 2 {
+ apiErr.Object = decodeDirObject(params[1])
+ }
+ return apiErr
+ case errDataTooLarge.Error():
+ apiErr := ObjectTooLarge{}
+ if len(params) >= 1 {
+ apiErr.Bucket = params[0]
+ }
+ if len(params) >= 2 {
+ apiErr.Object = decodeDirObject(params[1])
+ }
+ return apiErr
+ case errDataTooSmall.Error():
+ apiErr := ObjectTooSmall{}
+ if len(params) >= 1 {
+ apiErr.Bucket = params[0]
+ }
+ if len(params) >= 2 {
+ apiErr.Object = decodeDirObject(params[1])
+ }
+ return apiErr
+ case errErasureReadQuorum.Error():
+ apiErr := InsufficientReadQuorum{}
+ if len(params) >= 1 {
+ apiErr.Bucket = params[0]
+ }
+ if len(params) >= 2 {
+ apiErr.Object = decodeDirObject(params[1])
+ }
+ if v, ok := oerr.(InsufficientReadQuorum); ok {
+ apiErr.Type = v.Type
+ }
+ return apiErr
+ case errErasureWriteQuorum.Error():
+ apiErr := InsufficientWriteQuorum{}
+ if len(params) >= 1 {
+ apiErr.Bucket = params[0]
+ }
+ if len(params) >= 2 {
+ apiErr.Object = decodeDirObject(params[1])
+ }
+ return apiErr
+ case io.ErrUnexpectedEOF.Error(), io.ErrShortWrite.Error(), context.Canceled.Error(), context.DeadlineExceeded.Error():
+ apiErr := IncompleteBody{}
+ if len(params) >= 1 {
+ apiErr.Bucket = params[0]
+ }
+ if len(params) >= 2 {
+ apiErr.Object = decodeDirObject(params[1])
+ }
+ return apiErr
+ }
+ return err
+}
+
+// SignatureDoesNotMatch - when content md5 does not match with what was sent from client.
+type SignatureDoesNotMatch struct{}
+
+func (e SignatureDoesNotMatch) Error() string {
+ return "The request signature we calculated does not match the signature you provided. Check your key and signing method."
+}
+
+// StorageFull storage ran out of space.
+type StorageFull struct{}
+
+func (e StorageFull) Error() string {
+ return "Storage reached its minimum free drive threshold."
+}
+
+// SlowDown too many file descriptors open or backend busy .
+type SlowDown struct{}
+
+func (e SlowDown) Error() string {
+ return "Please reduce your request rate"
+}
+
+// RQErrType reason for read quorum error.
+type RQErrType int
+
+const (
+ // RQInsufficientOnlineDrives - not enough online drives.
+ RQInsufficientOnlineDrives RQErrType = 1 << iota
+ // RQInconsistentMeta - inconsistent metadata.
+ RQInconsistentMeta
+)
+
+func (t RQErrType) String() string {
+ switch t {
+ case RQInsufficientOnlineDrives:
+ return "InsufficientOnlineDrives"
+ case RQInconsistentMeta:
+ return "InconsistentMeta"
+ default:
+ return "Unknown"
+ }
+}
+
+// InsufficientReadQuorum storage cannot satisfy quorum for read operation.
+type InsufficientReadQuorum struct {
+ Bucket string
+ Object string
+ Err error
+ Type RQErrType
+}
+
+func (e InsufficientReadQuorum) Error() string {
+ return "Storage resources are insufficient for the read operation " + e.Bucket + "/" + e.Object
+}
+
+// Unwrap the error.
+func (e InsufficientReadQuorum) Unwrap() error {
+ return errErasureReadQuorum
+}
+
+// InsufficientWriteQuorum storage cannot satisfy quorum for write operation.
+type InsufficientWriteQuorum GenericError
+
+func (e InsufficientWriteQuorum) Error() string {
+ return "Storage resources are insufficient for the write operation " + e.Bucket + "/" + e.Object
+}
+
+// Unwrap the error.
+func (e InsufficientWriteQuorum) Unwrap() error {
+ return errErasureWriteQuorum
+}
+
+// GenericError - generic object layer error.
+type GenericError struct {
+ Bucket string
+ Object string
+ VersionID string
+ Err error
+}
+
+// Unwrap the error to its underlying error.
+func (e GenericError) Unwrap() error {
+ return e.Err
+}
+
+// InvalidArgument incorrect input argument
+type InvalidArgument GenericError
+
+func (e InvalidArgument) Error() string {
+ if e.Err != nil {
+ return "Invalid arguments provided for " + e.Bucket + "/" + e.Object + ": (" + e.Err.Error() + ")"
+ }
+ return "Invalid arguments provided for " + e.Bucket + "/" + e.Object
+}
+
+// BucketNotFound bucket does not exist.
+type BucketNotFound GenericError
+
+func (e BucketNotFound) Error() string {
+ return "Bucket not found: " + e.Bucket
+}
+
+// BucketAlreadyExists the requested bucket name is not available.
+type BucketAlreadyExists GenericError
+
+func (e BucketAlreadyExists) Error() string {
+ return "The requested bucket name is not available. The bucket namespace is shared by all users of the system. Please select a different name and try again."
+}
+
+// BucketAlreadyOwnedByYou already owned by you.
+type BucketAlreadyOwnedByYou GenericError
+
+func (e BucketAlreadyOwnedByYou) Error() string {
+ return "Bucket already owned by you: " + e.Bucket
+}
+
+// BucketNotEmpty bucket is not empty.
+type BucketNotEmpty GenericError
+
+func (e BucketNotEmpty) Error() string {
+ return "Bucket not empty: " + e.Bucket
+}
+
+// InvalidVersionID invalid version id
+type InvalidVersionID GenericError
+
+func (e InvalidVersionID) Error() string {
+ return "Invalid version id: " + e.Bucket + "/" + e.Object + "(" + e.VersionID + ")"
+}
+
+// VersionNotFound version does not exist.
+type VersionNotFound GenericError
+
+func (e VersionNotFound) Error() string {
+ return "Version not found: " + e.Bucket + "/" + e.Object + "(" + e.VersionID + ")"
+}
+
+// ObjectNotFound object does not exist.
+type ObjectNotFound GenericError
+
+func (e ObjectNotFound) Error() string {
+ return "Object not found: " + e.Bucket + "/" + e.Object
+}
+
+// MethodNotAllowed on the object
+type MethodNotAllowed GenericError
+
+func (e MethodNotAllowed) Error() string {
+ return "Method not allowed: " + e.Bucket + "/" + e.Object
+}
+
+// ObjectLocked object is currently WORM protected.
+type ObjectLocked GenericError
+
+func (e ObjectLocked) Error() string {
+ return "Object is WORM protected and cannot be overwritten: " + e.Bucket + "/" + e.Object + "(" + e.VersionID + ")"
+}
+
+// ObjectAlreadyExists object already exists.
+type ObjectAlreadyExists GenericError
+
+func (e ObjectAlreadyExists) Error() string {
+ return "Object: " + e.Bucket + "/" + e.Object + " already exists"
+}
+
+// ObjectExistsAsDirectory object already exists as a directory.
+type ObjectExistsAsDirectory GenericError
+
+func (e ObjectExistsAsDirectory) Error() string {
+ return "Object exists on : " + e.Bucket + " as directory " + e.Object
+}
+
+// PrefixAccessDenied object access is denied.
+type PrefixAccessDenied GenericError
+
+func (e PrefixAccessDenied) Error() string {
+ return "Prefix access is denied: " + e.Bucket + SlashSeparator + e.Object
+}
+
+// BucketExists bucket exists.
+type BucketExists GenericError
+
+func (e BucketExists) Error() string {
+ return "Bucket exists: " + e.Bucket
+}
+
+// InvalidUploadIDKeyCombination - invalid upload id and key marker combination.
+type InvalidUploadIDKeyCombination struct {
+ UploadIDMarker, KeyMarker string
+}
+
+func (e InvalidUploadIDKeyCombination) Error() string {
+ return fmt.Sprintf("Invalid combination of uploadID marker '%s' and marker '%s'", e.UploadIDMarker, e.KeyMarker)
+}
+
+// BucketPolicyNotFound - no bucket policy found.
+type BucketPolicyNotFound GenericError
+
+func (e BucketPolicyNotFound) Error() string {
+ return "No bucket policy configuration found for bucket: " + e.Bucket
+}
+
+// BucketLifecycleNotFound - no bucket lifecycle found.
+type BucketLifecycleNotFound GenericError
+
+func (e BucketLifecycleNotFound) Error() string {
+ return "No bucket lifecycle configuration found for bucket : " + e.Bucket
+}
+
+// BucketSSEConfigNotFound - no bucket encryption found
+type BucketSSEConfigNotFound GenericError
+
+func (e BucketSSEConfigNotFound) Error() string {
+ return "No bucket encryption configuration found for bucket: " + e.Bucket
+}
+
+// BucketTaggingNotFound - no bucket tags found
+type BucketTaggingNotFound GenericError
+
+func (e BucketTaggingNotFound) Error() string {
+ return "No bucket tags found for bucket: " + e.Bucket
+}
+
+// BucketObjectLockConfigNotFound - no bucket object lock config found
+type BucketObjectLockConfigNotFound GenericError
+
+func (e BucketObjectLockConfigNotFound) Error() string {
+ return "No bucket object lock configuration found for bucket: " + e.Bucket
+}
+
+// BucketQuotaConfigNotFound - no bucket quota config found.
+type BucketQuotaConfigNotFound GenericError
+
+func (e BucketQuotaConfigNotFound) Error() string {
+ return "No quota config found for bucket : " + e.Bucket
+}
+
+// BucketQuotaExceeded - bucket quota exceeded.
+type BucketQuotaExceeded GenericError
+
+func (e BucketQuotaExceeded) Error() string {
+ return "Bucket quota exceeded for bucket: " + e.Bucket
+}
+
+// BucketReplicationConfigNotFound - no bucket replication config found
+type BucketReplicationConfigNotFound GenericError
+
+func (e BucketReplicationConfigNotFound) Error() string {
+ return "The replication configuration was not found: " + e.Bucket
+}
+
+// BucketRemoteDestinationNotFound bucket does not exist.
+type BucketRemoteDestinationNotFound GenericError
+
+func (e BucketRemoteDestinationNotFound) Error() string {
+ return "Destination bucket does not exist: " + e.Bucket
+}
+
+// BucketRemoteTargetNotFound remote target does not exist.
+type BucketRemoteTargetNotFound GenericError
+
+func (e BucketRemoteTargetNotFound) Error() string {
+ return "Remote target not found: " + e.Bucket
+}
+
+// RemoteTargetConnectionErr remote target connection failure.
+type RemoteTargetConnectionErr struct {
+ Err error
+ Bucket string
+ Endpoint string
+ AccessKey string
+}
+
+func (e RemoteTargetConnectionErr) Error() string {
+ if e.Bucket != "" {
+ return fmt.Sprintf("Remote service endpoint offline, target bucket: %s or remote service credentials: %s invalid \n\t%s", e.Bucket, e.AccessKey, e.Err.Error())
+ }
+ return fmt.Sprintf("Remote service endpoint %s not available\n\t%s", e.Endpoint, e.Err.Error())
+}
+
+// BucketRemoteIdenticalToSource remote already exists for this target type.
+type BucketRemoteIdenticalToSource struct {
+ GenericError
+ Endpoint string
+}
+
+func (e BucketRemoteIdenticalToSource) Error() string {
+ return fmt.Sprintf("Remote service endpoint %s is self referential to current cluster", e.Endpoint)
+}
+
+// BucketRemoteAlreadyExists remote already exists for this target type.
+type BucketRemoteAlreadyExists GenericError
+
+func (e BucketRemoteAlreadyExists) Error() string {
+ return "Remote already exists for this bucket: " + e.Bucket
+}
+
+// BucketRemoteLabelInUse remote already exists for this target label.
+type BucketRemoteLabelInUse GenericError
+
+func (e BucketRemoteLabelInUse) Error() string {
+ return "Remote with this label already exists for this bucket: " + e.Bucket
+}
+
+// BucketRemoteArnTypeInvalid arn type for remote is not valid.
+type BucketRemoteArnTypeInvalid GenericError
+
+func (e BucketRemoteArnTypeInvalid) Error() string {
+ return "Remote ARN type not valid: " + e.Bucket
+}
+
+// BucketRemoteArnInvalid arn needs to be specified.
+type BucketRemoteArnInvalid GenericError
+
+func (e BucketRemoteArnInvalid) Error() string {
+ return "Remote ARN has invalid format: " + e.Bucket
+}
+
+// BucketRemoteRemoveDisallowed when replication configuration exists
+type BucketRemoteRemoveDisallowed GenericError
+
+func (e BucketRemoteRemoveDisallowed) Error() string {
+ return "Replication configuration exists with this ARN:" + e.Bucket
+}
+
+// BucketRemoteTargetNotVersioned remote target does not have versioning enabled.
+type BucketRemoteTargetNotVersioned GenericError
+
+func (e BucketRemoteTargetNotVersioned) Error() string {
+ return "Remote target does not have versioning enabled: " + e.Bucket
+}
+
+// BucketReplicationSourceNotVersioned replication source does not have versioning enabled.
+type BucketReplicationSourceNotVersioned GenericError
+
+func (e BucketReplicationSourceNotVersioned) Error() string {
+ return "Replication source does not have versioning enabled: " + e.Bucket
+}
+
+// TransitionStorageClassNotFound remote tier not configured.
+type TransitionStorageClassNotFound GenericError
+
+func (e TransitionStorageClassNotFound) Error() string {
+ return "Transition storage class not found "
+}
+
+// InvalidObjectState restore-object doesn't apply for the current state of the object.
+type InvalidObjectState GenericError
+
+func (e InvalidObjectState) Error() string {
+ return "The operation is not valid for the current state of the object " + e.Bucket + "/" + e.Object + "(" + e.VersionID + ")"
+}
+
+// Bucket related errors.
+
+// BucketNameInvalid - bucketname provided is invalid.
+type BucketNameInvalid GenericError
+
+// Error returns string an error formatted as the given text.
+func (e BucketNameInvalid) Error() string {
+ return "Bucket name invalid: " + e.Bucket
+}
+
+// Object related errors.
+
+// ObjectNameInvalid - object name provided is invalid.
+type ObjectNameInvalid GenericError
+
+// ObjectNameTooLong - object name too long.
+type ObjectNameTooLong GenericError
+
+// ObjectNamePrefixAsSlash - object name has a slash as prefix.
+type ObjectNamePrefixAsSlash GenericError
+
+// Error returns string an error formatted as the given text.
+func (e ObjectNameInvalid) Error() string {
+ return "Object name invalid: " + e.Bucket + "/" + e.Object
+}
+
+// Error returns string an error formatted as the given text.
+func (e ObjectNameTooLong) Error() string {
+ return "Object name too long: " + e.Bucket + "/" + e.Object
+}
+
+// Error returns string an error formatted as the given text.
+func (e ObjectNamePrefixAsSlash) Error() string {
+ return "Object name contains forward slash as prefix: " + e.Bucket + "/" + e.Object
+}
+
+// AllAccessDisabled All access to this object has been disabled
+type AllAccessDisabled GenericError
+
+// Error returns string an error formatted as the given text.
+func (e AllAccessDisabled) Error() string {
+ return "All access to this object has been disabled"
+}
+
+// IncompleteBody You did not provide the number of bytes specified by the Content-Length HTTP header.
+type IncompleteBody GenericError
+
+// Error returns string an error formatted as the given text.
+func (e IncompleteBody) Error() string {
+ return e.Bucket + "/" + e.Object + " has incomplete body"
+}
+
+// InvalidRange - invalid range typed error.
+type InvalidRange struct {
+ OffsetBegin int64
+ OffsetEnd int64
+ ResourceSize int64
+}
+
+func (e InvalidRange) Error() string {
+ return fmt.Sprintf("The requested range 'bytes=%d-%d' is not satisfiable", e.OffsetBegin, e.OffsetEnd)
+}
+
+// ObjectTooLarge error returned when the size of the object > max object size allowed (5G) per request.
+type ObjectTooLarge GenericError
+
+func (e ObjectTooLarge) Error() string {
+ return "size of the object greater than what is allowed(5G)"
+}
+
+// ObjectTooSmall error returned when the size of the object < what is expected.
+type ObjectTooSmall GenericError
+
+func (e ObjectTooSmall) Error() string {
+ return "size of the object less than what is expected"
+}
+
+// OperationTimedOut - a timeout occurred.
+type OperationTimedOut struct{}
+
+func (e OperationTimedOut) Error() string {
+ return "Operation timed out"
+}
+
+// Multipart related errors.
+
+// MalformedUploadID malformed upload id.
+type MalformedUploadID struct {
+ UploadID string
+}
+
+func (e MalformedUploadID) Error() string {
+ return "Malformed upload id " + e.UploadID
+}
+
+// InvalidUploadID invalid upload id.
+type InvalidUploadID struct {
+ Bucket string
+ Object string
+ UploadID string
+}
+
+func (e InvalidUploadID) Error() string {
+ return "Invalid upload id " + e.UploadID
+}
+
+// InvalidPart One or more of the specified parts could not be found
+type InvalidPart struct {
+ PartNumber int
+ ExpETag string
+ GotETag string
+}
+
+func (e InvalidPart) Error() string {
+ return fmt.Sprintf("Specified part could not be found. PartNumber %d, Expected %s, got %s",
+ e.PartNumber, e.ExpETag, e.GotETag)
+}
+
+// PartTooSmall - error if part size is less than 5MB.
+type PartTooSmall struct {
+ PartSize int64
+ PartNumber int
+ PartETag string
+}
+
+func (e PartTooSmall) Error() string {
+ return fmt.Sprintf("Part size for %d should be at least 5MB", e.PartNumber)
+}
+
+// PartTooBig returned if size of part is bigger than the allowed limit.
+type PartTooBig struct{}
+
+func (e PartTooBig) Error() string {
+ return "Part size bigger than the allowed limit"
+}
+
+// InvalidETag error returned when the etag has changed on disk
+type InvalidETag struct{}
+
+func (e InvalidETag) Error() string {
+ return "etag of the object has changed"
+}
+
+// BackendDown is returned for network errors
+type BackendDown struct {
+ Err string
+}
+
+func (e BackendDown) Error() string {
+ return e.Err
+}
+
+// NotImplemented If a feature is not implemented
+type NotImplemented struct {
+ Message string
+}
+
+func (e NotImplemented) Error() string {
+ return e.Message
+}
+
+// UnsupportedMetadata - unsupported metadata
+type UnsupportedMetadata struct{}
+
+func (e UnsupportedMetadata) Error() string {
+ return "Unsupported headers in Metadata"
+}
+
+// isErrBucketNotFound - Check if error type is BucketNotFound.
+func isErrBucketNotFound(err error) bool {
+ if errors.Is(err, errVolumeNotFound) {
+ return true
+ }
+
+ var bkNotFound BucketNotFound
+ return errors.As(err, &bkNotFound)
+}
+
+// isErrReadQuorum check if the error type is InsufficientReadQuorum
+func isErrReadQuorum(err error) bool {
+ var rquorum InsufficientReadQuorum
+ return errors.As(err, &rquorum)
+}
+
+// isErrWriteQuorum check if the error type is InsufficientWriteQuorum
+func isErrWriteQuorum(err error) bool {
+ var rquorum InsufficientWriteQuorum
+ return errors.As(err, &rquorum)
+}
+
+// isErrObjectNotFound - Check if error type is ObjectNotFound.
+func isErrObjectNotFound(err error) bool {
+ if errors.Is(err, errFileNotFound) {
+ return true
+ }
+
+ var objNotFound ObjectNotFound
+ return errors.As(err, &objNotFound)
+}
+
+// isErrVersionNotFound - Check if error type is VersionNotFound.
+func isErrVersionNotFound(err error) bool {
+ if errors.Is(err, errFileVersionNotFound) {
+ return true
+ }
+
+ var versionNotFound VersionNotFound
+ return errors.As(err, &versionNotFound)
+}
+
+// isErrSignatureDoesNotMatch - Check if error type is SignatureDoesNotMatch.
+func isErrSignatureDoesNotMatch(err error) bool {
+ var signatureDoesNotMatch SignatureDoesNotMatch
+ return errors.As(err, &signatureDoesNotMatch)
+}
+
+// PreConditionFailed - Check if copy precondition failed
+type PreConditionFailed struct{}
+
+func (e PreConditionFailed) Error() string {
+ return "At least one of the pre-conditions you specified did not hold"
+}
+
+func isErrPreconditionFailed(err error) bool {
+ _, ok := err.(PreConditionFailed)
+ return ok
+}
+
+// isErrMethodNotAllowed - Check if error type is MethodNotAllowed.
+func isErrMethodNotAllowed(err error) bool {
+ var methodNotAllowed MethodNotAllowed
+ return errors.As(err, &methodNotAllowed)
+}
+
+func isErrInvalidRange(err error) bool {
+ if errors.Is(err, errInvalidRange) {
+ return true
+ }
+ _, ok := err.(InvalidRange)
+ return ok
+}
+
+// ReplicationPermissionCheck - Check if error type is ReplicationPermissionCheck.
+type ReplicationPermissionCheck struct{}
+
+func (e ReplicationPermissionCheck) Error() string {
+ return "Replication permission validation requests cannot be completed"
+}
+
+func isReplicationPermissionCheck(err error) bool {
+ _, ok := err.(ReplicationPermissionCheck)
+ return ok
+}
+
+// DataMovementOverwriteErr - captures the error when a data movement activity
+// like rebalance incorrectly tries to overwrite an object.
+type DataMovementOverwriteErr GenericError
+
+func (de DataMovementOverwriteErr) Error() string {
+ objInfoStr := fmt.Sprintf("bucket=%s object=%s", de.Bucket, de.Object)
+ if de.VersionID != "" {
+ objInfoStr = fmt.Sprintf("%s version-id=%s", objInfoStr, de.VersionID)
+ }
+ return fmt.Sprintf("invalid data movement operation, source and destination pool are the same for %s", objInfoStr)
+}
+
+func isDataMovementOverWriteErr(err error) bool {
+ var de DataMovementOverwriteErr
+ return errors.As(err, &de)
+}
diff --git a/cmd/object-api-getobjectinfo_test.go b/cmd/object-api-getobjectinfo_test.go
new file mode 100644
index 00000000..6bee4b56
--- /dev/null
+++ b/cmd/object-api-getobjectinfo_test.go
@@ -0,0 +1,115 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "testing"
+)
+
+// Wrapper for calling GetObjectInfo tests for both Erasure multiple disks and single node setup.
+func TestGetObjectInfo(t *testing.T) {
+ ExecObjectLayerTest(t, testGetObjectInfo)
+}
+
+// Testing GetObjectInfo().
+func testGetObjectInfo(obj ObjectLayer, instanceType string, t TestErrHandler) {
+ // This bucket is used for testing getObjectInfo operations.
+ err := obj.MakeBucket(context.Background(), "test-getobjectinfo", MakeBucketOptions{})
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ opts := ObjectOptions{}
+ _, err = obj.PutObject(context.Background(), "test-getobjectinfo", "Asia/asiapics.jpg", mustGetPutObjReader(t, bytes.NewBufferString("asiapics"), int64(len("asiapics")), "", ""), opts)
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ // Put an empty directory
+ _, err = obj.PutObject(context.Background(), "test-getobjectinfo", "Asia/empty-dir/", mustGetPutObjReader(t, bytes.NewBufferString(""), int64(len("")), "", ""), opts)
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ resultCases := []ObjectInfo{
+ // ObjectInfo -1.
+ // ObjectName set to a existing object in the test case (Test case 14).
+ {Bucket: "test-getobjectinfo", Name: "Asia/asiapics.jpg", ContentType: "image/jpeg", IsDir: false},
+ {Bucket: "test-getobjectinfo", Name: "Asia/empty-dir/", ContentType: "application/octet-stream", IsDir: true},
+ }
+ testCases := []struct {
+ bucketName string
+ objectName string
+
+ // Expected output of GetObjectInfo.
+ result ObjectInfo
+ err error
+ // Flag indicating whether the test is expected to pass or not.
+ shouldPass bool
+ }{
+ // Test cases with invalid bucket names ( Test number 1-4 ).
+ {".test", "", ObjectInfo{}, BucketNameInvalid{Bucket: ".test"}, false},
+ {"---", "", ObjectInfo{}, BucketNameInvalid{Bucket: "---"}, false},
+ {"ad", "", ObjectInfo{}, BucketNameInvalid{Bucket: "ad"}, false},
+ // Test cases with valid but non-existing bucket names (Test number 5-6).
+ {"abcdefgh", "abc", ObjectInfo{}, BucketNotFound{Bucket: "abcdefgh"}, false},
+ {"ijklmnop", "efg", ObjectInfo{}, BucketNotFound{Bucket: "ijklmnop"}, false},
+ // Test cases with valid but non-existing bucket names and invalid object name (Test number 7-8).
+ {"test-getobjectinfo", "", ObjectInfo{}, ObjectNameInvalid{Bucket: "test-getobjectinfo", Object: ""}, false},
+ {"test-getobjectinfo", "", ObjectInfo{}, ObjectNameInvalid{Bucket: "test-getobjectinfo", Object: ""}, false},
+ // Test cases with non-existing object name with existing bucket (Test number 9-11).
+ {"test-getobjectinfo", "Africa", ObjectInfo{}, ObjectNotFound{Bucket: "test-getobjectinfo", Object: "Africa"}, false},
+ {"test-getobjectinfo", "Antartica", ObjectInfo{}, ObjectNotFound{Bucket: "test-getobjectinfo", Object: "Antartica"}, false},
+ {"test-getobjectinfo", "Asia/myfile", ObjectInfo{}, ObjectNotFound{Bucket: "test-getobjectinfo", Object: "Asia/myfile"}, false},
+ // Valid case with existing object (Test number 12).
+ {"test-getobjectinfo", "Asia/asiapics.jpg", resultCases[0], nil, true},
+ {"test-getobjectinfo", "Asia/empty-dir/", resultCases[1], nil, true},
+ }
+ for i, testCase := range testCases {
+ result, err := obj.GetObjectInfo(context.Background(), testCase.bucketName, testCase.objectName, opts)
+ if err != nil && testCase.shouldPass {
+ t.Errorf("Test %d: %s: Expected to pass, but failed with: %s", i+1, instanceType, err.Error())
+ }
+ if err == nil && !testCase.shouldPass {
+ t.Errorf("Test %d: %s: Expected to fail with \"%s\", but passed instead", i+1, instanceType, testCase.err.Error())
+ }
+ // Failed as expected, but does it fail for the expected reason.
+ if err != nil && !testCase.shouldPass {
+ if testCase.err.Error() != err.Error() {
+ t.Errorf("Test %d: %s: Expected to fail with error \"%s\", but instead failed with error \"%s\" instead", i+1, instanceType, testCase.err.Error(), err.Error())
+ }
+ }
+
+ // Test passes as expected, but the output values are verified for correctness here.
+ if err == nil && testCase.shouldPass {
+ if testCase.result.Bucket != result.Bucket {
+ t.Fatalf("Test %d: %s: Expected Bucket name to be '%s', but found '%s' instead", i+1, instanceType, testCase.result.Bucket, result.Bucket)
+ }
+ if testCase.result.Name != result.Name {
+ t.Errorf("Test %d: %s: Expected Object name to be %s, but instead found it to be %s", i+1, instanceType, testCase.result.Name, result.Name)
+ }
+ if testCase.result.ContentType != result.ContentType {
+ t.Errorf("Test %d: %s: Expected Content Type of the object to be %v, but instead found it to be %v", i+1, instanceType, testCase.result.ContentType, result.ContentType)
+ }
+ if testCase.result.IsDir != result.IsDir {
+ t.Errorf("Test %d: %s: Expected IsDir flag of the object to be %v, but instead found it to be %v", i+1, instanceType, testCase.result.IsDir, result.IsDir)
+ }
+ }
+ }
+}
diff --git a/cmd/object-api-input-checks.go b/cmd/object-api-input-checks.go
new file mode 100644
index 00000000..9c8b213e
--- /dev/null
+++ b/cmd/object-api-input-checks.go
@@ -0,0 +1,178 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "encoding/base64"
+ "runtime"
+ "strings"
+
+ "github.com/minio/minio-go/v7/pkg/s3utils"
+)
+
+// Checks on CopyObject arguments, bucket and object.
+func checkCopyObjArgs(ctx context.Context, bucket, object string) error {
+ return checkBucketAndObjectNames(ctx, bucket, object)
+}
+
+// Checks on GetObject arguments, bucket and object.
+func checkGetObjArgs(ctx context.Context, bucket, object string) error {
+ return checkBucketAndObjectNames(ctx, bucket, object)
+}
+
+// Checks on DeleteObject arguments, bucket and object.
+func checkDelObjArgs(ctx context.Context, bucket, object string) error {
+ return checkBucketAndObjectNames(ctx, bucket, object)
+}
+
+// Checks bucket and object name validity, returns nil if both are valid.
+func checkBucketAndObjectNames(ctx context.Context, bucket, object string) error {
+ // Verify if bucket is valid.
+ if !isMinioMetaBucketName(bucket) && s3utils.CheckValidBucketNameStrict(bucket) != nil {
+ return BucketNameInvalid{Bucket: bucket}
+ }
+ // Verify if object is valid.
+ if len(object) == 0 {
+ return ObjectNameInvalid{Bucket: bucket, Object: object}
+ }
+ if !IsValidObjectPrefix(object) {
+ return ObjectNameInvalid{Bucket: bucket, Object: object}
+ }
+ if runtime.GOOS == globalWindowsOSName && strings.Contains(object, "\\") {
+ // Objects cannot be contain \ in Windows and is listed as `Characters to Avoid`.
+ return ObjectNameInvalid{Bucket: bucket, Object: object}
+ }
+ return nil
+}
+
+// Checks for all ListObjects arguments validity.
+func checkListObjsArgs(ctx context.Context, bucket, prefix, marker string) error {
+ // Verify if bucket is valid.
+ if !isMinioMetaBucketName(bucket) && s3utils.CheckValidBucketNameStrict(bucket) != nil {
+ return BucketNameInvalid{Bucket: bucket}
+ }
+
+ // Validates object prefix validity after bucket exists.
+ if !IsValidObjectPrefix(prefix) {
+ return ObjectNameInvalid{
+ Bucket: bucket,
+ Object: prefix,
+ }
+ }
+ return nil
+}
+
+// Checks for all ListMultipartUploads arguments validity.
+func checkListMultipartArgs(ctx context.Context, bucket, prefix, keyMarker, uploadIDMarker, delimiter string) error {
+ if err := checkListObjsArgs(ctx, bucket, prefix, keyMarker); err != nil {
+ return err
+ }
+ if uploadIDMarker != "" {
+ if HasSuffix(keyMarker, SlashSeparator) {
+ return InvalidUploadIDKeyCombination{
+ UploadIDMarker: uploadIDMarker,
+ KeyMarker: keyMarker,
+ }
+ }
+ _, err := base64.RawURLEncoding.DecodeString(uploadIDMarker)
+ if err != nil {
+ return MalformedUploadID{
+ UploadID: uploadIDMarker,
+ }
+ }
+ }
+ return nil
+}
+
+// Checks for NewMultipartUpload arguments validity, also validates if bucket exists.
+func checkNewMultipartArgs(ctx context.Context, bucket, object string) error {
+ return checkObjectArgs(ctx, bucket, object)
+}
+
+func checkMultipartObjectArgs(ctx context.Context, bucket, object, uploadID string) error {
+ _, err := base64.RawURLEncoding.DecodeString(uploadID)
+ if err != nil {
+ return MalformedUploadID{
+ UploadID: uploadID,
+ }
+ }
+ return checkObjectArgs(ctx, bucket, object)
+}
+
+// Checks for PutObjectPart arguments validity, also validates if bucket exists.
+func checkPutObjectPartArgs(ctx context.Context, bucket, object, uploadID string) error {
+ return checkMultipartObjectArgs(ctx, bucket, object, uploadID)
+}
+
+// Checks for ListParts arguments validity, also validates if bucket exists.
+func checkListPartsArgs(ctx context.Context, bucket, object, uploadID string) error {
+ return checkMultipartObjectArgs(ctx, bucket, object, uploadID)
+}
+
+// Checks for CompleteMultipartUpload arguments validity, also validates if bucket exists.
+func checkCompleteMultipartArgs(ctx context.Context, bucket, object, uploadID string) error {
+ return checkMultipartObjectArgs(ctx, bucket, object, uploadID)
+}
+
+// Checks for AbortMultipartUpload arguments validity, also validates if bucket exists.
+func checkAbortMultipartArgs(ctx context.Context, bucket, object, uploadID string) error {
+ return checkMultipartObjectArgs(ctx, bucket, object, uploadID)
+}
+
+// Checks Object arguments validity.
+func checkObjectArgs(ctx context.Context, bucket, object string) error {
+ // Verify if bucket is valid.
+ if !isMinioMetaBucketName(bucket) && s3utils.CheckValidBucketNameStrict(bucket) != nil {
+ return BucketNameInvalid{Bucket: bucket}
+ }
+
+ if err := checkObjectNameForLengthAndSlash(bucket, object); err != nil {
+ return err
+ }
+
+ // Validates object name validity after bucket exists.
+ if !IsValidObjectName(object) {
+ return ObjectNameInvalid{
+ Bucket: bucket,
+ Object: object,
+ }
+ }
+
+ return nil
+}
+
+// Checks for PutObject arguments validity.
+func checkPutObjectArgs(ctx context.Context, bucket, object string) error {
+ // Verify if bucket is valid.
+ if !isMinioMetaBucketName(bucket) && s3utils.CheckValidBucketNameStrict(bucket) != nil {
+ return BucketNameInvalid{Bucket: bucket}
+ }
+
+ if err := checkObjectNameForLengthAndSlash(bucket, object); err != nil {
+ return err
+ }
+ if len(object) == 0 ||
+ !IsValidObjectPrefix(object) {
+ return ObjectNameInvalid{
+ Bucket: bucket,
+ Object: object,
+ }
+ }
+ return nil
+}
diff --git a/cmd/object-api-interface.go b/cmd/object-api-interface.go
new file mode 100644
index 00000000..06c73741
--- /dev/null
+++ b/cmd/object-api-interface.go
@@ -0,0 +1,338 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "io"
+ "net/http"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/minio-go/v7/pkg/encrypt"
+ "github.com/minio/minio-go/v7/pkg/tags"
+ "github.com/minio/minio/internal/hash"
+
+ "github.com/minio/minio/internal/bucket/replication"
+ xioutil "github.com/minio/minio/internal/ioutil"
+)
+
+//go:generate msgp -file $GOFILE -io=false -tests=false -unexported=false
+
+//msgp:ignore ObjectOptions TransitionOptions DeleteBucketOptions
+
+// CheckPreconditionFn returns true if precondition check failed.
+type CheckPreconditionFn func(o ObjectInfo) bool
+
+// EvalMetadataFn validates input objInfo and GetObjectInfo error and returns an updated metadata and replication decision if any
+type EvalMetadataFn func(o *ObjectInfo, gerr error) (ReplicateDecision, error)
+
+// EvalRetentionBypassFn validates input objInfo and GetObjectInfo error and returns an error if retention bypass is not allowed.
+type EvalRetentionBypassFn func(o ObjectInfo, gerr error) error
+
+// GetObjectInfoFn is the signature of GetObjectInfo function.
+type GetObjectInfoFn func(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error)
+
+// WalkVersionsSortOrder represents the sort order in which versions of an
+// object should be returned by ObjectLayer.Walk method
+type WalkVersionsSortOrder uint8
+
+const (
+ // WalkVersionsSortAsc - Sort in ascending order of ModTime
+ WalkVersionsSortAsc WalkVersionsSortOrder = iota
+ // WalkVersionsSortDesc - Sort in descending order of ModTime
+ WalkVersionsSortDesc
+)
+
+// ObjectOptions represents object options for ObjectLayer object operations
+type ObjectOptions struct {
+ ServerSideEncryption encrypt.ServerSide
+ VersionSuspended bool // indicates if the bucket was previously versioned but is currently suspended.
+ Versioned bool // indicates if the bucket is versioned
+ VersionID string // Specifies the versionID which needs to be overwritten or read
+ MTime time.Time // Is only set in POST/PUT operations
+ Expires time.Time // Is only used in POST/PUT operations
+
+ DeleteMarker bool // Is only set in DELETE operations for delete marker replication
+ CheckDMReplicationReady bool // Is delete marker ready to be replicated - set only during HEAD
+ Tagging bool // Is only in GET/HEAD operations to return tagging metadata along with regular metadata and body.
+
+ UserDefined map[string]string // only set in case of POST/PUT operations
+ ObjectAttributes map[string]struct{} // Attribute tags defined by the users for the GetObjectAttributes request
+ MaxParts int // used in GetObjectAttributes. Signals how many parts we should return
+ PartNumberMarker int // used in GetObjectAttributes. Signals the part number after which results should be returned
+ PartNumber int // only useful in case of GetObject/HeadObject
+ CheckPrecondFn CheckPreconditionFn // only set during GetObject/HeadObject/CopyObjectPart preconditional valuation
+ EvalMetadataFn EvalMetadataFn // only set for retention settings, meant to be used only when updating metadata in-place.
+ DeleteReplication ReplicationState // Represents internal replication state needed for Delete replication
+ Transition TransitionOptions
+ Expiration ExpirationOptions
+ LifecycleAuditEvent lcAuditEvent
+
+ WantChecksum *hash.Checksum // x-amz-checksum-XXX checksum sent to PutObject/ CompleteMultipartUpload.
+
+ WantServerSideChecksumType hash.ChecksumType // if set, we compute a server-side checksum of this type
+
+ NoDecryption bool // indicates if the stream must be decrypted.
+ PreserveETag string // preserves this etag during a PUT call.
+ NoLock bool // indicates to lower layers if the caller is expecting to hold locks.
+ HasIfMatch bool // indicates if the request has If-Match header
+ ProxyRequest bool // only set for GET/HEAD in active-active replication scenario
+ ProxyHeaderSet bool // only set for GET/HEAD in active-active replication scenario
+ ReplicationRequest bool // true only if replication request
+ ReplicationSourceTaggingTimestamp time.Time // set if MinIOSourceTaggingTimestamp received
+ ReplicationSourceLegalholdTimestamp time.Time // set if MinIOSourceObjectLegalholdTimestamp received
+ ReplicationSourceRetentionTimestamp time.Time // set if MinIOSourceObjectRetentionTimestamp received
+ DeletePrefix bool // set true to enforce a prefix deletion, only application for DeleteObject API,
+ DeletePrefixObject bool // set true when object's erasure set is resolvable by object name (using getHashedSetIndex)
+
+ Speedtest bool // object call specifically meant for SpeedTest code, set to 'true' when invoked by SpeedtestHandler.
+
+ // Use the maximum parity (N/2), used when saving server configuration files
+ MaxParity bool
+
+ // Provides a per object encryption function, allowing metadata encryption.
+ EncryptFn objectMetaEncryptFn
+
+ // SkipDecommissioned set to 'true' if the call requires skipping the pool being decommissioned.
+ // mainly set for certain WRITE operations.
+ SkipDecommissioned bool
+ // SkipRebalancing should be set to 'true' if the call should skip pools
+ // participating in a rebalance operation. Typically set for 'write' operations.
+ SkipRebalancing bool
+
+ SrcPoolIdx int // set by PutObject/CompleteMultipart operations due to rebalance; used to prevent rebalance src, dst pools to be the same
+
+ DataMovement bool // indicates an going decommisionning or rebalacing
+
+ PrefixEnabledFn func(prefix string) bool // function which returns true if versioning is enabled on prefix
+
+ // IndexCB will return any index created but the compression.
+ // Object must have been read at this point.
+ IndexCB func() []byte
+
+ // InclFreeVersions indicates that free versions need to be included
+ // when looking up a version by fi.VersionID
+ InclFreeVersions bool
+ // SkipFreeVersion skips adding a free version when a tiered version is
+ // being 'replaced'
+ // Note: Used only when a tiered object is being expired.
+ SkipFreeVersion bool
+
+ MetadataChg bool // is true if it is a metadata update operation.
+ EvalRetentionBypassFn EvalRetentionBypassFn // only set for enforcing retention bypass on DeleteObject.
+
+ FastGetObjInfo bool // Only for S3 Head/Get Object calls for now
+ NoAuditLog bool // Only set for decom, rebalance, to avoid double audits.
+}
+
+// WalkOptions provides filtering, marker and other Walk() specific options.
+type WalkOptions struct {
+ Filter func(info FileInfo) bool // return WalkFilter returns 'true/false'
+ Marker string // set to skip until this object
+ LatestOnly bool // returns only latest versions for all matching objects
+ AskDisks string // dictates how many disks are being listed
+ VersionsSort WalkVersionsSortOrder // sort order for versions of the same object; default: Ascending order in ModTime
+ Limit int // maximum number of items, 0 means no limit
+}
+
+// ExpirationOptions represents object options for object expiration at objectLayer.
+type ExpirationOptions struct {
+ Expire bool
+}
+
+// TransitionOptions represents object options for transition ObjectLayer operation
+type TransitionOptions struct {
+ Status string
+ Tier string
+ ETag string
+ RestoreRequest *RestoreObjectRequest
+ RestoreExpiry time.Time
+ ExpireRestored bool
+}
+
+// MakeBucketOptions represents bucket options for ObjectLayer bucket operations
+type MakeBucketOptions struct {
+ LockEnabled bool
+ VersioningEnabled bool
+ ForceCreate bool // Create buckets even if they are already created.
+ CreatedAt time.Time // only for site replication
+ NoLock bool // does not lock the make bucket call if set to 'true'
+}
+
+// DeleteBucketOptions provides options for DeleteBucket calls.
+type DeleteBucketOptions struct {
+ NoLock bool // does not lock the delete bucket call if set to 'true'
+ NoRecreate bool // do not recreate bucket on delete failures
+ Force bool // Force deletion
+ SRDeleteOp SRBucketDeleteOp // only when site replication is enabled
+}
+
+// BucketOptions provides options for ListBuckets and GetBucketInfo call.
+type BucketOptions struct {
+ Deleted bool // true only when site replication is enabled
+ Cached bool // true only when we are requesting a cached response instead of hitting the disk for example ListBuckets() call.
+ NoMetadata bool
+}
+
+// SetReplicaStatus sets replica status and timestamp for delete operations in ObjectOptions
+func (o *ObjectOptions) SetReplicaStatus(st replication.StatusType) {
+ o.DeleteReplication.ReplicaStatus = st
+ o.DeleteReplication.ReplicaTimeStamp = UTCNow()
+}
+
+// DeleteMarkerReplicationStatus - returns replication status of delete marker from DeleteReplication state in ObjectOptions
+func (o *ObjectOptions) DeleteMarkerReplicationStatus() replication.StatusType {
+ return o.DeleteReplication.CompositeReplicationStatus()
+}
+
+// VersionPurgeStatus - returns version purge status from DeleteReplication state in ObjectOptions
+func (o *ObjectOptions) VersionPurgeStatus() VersionPurgeStatusType {
+ return o.DeleteReplication.CompositeVersionPurgeStatus()
+}
+
+// SetDeleteReplicationState sets the delete replication options.
+func (o *ObjectOptions) SetDeleteReplicationState(dsc ReplicateDecision, vID string) {
+ o.DeleteReplication = ReplicationState{
+ ReplicateDecisionStr: dsc.String(),
+ }
+ switch o.VersionID {
+ case "":
+ o.DeleteReplication.ReplicationStatusInternal = dsc.PendingStatus()
+ o.DeleteReplication.Targets = replicationStatusesMap(o.DeleteReplication.ReplicationStatusInternal)
+ default:
+ o.DeleteReplication.VersionPurgeStatusInternal = dsc.PendingStatus()
+ o.DeleteReplication.PurgeTargets = versionPurgeStatusesMap(o.DeleteReplication.VersionPurgeStatusInternal)
+ }
+}
+
+// PutReplicationState gets ReplicationState for PUT operation from ObjectOptions
+func (o *ObjectOptions) PutReplicationState() (r ReplicationState) {
+ rstatus, ok := o.UserDefined[ReservedMetadataPrefixLower+ReplicationStatus]
+ if !ok {
+ return r
+ }
+ r.ReplicationStatusInternal = rstatus
+ r.Targets = replicationStatusesMap(rstatus)
+ return r
+}
+
+// SetEvalMetadataFn sets the metadata evaluation function
+func (o *ObjectOptions) SetEvalMetadataFn(f EvalMetadataFn) {
+ o.EvalMetadataFn = f
+}
+
+// SetEvalRetentionBypassFn sets the retention bypass function
+func (o *ObjectOptions) SetEvalRetentionBypassFn(f EvalRetentionBypassFn) {
+ o.EvalRetentionBypassFn = f
+}
+
+// ObjectLayer implements primitives for object API layer.
+type ObjectLayer interface {
+ // Locking operations on object.
+ NewNSLock(bucket string, objects ...string) RWLocker
+
+ // Storage operations.
+ Shutdown(context.Context) error
+ NSScanner(ctx context.Context, updates chan<- DataUsageInfo, wantCycle uint32, scanMode madmin.HealScanMode) error
+ BackendInfo() madmin.BackendInfo
+ Legacy() bool // Only returns true for deployments which use CRCMOD as its object distribution algorithm.
+ StorageInfo(ctx context.Context, metrics bool) StorageInfo
+ LocalStorageInfo(ctx context.Context, metrics bool) StorageInfo
+
+ // Bucket operations.
+ MakeBucket(ctx context.Context, bucket string, opts MakeBucketOptions) error
+ GetBucketInfo(ctx context.Context, bucket string, opts BucketOptions) (bucketInfo BucketInfo, err error)
+ ListBuckets(ctx context.Context, opts BucketOptions) (buckets []BucketInfo, err error)
+ DeleteBucket(ctx context.Context, bucket string, opts DeleteBucketOptions) error
+ ListObjects(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (result ListObjectsInfo, err error)
+ ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (result ListObjectsV2Info, err error)
+ ListObjectVersions(ctx context.Context, bucket, prefix, marker, versionMarker, delimiter string, maxKeys int) (result ListObjectVersionsInfo, err error)
+ // Walk lists all objects including versions, delete markers.
+ Walk(ctx context.Context, bucket, prefix string, results chan<- itemOrErr[ObjectInfo], opts WalkOptions) error
+
+ // Object operations.
+
+ // GetObjectNInfo returns a GetObjectReader that satisfies the
+ // ReadCloser interface. The Close method runs any cleanup
+ // functions, so it must always be called after reading till EOF
+ //
+ // IMPORTANTLY, when implementations return err != nil, this
+ // function MUST NOT return a non-nil ReadCloser.
+ GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions) (reader *GetObjectReader, err error)
+ GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
+ PutObject(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error)
+ CopyObject(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error)
+ DeleteObject(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error)
+ DeleteObjects(ctx context.Context, bucket string, objects []ObjectToDelete, opts ObjectOptions) ([]DeletedObject, []error)
+ TransitionObject(ctx context.Context, bucket, object string, opts ObjectOptions) error
+ RestoreTransitionedObject(ctx context.Context, bucket, object string, opts ObjectOptions) error
+
+ // Multipart operations.
+ ListMultipartUploads(ctx context.Context, bucket, prefix, keyMarker, uploadIDMarker, delimiter string, maxUploads int) (result ListMultipartsInfo, err error)
+ NewMultipartUpload(ctx context.Context, bucket, object string, opts ObjectOptions) (result *NewMultipartUploadResult, err error)
+ CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, uploadID string, partID int,
+ startOffset int64, length int64, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (info PartInfo, err error)
+ PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, data *PutObjReader, opts ObjectOptions) (info PartInfo, err error)
+ GetMultipartInfo(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) (info MultipartInfo, err error)
+ ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int, opts ObjectOptions) (result ListPartsInfo, err error)
+ AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) error
+ CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart, opts ObjectOptions) (objInfo ObjectInfo, err error)
+
+ GetDisks(poolIdx, setIdx int) ([]StorageAPI, error) // return the disks belonging to pool and set.
+ SetDriveCounts() []int // list of erasure stripe size for each pool in order.
+
+ // Healing operations.
+ HealFormat(ctx context.Context, dryRun bool) (madmin.HealResultItem, error)
+ HealBucket(ctx context.Context, bucket string, opts madmin.HealOpts) (madmin.HealResultItem, error)
+ HealObject(ctx context.Context, bucket, object, versionID string, opts madmin.HealOpts) (madmin.HealResultItem, error)
+ HealObjects(ctx context.Context, bucket, prefix string, opts madmin.HealOpts, fn HealObjectFn) error
+ CheckAbandonedParts(ctx context.Context, bucket, object string, opts madmin.HealOpts) error
+
+ // Returns health of the backend
+ Health(ctx context.Context, opts HealthOptions) HealthResult
+
+ // Metadata operations
+ PutObjectMetadata(context.Context, string, string, ObjectOptions) (ObjectInfo, error)
+ DecomTieredObject(context.Context, string, string, FileInfo, ObjectOptions) error
+
+ // ObjectTagging operations
+ PutObjectTags(context.Context, string, string, string, ObjectOptions) (ObjectInfo, error)
+ GetObjectTags(context.Context, string, string, ObjectOptions) (*tags.Tags, error)
+ DeleteObjectTags(context.Context, string, string, ObjectOptions) (ObjectInfo, error)
+}
+
+// GetObject - TODO(aead): This function just acts as an adapter for GetObject tests and benchmarks
+// since the GetObject method of the ObjectLayer interface has been removed. Once, the
+// tests are adjusted to use GetObjectNInfo this function can be removed.
+func GetObject(ctx context.Context, api ObjectLayer, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) (err error) {
+ var header http.Header
+ if etag != "" {
+ header.Set("ETag", etag)
+ }
+ Range := &HTTPRangeSpec{Start: startOffset, End: startOffset + length}
+
+ reader, err := api.GetObjectNInfo(ctx, bucket, object, Range, header, opts)
+ if err != nil {
+ return err
+ }
+ defer reader.Close()
+
+ _, err = xioutil.Copy(writer, reader)
+ return err
+}
diff --git a/cmd/object-api-interface_gen.go b/cmd/object-api-interface_gen.go
new file mode 100644
index 00000000..919cdb17
--- /dev/null
+++ b/cmd/object-api-interface_gen.go
@@ -0,0 +1,337 @@
+// Code generated by github.com/tinylib/msgp DO NOT EDIT.
+
+package cmd
+
+import (
+ "github.com/tinylib/msgp/msgp"
+)
+
+// MarshalMsg implements msgp.Marshaler
+func (z BucketOptions) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 3
+ // string "Deleted"
+ o = append(o, 0x83, 0xa7, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64)
+ o = msgp.AppendBool(o, z.Deleted)
+ // string "Cached"
+ o = append(o, 0xa6, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64)
+ o = msgp.AppendBool(o, z.Cached)
+ // string "NoMetadata"
+ o = append(o, 0xaa, 0x4e, 0x6f, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61)
+ o = msgp.AppendBool(o, z.NoMetadata)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *BucketOptions) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Deleted":
+ z.Deleted, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Deleted")
+ return
+ }
+ case "Cached":
+ z.Cached, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Cached")
+ return
+ }
+ case "NoMetadata":
+ z.NoMetadata, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "NoMetadata")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z BucketOptions) Msgsize() (s int) {
+ s = 1 + 8 + msgp.BoolSize + 7 + msgp.BoolSize + 11 + msgp.BoolSize
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z ExpirationOptions) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 1
+ // string "Expire"
+ o = append(o, 0x81, 0xa6, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65)
+ o = msgp.AppendBool(o, z.Expire)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *ExpirationOptions) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Expire":
+ z.Expire, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Expire")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z ExpirationOptions) Msgsize() (s int) {
+ s = 1 + 7 + msgp.BoolSize
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *MakeBucketOptions) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 5
+ // string "LockEnabled"
+ o = append(o, 0x85, 0xab, 0x4c, 0x6f, 0x63, 0x6b, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64)
+ o = msgp.AppendBool(o, z.LockEnabled)
+ // string "VersioningEnabled"
+ o = append(o, 0xb1, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64)
+ o = msgp.AppendBool(o, z.VersioningEnabled)
+ // string "ForceCreate"
+ o = append(o, 0xab, 0x46, 0x6f, 0x72, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65)
+ o = msgp.AppendBool(o, z.ForceCreate)
+ // string "CreatedAt"
+ o = append(o, 0xa9, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
+ o = msgp.AppendTime(o, z.CreatedAt)
+ // string "NoLock"
+ o = append(o, 0xa6, 0x4e, 0x6f, 0x4c, 0x6f, 0x63, 0x6b)
+ o = msgp.AppendBool(o, z.NoLock)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *MakeBucketOptions) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "LockEnabled":
+ z.LockEnabled, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "LockEnabled")
+ return
+ }
+ case "VersioningEnabled":
+ z.VersioningEnabled, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "VersioningEnabled")
+ return
+ }
+ case "ForceCreate":
+ z.ForceCreate, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "ForceCreate")
+ return
+ }
+ case "CreatedAt":
+ z.CreatedAt, bts, err = msgp.ReadTimeBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "CreatedAt")
+ return
+ }
+ case "NoLock":
+ z.NoLock, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "NoLock")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *MakeBucketOptions) Msgsize() (s int) {
+ s = 1 + 12 + msgp.BoolSize + 18 + msgp.BoolSize + 12 + msgp.BoolSize + 10 + msgp.TimeSize + 7 + msgp.BoolSize
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z *WalkOptions) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ // map header, size 5
+ // string "Marker"
+ o = append(o, 0x85, 0xa6, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72)
+ o = msgp.AppendString(o, z.Marker)
+ // string "LatestOnly"
+ o = append(o, 0xaa, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x4f, 0x6e, 0x6c, 0x79)
+ o = msgp.AppendBool(o, z.LatestOnly)
+ // string "AskDisks"
+ o = append(o, 0xa8, 0x41, 0x73, 0x6b, 0x44, 0x69, 0x73, 0x6b, 0x73)
+ o = msgp.AppendString(o, z.AskDisks)
+ // string "VersionsSort"
+ o = append(o, 0xac, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x53, 0x6f, 0x72, 0x74)
+ o = msgp.AppendUint8(o, uint8(z.VersionsSort))
+ // string "Limit"
+ o = append(o, 0xa5, 0x4c, 0x69, 0x6d, 0x69, 0x74)
+ o = msgp.AppendInt(o, z.Limit)
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *WalkOptions) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ var field []byte
+ _ = field
+ var zb0001 uint32
+ zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ for zb0001 > 0 {
+ zb0001--
+ field, bts, err = msgp.ReadMapKeyZC(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ switch msgp.UnsafeString(field) {
+ case "Marker":
+ z.Marker, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Marker")
+ return
+ }
+ case "LatestOnly":
+ z.LatestOnly, bts, err = msgp.ReadBoolBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "LatestOnly")
+ return
+ }
+ case "AskDisks":
+ z.AskDisks, bts, err = msgp.ReadStringBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "AskDisks")
+ return
+ }
+ case "VersionsSort":
+ {
+ var zb0002 uint8
+ zb0002, bts, err = msgp.ReadUint8Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "VersionsSort")
+ return
+ }
+ z.VersionsSort = WalkVersionsSortOrder(zb0002)
+ }
+ case "Limit":
+ z.Limit, bts, err = msgp.ReadIntBytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err, "Limit")
+ return
+ }
+ default:
+ bts, err = msgp.Skip(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ }
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z *WalkOptions) Msgsize() (s int) {
+ s = 1 + 7 + msgp.StringPrefixSize + len(z.Marker) + 11 + msgp.BoolSize + 9 + msgp.StringPrefixSize + len(z.AskDisks) + 13 + msgp.Uint8Size + 6 + msgp.IntSize
+ return
+}
+
+// MarshalMsg implements msgp.Marshaler
+func (z WalkVersionsSortOrder) MarshalMsg(b []byte) (o []byte, err error) {
+ o = msgp.Require(b, z.Msgsize())
+ o = msgp.AppendUint8(o, uint8(z))
+ return
+}
+
+// UnmarshalMsg implements msgp.Unmarshaler
+func (z *WalkVersionsSortOrder) UnmarshalMsg(bts []byte) (o []byte, err error) {
+ {
+ var zb0001 uint8
+ zb0001, bts, err = msgp.ReadUint8Bytes(bts)
+ if err != nil {
+ err = msgp.WrapError(err)
+ return
+ }
+ (*z) = WalkVersionsSortOrder(zb0001)
+ }
+ o = bts
+ return
+}
+
+// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
+func (z WalkVersionsSortOrder) Msgsize() (s int) {
+ s = msgp.Uint8Size
+ return
+}
diff --git a/cmd/object-api-listobjects_test.go b/cmd/object-api-listobjects_test.go
new file mode 100644
index 00000000..58bfc4f8
--- /dev/null
+++ b/cmd/object-api-listobjects_test.go
@@ -0,0 +1,2046 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "crypto/md5"
+ "encoding/hex"
+ "fmt"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/minio/minio/internal/bucket/lifecycle"
+)
+
+func TestListObjectsVersionedFolders(t *testing.T) {
+ ExecObjectLayerTest(t, testListObjectsVersionedFolders)
+}
+
+func testListObjectsVersionedFolders(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
+ t, _ := t1.(*testing.T)
+ testBuckets := []string{
+ // This bucket is used for testing ListObject operations.
+ "test-bucket-folders",
+ // This bucket has file delete marker.
+ "test-bucket-files",
+ }
+ for _, bucket := range testBuckets {
+ err := obj.MakeBucket(context.Background(), bucket, MakeBucketOptions{
+ VersioningEnabled: true,
+ })
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ }
+
+ var err error
+ testObjects := []struct {
+ parentBucket string
+ name string
+ content string
+ meta map[string]string
+ addDeleteMarker bool
+ }{
+ {testBuckets[0], "unique/folder/", "", nil, true},
+ {testBuckets[0], "unique/folder/1.txt", "content", nil, false},
+ {testBuckets[1], "unique/folder/1.txt", "content", nil, true},
+ }
+ for _, object := range testObjects {
+ md5Bytes := md5.Sum([]byte(object.content))
+ _, err = obj.PutObject(context.Background(), object.parentBucket, object.name, mustGetPutObjReader(t, bytes.NewBufferString(object.content),
+ int64(len(object.content)), hex.EncodeToString(md5Bytes[:]), ""), ObjectOptions{
+ Versioned: globalBucketVersioningSys.PrefixEnabled(object.parentBucket, object.name),
+ UserDefined: object.meta,
+ })
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ if object.addDeleteMarker {
+ oi, err := obj.DeleteObject(context.Background(), object.parentBucket, object.name, ObjectOptions{
+ Versioned: globalBucketVersioningSys.PrefixEnabled(object.parentBucket, object.name),
+ })
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ if oi.DeleteMarker != object.addDeleteMarker {
+ t.Fatalf("Expected, marker %t : got %t", object.addDeleteMarker, oi.DeleteMarker)
+ }
+ }
+ }
+
+ // Formulating the result data set to be expected from ListObjects call inside the tests,
+ // This will be used in testCases and used for asserting the correctness of ListObjects output in the tests.
+
+ resultCases := []ListObjectsInfo{
+ {
+ IsTruncated: false,
+ Prefixes: []string{"unique/folder/"},
+ },
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "unique/folder/1.txt"},
+ },
+ },
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{},
+ },
+ }
+
+ resultCasesV := []ListObjectVersionsInfo{
+ {
+ IsTruncated: false,
+ Prefixes: []string{"unique/folder/"},
+ },
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {
+ Name: "unique/folder/",
+ DeleteMarker: true,
+ },
+ {
+ Name: "unique/folder/",
+ DeleteMarker: false,
+ },
+ {
+ Name: "unique/folder/1.txt",
+ DeleteMarker: false,
+ },
+ },
+ },
+ }
+
+ testCases := []struct {
+ // Inputs to ListObjects.
+ bucketName string
+ prefix string
+ marker string
+ delimiter string
+ maxKeys int
+ versioned bool
+ // Expected output of ListObjects.
+ resultL ListObjectsInfo
+ resultV ListObjectVersionsInfo
+ err error
+ // Flag indicating whether the test is expected to pass or not.
+ shouldPass bool
+ }{
+ {testBuckets[0], "unique/", "", "/", 1000, false, resultCases[0], ListObjectVersionsInfo{}, nil, true},
+ {testBuckets[0], "unique/folder", "", "/", 1000, false, resultCases[0], ListObjectVersionsInfo{}, nil, true},
+ {testBuckets[0], "unique/", "", "", 1000, false, resultCases[1], ListObjectVersionsInfo{}, nil, true},
+ {testBuckets[1], "unique/", "", "/", 1000, false, resultCases[0], ListObjectVersionsInfo{}, nil, true},
+ {testBuckets[1], "unique/folder/", "", "/", 1000, false, resultCases[2], ListObjectVersionsInfo{}, nil, true},
+ {testBuckets[0], "unique/", "", "/", 1000, true, ListObjectsInfo{}, resultCasesV[0], nil, true},
+ {testBuckets[0], "unique/", "", "", 1000, true, ListObjectsInfo{}, resultCasesV[1], nil, true},
+ }
+
+ for i, testCase := range testCases {
+ t.Run(fmt.Sprintf("%s-Test%d", instanceType, i+1), func(t *testing.T) {
+ var err error
+ var resultL ListObjectsInfo
+ var resultV ListObjectVersionsInfo
+ if testCase.versioned {
+ t.Log("ListObjectVersions, bucket:", testCase.bucketName, "prefix:",
+ testCase.prefix, "marker:", testCase.marker, "delimiter:",
+ testCase.delimiter, "maxkeys:", testCase.maxKeys)
+
+ resultV, err = obj.ListObjectVersions(t.Context(), testCase.bucketName,
+ testCase.prefix, testCase.marker, "", testCase.delimiter, testCase.maxKeys)
+ } else {
+ t.Log("ListObjects, bucket:", testCase.bucketName, "prefix:",
+ testCase.prefix, "marker:", testCase.marker, "delimiter:",
+ testCase.delimiter, "maxkeys:", testCase.maxKeys)
+
+ resultL, err = obj.ListObjects(t.Context(), testCase.bucketName,
+ testCase.prefix, testCase.marker, testCase.delimiter, testCase.maxKeys)
+ }
+ if err != nil && testCase.shouldPass {
+ t.Errorf("Test %d: %s: Expected to pass, but failed with: %s", i+1, instanceType, err.Error())
+ }
+ if err == nil && !testCase.shouldPass {
+ t.Errorf("Test %d: %s: Expected to fail with \"%s\", but passed instead", i+1, instanceType, testCase.err.Error())
+ }
+ // Failed as expected, but does it fail for the expected reason.
+ if err != nil && !testCase.shouldPass {
+ if !strings.Contains(err.Error(), testCase.err.Error()) {
+ t.Errorf("Test %d: %s: Expected to fail with error \"%s\", but instead failed with error \"%s\" instead", i+1, instanceType, testCase.err.Error(), err.Error())
+ }
+ }
+ // Since there are cases for which ListObjects fails, this is
+ // necessary. Test passes as expected, but the output values
+ // are verified for correctness here.
+ if err == nil && testCase.shouldPass {
+ // The length of the expected ListObjectsResult.Objects
+ // should match in both expected result from test cases
+ // and in the output. On failure calling t.Fatalf,
+ // otherwise it may lead to index out of range error in
+ // assertion following this.
+ if !testCase.versioned {
+ if len(testCase.resultL.Objects) != len(resultL.Objects) {
+ t.Logf("want: %v", objInfoNames(testCase.resultL.Objects))
+ t.Logf("got: %v", objInfoNames(resultL.Objects))
+ t.Errorf("Test %d: %s: Expected number of object in the result to be '%d', but found '%d' objects instead", i+1, instanceType, len(testCase.resultL.Objects), len(resultL.Objects))
+ }
+ for j := 0; j < len(testCase.resultL.Objects); j++ {
+ if j >= len(resultL.Objects) {
+ t.Errorf("Test %d: %s: Expected object name to be \"%s\", but not nothing instead", i+1, instanceType, testCase.resultL.Objects[j].Name)
+ continue
+ }
+ if testCase.resultL.Objects[j].Name != resultL.Objects[j].Name {
+ t.Errorf("Test %d: %s: Expected object name to be \"%s\", but found \"%s\" instead", i+1, instanceType, testCase.resultL.Objects[j].Name, resultL.Objects[j].Name)
+ }
+ }
+
+ if len(testCase.resultL.Prefixes) != len(resultL.Prefixes) {
+ t.Logf("want: %v", testCase.resultL.Prefixes)
+ t.Logf("got: %v", resultL.Prefixes)
+ t.Errorf("Test %d: %s: Expected number of prefixes in the result to be '%d', but found '%d' prefixes instead", i+1, instanceType, len(testCase.resultL.Prefixes), len(resultL.Prefixes))
+ }
+ for j := 0; j < len(testCase.resultL.Prefixes); j++ {
+ if j >= len(resultL.Prefixes) {
+ t.Errorf("Test %d: %s: Expected prefix name to be \"%s\", but found no result", i+1, instanceType, testCase.resultL.Prefixes[j])
+ continue
+ }
+ if testCase.resultL.Prefixes[j] != resultL.Prefixes[j] {
+ t.Errorf("Test %d: %s: Expected prefix name to be \"%s\", but found \"%s\" instead", i+1, instanceType, testCase.resultL.Prefixes[j], resultL.Prefixes[j])
+ }
+ }
+
+ if testCase.resultL.IsTruncated != resultL.IsTruncated {
+ // Allow an extra continuation token.
+ if !resultL.IsTruncated || len(resultL.Objects) == 0 {
+ t.Errorf("Test %d: %s: Expected IsTruncated flag to be %v, but instead found it to be %v", i+1, instanceType, testCase.resultL.IsTruncated, resultL.IsTruncated)
+ }
+ }
+
+ if testCase.resultL.IsTruncated && resultL.NextMarker == "" {
+ t.Errorf("Test %d: %s: Expected NextMarker to contain a string since listing is truncated, but instead found it to be empty", i+1, instanceType)
+ }
+
+ if !testCase.resultL.IsTruncated && resultL.NextMarker != "" {
+ if !resultL.IsTruncated || len(resultL.Objects) == 0 {
+ t.Errorf("Test %d: %s: Expected NextMarker to be empty since listing is not truncated, but instead found `%v`", i+1, instanceType, resultL.NextMarker)
+ }
+ }
+ } else {
+ if len(testCase.resultV.Objects) != len(resultV.Objects) {
+ t.Logf("want: %v", objInfoNames(testCase.resultV.Objects))
+ t.Logf("got: %v", objInfoNames(resultV.Objects))
+ t.Errorf("Test %d: %s: Expected number of object in the result to be '%d', but found '%d' objects instead", i+1, instanceType, len(testCase.resultV.Objects), len(resultV.Objects))
+ }
+ for j := 0; j < len(testCase.resultV.Objects); j++ {
+ if j >= len(resultV.Objects) {
+ t.Errorf("Test %d: %s: Expected object name to be \"%s\", but not nothing instead", i+1, instanceType, testCase.resultV.Objects[j].Name)
+ continue
+ }
+ if testCase.resultV.Objects[j].Name != resultV.Objects[j].Name {
+ t.Errorf("Test %d: %s: Expected object name to be \"%s\", but found \"%s\" instead", i+1, instanceType, testCase.resultV.Objects[j].Name, resultV.Objects[j].Name)
+ }
+ }
+
+ if len(testCase.resultV.Prefixes) != len(resultV.Prefixes) {
+ t.Logf("want: %v", testCase.resultV.Prefixes)
+ t.Logf("got: %v", resultV.Prefixes)
+ t.Errorf("Test %d: %s: Expected number of prefixes in the result to be '%d', but found '%d' prefixes instead", i+1, instanceType, len(testCase.resultV.Prefixes), len(resultV.Prefixes))
+ }
+ for j := 0; j < len(testCase.resultV.Prefixes); j++ {
+ if j >= len(resultV.Prefixes) {
+ t.Errorf("Test %d: %s: Expected prefix name to be \"%s\", but found no result", i+1, instanceType, testCase.resultV.Prefixes[j])
+ continue
+ }
+ if testCase.resultV.Prefixes[j] != resultV.Prefixes[j] {
+ t.Errorf("Test %d: %s: Expected prefix name to be \"%s\", but found \"%s\" instead", i+1, instanceType, testCase.resultV.Prefixes[j], resultV.Prefixes[j])
+ }
+ }
+
+ if testCase.resultV.IsTruncated != resultV.IsTruncated {
+ // Allow an extra continuation token.
+ if !resultV.IsTruncated || len(resultV.Objects) == 0 {
+ t.Errorf("Test %d: %s: Expected IsTruncated flag to be %v, but instead found it to be %v", i+1, instanceType, testCase.resultV.IsTruncated, resultV.IsTruncated)
+ }
+ }
+
+ if testCase.resultV.IsTruncated && resultV.NextMarker == "" {
+ t.Errorf("Test %d: %s: Expected NextMarker to contain a string since listing is truncated, but instead found it to be empty", i+1, instanceType)
+ }
+
+ if !testCase.resultV.IsTruncated && resultV.NextMarker != "" {
+ if !resultV.IsTruncated || len(resultV.Objects) == 0 {
+ t.Errorf("Test %d: %s: Expected NextMarker to be empty since listing is not truncated, but instead found `%v`", i+1, instanceType, resultV.NextMarker)
+ }
+ }
+ }
+ }
+ })
+ }
+}
+
+// Wrapper for calling ListObjectsOnVersionedBuckets tests for both
+// Erasure multiple disks and single node setup.
+func TestListObjectsOnVersionedBuckets(t *testing.T) {
+ ExecObjectLayerTest(t, testListObjectsOnVersionedBuckets)
+}
+
+// Wrapper for calling ListObjects tests for both Erasure multiple
+// disks and single node setup.
+func TestListObjects(t *testing.T) {
+ ExecObjectLayerTest(t, testListObjects)
+}
+
+// Unit test for ListObjects on VersionedBucket.
+func testListObjectsOnVersionedBuckets(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
+ _testListObjects(obj, instanceType, t1, true)
+}
+
+// Unit test for ListObjects.
+func testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
+ _testListObjects(obj, instanceType, t1, false)
+}
+
+func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, versioned bool) {
+ t, _ := t1.(*testing.T)
+ testBuckets := []string{
+ // This bucket is used for testing ListObject operations.
+ 0: "test-bucket-list-object",
+ // This bucket will be tested with empty directories
+ 1: "test-bucket-empty-dir",
+ // Will not store any objects in this bucket,
+ // Its to test ListObjects on an empty bucket.
+ 2: "empty-bucket",
+ // Listing the case where the marker > last object.
+ 3: "test-bucket-single-object",
+ // Listing uncommon delimiter.
+ 4: "test-bucket-delimiter",
+ // Listing prefixes > maxKeys
+ 5: "test-bucket-max-keys-prefixes",
+ // Listing custom delimiters
+ 6: "test-bucket-custom-delimiter",
+ }
+ for _, bucket := range testBuckets {
+ err := obj.MakeBucket(context.Background(), bucket, MakeBucketOptions{
+ VersioningEnabled: versioned,
+ })
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ }
+
+ var err error
+ testObjects := []struct {
+ parentBucket string
+ name string
+ content string
+ meta map[string]string
+ }{
+ {testBuckets[0], "Asia-maps.png", "asis-maps", map[string]string{"content-type": "image/png"}},
+ {testBuckets[0], "Asia/India/India-summer-photos-1", "contentstring", nil},
+ {testBuckets[0], "Asia/India/Karnataka/Bangalore/Koramangala/pics", "contentstring", nil},
+ {testBuckets[0], "newPrefix0", "newPrefix0", nil},
+ {testBuckets[0], "newPrefix1", "newPrefix1", nil},
+ {testBuckets[0], "newzen/zen/recurse/again/again/again/pics", "recurse", nil},
+ {testBuckets[0], "obj0", "obj0", nil},
+ {testBuckets[0], "obj1", "obj1", nil},
+ {testBuckets[0], "obj2", "obj2", nil},
+ {testBuckets[1], "obj1", "obj1", nil},
+ {testBuckets[1], "obj2", "obj2", nil},
+ {testBuckets[1], "temporary/0/", "", nil},
+ {testBuckets[3], "A/B", "contentstring", nil},
+ {testBuckets[4], "file1/receipt.json", "content", nil},
+ {testBuckets[4], "file1/guidSplunk-aaaa/file", "content", nil},
+ {testBuckets[5], "dir/day_id=2017-10-10/issue", "content", nil},
+ {testBuckets[5], "dir/day_id=2017-10-11/issue", "content", nil},
+ {testBuckets[5], "foo/201910/1122", "content", nil},
+ {testBuckets[5], "foo/201910/1112", "content", nil},
+ {testBuckets[5], "foo/201910/2112", "content", nil},
+ {testBuckets[5], "foo/201910_txt", "content", nil},
+ {testBuckets[5], "201910/foo/bar/xl.meta/1.txt", "content", nil},
+ {testBuckets[6], "aaa", "content", nil},
+ {testBuckets[6], "bbb_aaa", "content", nil},
+ {testBuckets[6], "bbb_aaa", "content", nil},
+ {testBuckets[6], "ccc", "content", nil},
+ }
+ for _, object := range testObjects {
+ md5Bytes := md5.Sum([]byte(object.content))
+ _, err = obj.PutObject(context.Background(), object.parentBucket, object.name,
+ mustGetPutObjReader(t, bytes.NewBufferString(object.content),
+ int64(len(object.content)), hex.EncodeToString(md5Bytes[:]), ""), ObjectOptions{
+ Versioned: globalBucketVersioningSys.PrefixEnabled(object.parentBucket, object.name),
+ UserDefined: object.meta,
+ })
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ }
+
+ // Formulating the result data set to be expected from ListObjects call inside the tests,
+ // This will be used in testCases and used for asserting the correctness of ListObjects output in the tests.
+
+ resultCases := []ListObjectsInfo{
+ // ListObjectsResult-0.
+ // Testing for listing all objects in the bucket, (testCase 20,21,22).
+ 0: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "Asia-maps.png"},
+ {Name: "Asia/India/India-summer-photos-1"},
+ {Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
+ {Name: "newPrefix0"},
+ {Name: "newPrefix1"},
+ {Name: "newzen/zen/recurse/again/again/again/pics"},
+ {Name: "obj0"},
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-1.
+ // Used for asserting the truncated case, (testCase 23).
+ 1: {
+ IsTruncated: true,
+ Objects: []ObjectInfo{
+ {Name: "Asia-maps.png"},
+ {Name: "Asia/India/India-summer-photos-1"},
+ {Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
+ {Name: "newPrefix0"},
+ {Name: "newPrefix1"},
+ },
+ },
+ // ListObjectsResult-2.
+ // (TestCase 24).
+ 2: {
+ IsTruncated: true,
+ Objects: []ObjectInfo{
+ {Name: "Asia-maps.png"},
+ {Name: "Asia/India/India-summer-photos-1"},
+ {Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
+ {Name: "newPrefix0"},
+ },
+ },
+ // ListObjectsResult-3.
+ // (TestCase 25).
+ 3: {
+ IsTruncated: true,
+ Objects: []ObjectInfo{
+ {Name: "Asia-maps.png"},
+ {Name: "Asia/India/India-summer-photos-1"},
+ {Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
+ },
+ },
+ // ListObjectsResult-4.
+ // Again used for truncated case.
+ // (TestCase 26).
+ 4: {
+ IsTruncated: true,
+ Objects: []ObjectInfo{
+ {Name: "Asia-maps.png"},
+ },
+ },
+ // ListObjectsResult-5.
+ // Used for Asserting prefixes.
+ // Used for test case with prefix "new", (testCase 27-29).
+ 5: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "newPrefix0"},
+ {Name: "newPrefix1"},
+ {Name: "newzen/zen/recurse/again/again/again/pics"},
+ },
+ },
+ // ListObjectsResult-6.
+ // Used for Asserting prefixes.
+ // Used for test case with prefix = "obj", (testCase 30).
+ 6: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "obj0"},
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-7.
+ // Used for Asserting prefixes and truncation.
+ // Used for test case with prefix = "new" and maxKeys = 1, (testCase 31).
+ 7: {
+ IsTruncated: true,
+ Objects: []ObjectInfo{
+ {Name: "newPrefix0"},
+ },
+ },
+ // ListObjectsResult-8.
+ // Used for Asserting prefixes.
+ // Used for test case with prefix = "obj" and maxKeys = 2, (testCase 32).
+ 8: {
+ IsTruncated: true,
+ Objects: []ObjectInfo{
+ {Name: "obj0"},
+ {Name: "obj1"},
+ },
+ },
+ // ListObjectsResult-9.
+ // Used for asserting the case with marker, but without prefix.
+ // marker is set to "newPrefix0" in the testCase, (testCase 33).
+ 9: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "newPrefix1"},
+ {Name: "newzen/zen/recurse/again/again/again/pics"},
+ {Name: "obj0"},
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-10.
+ // marker is set to "newPrefix1" in the testCase, (testCase 34).
+ 10: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "newzen/zen/recurse/again/again/again/pics"},
+ {Name: "obj0"},
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-11.
+ // marker is set to "obj0" in the testCase, (testCase 35).
+ 11: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-12.
+ // Marker is set to "obj1" in the testCase, (testCase 36).
+ 12: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-13.
+ // Marker is set to "man" in the testCase, (testCase37).
+ 13: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "newPrefix0"},
+ {Name: "newPrefix1"},
+ {Name: "newzen/zen/recurse/again/again/again/pics"},
+ {Name: "obj0"},
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-14.
+ // Marker is set to "Abc" in the testCase, (testCase 39).
+ 14: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "Asia-maps.png"},
+ {Name: "Asia/India/India-summer-photos-1"},
+ {Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
+ {Name: "newPrefix0"},
+ {Name: "newPrefix1"},
+ {Name: "newzen/zen/recurse/again/again/again/pics"},
+ {Name: "obj0"},
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-15.
+ // Marker is set to "Asia/India/India-summer-photos-1" in the testCase, (testCase 40).
+ 15: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
+ {Name: "newPrefix0"},
+ {Name: "newPrefix1"},
+ {Name: "newzen/zen/recurse/again/again/again/pics"},
+ {Name: "obj0"},
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-16.
+ // Marker is set to "Asia/India/Karnataka/Bangalore/Koramangala/pics" in the testCase, (testCase 41).
+ 16: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "newPrefix0"},
+ {Name: "newPrefix1"},
+ {Name: "newzen/zen/recurse/again/again/again/pics"},
+ {Name: "obj0"},
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-17.
+ // Used for asserting the case with marker, without prefix but with truncation.
+ // Marker = "newPrefix0" & maxKeys = 3 in the testCase, (testCase42).
+ // Output truncated to 3 values.
+ 17: {
+ IsTruncated: true,
+ Objects: []ObjectInfo{
+ {Name: "newPrefix1"},
+ {Name: "newzen/zen/recurse/again/again/again/pics"},
+ {Name: "obj0"},
+ },
+ },
+ // ListObjectsResult-18.
+ // Marker = "newPrefix1" & maxkeys = 1 in the testCase, (testCase43).
+ // Output truncated to 1 value.
+ 18: {
+ IsTruncated: true,
+ Objects: []ObjectInfo{
+ {Name: "newzen/zen/recurse/again/again/again/pics"},
+ },
+ },
+ // ListObjectsResult-19.
+ // Marker = "obj0" & maxKeys = 1 in the testCase, (testCase44).
+ // Output truncated to 1 value.
+ 19: {
+ IsTruncated: true,
+ Objects: []ObjectInfo{
+ {Name: "obj1"},
+ },
+ },
+ // ListObjectsResult-20.
+ // Marker = "obj0" & prefix = "obj" in the testCase, (testCase 45).
+ 20: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-21.
+ // Marker = "obj1" & prefix = "obj" in the testCase, (testCase 46).
+ 21: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-22.
+ // Marker = "newPrefix0" & prefix = "new" in the testCase,, (testCase 47).
+ 22: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "newPrefix1"},
+ {Name: "newzen/zen/recurse/again/again/again/pics"},
+ },
+ },
+ // ListObjectsResult-23.
+ // Prefix is set to "Asia/India/" in the testCase, and delimiter is not set (testCase 55).
+ 23: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "Asia/India/India-summer-photos-1"},
+ {Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
+ },
+ },
+
+ // ListObjectsResult-24.
+ // Prefix is set to "Asia" in the testCase, and delimiter is not set (testCase 56).
+ 24: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "Asia-maps.png"},
+ {Name: "Asia/India/India-summer-photos-1"},
+ {Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
+ },
+ },
+
+ // ListObjectsResult-25.
+ // Prefix is set to "Asia" in the testCase, and delimiter is set (testCase 57).
+ 25: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "Asia-maps.png"},
+ },
+ Prefixes: []string{"Asia/"},
+ },
+ // ListObjectsResult-26.
+ // prefix = "new" and delimiter is set in the testCase.(testCase 58).
+ 26: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "newPrefix0"},
+ {Name: "newPrefix1"},
+ },
+ Prefixes: []string{"newzen/"},
+ },
+ // ListObjectsResult-27.
+ // Prefix is set to "Asia/India/" in the testCase, and delimiter is set to forward slash '/' (testCase 59).
+ 27: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "Asia/India/India-summer-photos-1"},
+ },
+ Prefixes: []string{"Asia/India/Karnataka/"},
+ },
+ // ListObjectsResult-28.
+ // Marker is set to "Asia/India/India-summer-photos-1" and delimiter set in the testCase, (testCase 60).
+ 28: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "newPrefix0"},
+ {Name: "newPrefix1"},
+ {Name: "obj0"},
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ Prefixes: []string{"newzen/"},
+ },
+ // ListObjectsResult-29.
+ // Marker is set to "Asia/India/Karnataka/Bangalore/Koramangala/pics" in the testCase and delimiter set, (testCase 61).
+ 29: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "newPrefix0"},
+ {Name: "newPrefix1"},
+ {Name: "obj0"},
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ Prefixes: []string{"newzen/"},
+ },
+ // ListObjectsResult-30.
+ // Prefix and Delimiter is set to '/', (testCase 62).
+ 30: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{},
+ },
+ // ListObjectsResult-31 Empty directory, recursive listing
+ 31: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "obj1"},
+ {Name: "obj2"},
+ {Name: "temporary/0/"},
+ },
+ },
+ // ListObjectsResult-32 Empty directory, non recursive listing
+ 32: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ Prefixes: []string{"temporary/"},
+ },
+ // ListObjectsResult-33 Listing empty directory only
+ 33: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "temporary/0/"},
+ },
+ },
+ // ListObjectsResult-34:
+ // * Listing with marker > last object should return empty
+ // * Listing an object with a trailing slash and '/' delimiter
+ 34: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{},
+ },
+ // ListObjectsResult-35 list with custom uncommon delimiter
+ 35: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "file1/receipt.json"},
+ },
+ Prefixes: []string{"file1/guidSplunk"},
+ },
+ // ListObjectsResult-36 list with nextmarker prefix and maxKeys set to 1.
+ 36: {
+ IsTruncated: true,
+ Prefixes: []string{"dir/day_id=2017-10-10/"},
+ },
+ // ListObjectsResult-37 list with prefix match 2 levels deep
+ 37: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "foo/201910/1112"},
+ {Name: "foo/201910/1122"},
+ },
+ },
+ // ListObjectsResult-38 list with prefix match 1 level deep
+ 38: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "foo/201910/1112"},
+ {Name: "foo/201910/1122"},
+ {Name: "foo/201910/2112"},
+ {Name: "foo/201910_txt"},
+ },
+ },
+ // ListObjectsResult-39 list with prefix match 1 level deep
+ 39: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "201910/foo/bar/xl.meta/1.txt"},
+ },
+ },
+ // ListObjectsResult-40
+ 40: {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "aaa"},
+ {Name: "ccc"},
+ },
+ Prefixes: []string{"bbb_"},
+ },
+ }
+
+ testCases := []struct {
+ // Inputs to ListObjects.
+ bucketName string
+ prefix string
+ marker string
+ delimiter string
+ maxKeys int32
+ // Expected output of ListObjects.
+ result ListObjectsInfo
+ err error
+ // Flag indicating whether the test is expected to pass or not.
+ shouldPass bool
+ }{
+ // Test cases with invalid bucket names ( Test number 1-4 ).
+ {".test", "", "", "", 0, ListObjectsInfo{}, BucketNameInvalid{Bucket: ".test"}, false},
+ {"Test", "", "", "", 0, ListObjectsInfo{}, BucketNameInvalid{Bucket: "Test"}, false},
+ {"---", "", "", "", 0, ListObjectsInfo{}, BucketNameInvalid{Bucket: "---"}, false},
+ {"ad", "", "", "", 0, ListObjectsInfo{}, BucketNameInvalid{Bucket: "ad"}, false},
+ // Valid bucket names, but they do not exist (6-8).
+ {"volatile-bucket-1", "", "", "", 1000, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-1"}, false},
+ {"volatile-bucket-2", "", "", "", 1000, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-2"}, false},
+ {"volatile-bucket-3", "", "", "", 1000, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-3"}, false},
+ // If marker is *after* the last possible object from the prefix it should return an empty list.
+ {"test-bucket-list-object", "Asia", "europe-object", "", 0, ListObjectsInfo{}, nil, true},
+ // If the marker is *before* the first possible object from the prefix it should return the first object.
+ {"test-bucket-list-object", "Asia", "A", "", 1, resultCases[4], nil, true},
+ // Setting a non-existing directory to be prefix (12-13).
+ {"empty-bucket", "europe/france/", "", "", 1, ListObjectsInfo{}, nil, true},
+ {"empty-bucket", "africa/tunisia/", "", "", 1, ListObjectsInfo{}, nil, true},
+ // Testing on empty bucket, that is, bucket without any objects in it (14).
+ {"empty-bucket", "", "", "", 0, ListObjectsInfo{}, nil, true},
+ // Setting maxKeys to negative value (15-16).
+ {"empty-bucket", "", "", "", -1, ListObjectsInfo{}, nil, true},
+ {"empty-bucket", "", "", "", 1, ListObjectsInfo{}, nil, true},
+ // Setting maxKeys to a very large value (17).
+ {"empty-bucket", "", "", "", 111100000, ListObjectsInfo{}, nil, true},
+ // Testing for all 10 objects in the bucket (18).
+ {"test-bucket-list-object", "", "", "", 10, resultCases[0], nil, true},
+ // Testing for negative value of maxKey, this should set maxKeys to listObjectsLimit (19).
+ {"test-bucket-list-object", "", "", "", -1, resultCases[0], nil, true},
+ // Testing for very large value of maxKey, this should set maxKeys to listObjectsLimit (20).
+ {"test-bucket-list-object", "", "", "", 1234567890, resultCases[0], nil, true},
+ // Testing for truncated value (21-24).
+ {"test-bucket-list-object", "", "", "", 5, resultCases[1], nil, true},
+ {"test-bucket-list-object", "", "", "", 4, resultCases[2], nil, true},
+ {"test-bucket-list-object", "", "", "", 3, resultCases[3], nil, true},
+ {"test-bucket-list-object", "", "", "", 1, resultCases[4], nil, true},
+ // Testing with prefix (25-28).
+ {"test-bucket-list-object", "new", "", "", 3, resultCases[5], nil, true},
+ {"test-bucket-list-object", "new", "", "", 4, resultCases[5], nil, true},
+ {"test-bucket-list-object", "new", "", "", 5, resultCases[5], nil, true},
+ {"test-bucket-list-object", "obj", "", "", 3, resultCases[6], nil, true},
+ {"test-bucket-list-object", "/obj", "", "", 0, ListObjectsInfo{}, nil, true},
+ // Testing with prefix and truncation (29-30).
+ {"test-bucket-list-object", "new", "", "", 1, resultCases[7], nil, true},
+ {"test-bucket-list-object", "obj", "", "", 2, resultCases[8], nil, true},
+ // Testing with marker, but without prefix and truncation (31-35).
+ {"test-bucket-list-object", "", "newPrefix0", "", 6, resultCases[9], nil, true},
+ {"test-bucket-list-object", "", "newPrefix1", "", 5, resultCases[10], nil, true},
+ {"test-bucket-list-object", "", "obj0", "", 4, resultCases[11], nil, true},
+ {"test-bucket-list-object", "", "obj1", "", 2, resultCases[12], nil, true},
+ {"test-bucket-list-object", "", "man", "", 11, resultCases[13], nil, true},
+ // Marker being set to a value which is greater than and all object names when sorted (36).
+ // Expected to send an empty response in this case.
+ {"test-bucket-list-object", "", "zen", "", 10, ListObjectsInfo{}, nil, true},
+ // Marker being set to a value which is lesser than and all object names when sorted (37).
+ // Expected to send all the objects in the bucket in this case.
+ {"test-bucket-list-object", "", "Abc", "", 10, resultCases[14], nil, true},
+ // Marker is to a hierarchical value (38-39).
+ {"test-bucket-list-object", "", "Asia/India/India-summer-photos-1", "", 10, resultCases[15], nil, true},
+ {"test-bucket-list-object", "", "Asia/India/Karnataka/Bangalore/Koramangala/pics", "", 10, resultCases[16], nil, true},
+ // Testing with marker and truncation, but no prefix (40-42).
+ {"test-bucket-list-object", "", "newPrefix0", "", 3, resultCases[17], nil, true},
+ {"test-bucket-list-object", "", "newPrefix1", "", 1, resultCases[18], nil, true},
+ {"test-bucket-list-object", "", "obj0", "", 1, resultCases[19], nil, true},
+ // Testing with both marker and prefix, but without truncation (43-45).
+ // The valid combination of marker and prefix should satisfy strings.HasPrefix(marker, prefix).
+ {"test-bucket-list-object", "obj", "obj0", "", 2, resultCases[20], nil, true},
+ {"test-bucket-list-object", "obj", "obj1", "", 1, resultCases[21], nil, true},
+ {"test-bucket-list-object", "new", "newPrefix0", "", 2, resultCases[22], nil, true},
+ // Testing with maxKeys set to 0 (46-52).
+ // The parameters have to valid.
+ {"test-bucket-list-object", "", "obj1", "", 0, ListObjectsInfo{}, nil, true},
+ {"test-bucket-list-object", "", "obj0", "", 0, ListObjectsInfo{}, nil, true},
+ {"test-bucket-list-object", "new", "", "", 0, ListObjectsInfo{}, nil, true},
+ {"test-bucket-list-object", "obj", "", "", 0, ListObjectsInfo{}, nil, true},
+ {"test-bucket-list-object", "obj", "obj0", "", 0, ListObjectsInfo{}, nil, true},
+ {"test-bucket-list-object", "obj", "obj1", "", 0, ListObjectsInfo{}, nil, true},
+ {"test-bucket-list-object", "new", "newPrefix0", "", 0, ListObjectsInfo{}, nil, true},
+ // Tests on hierarchical key names as prefix.
+ // Without delimteter the code should recurse into the prefix Dir.
+ // Tests with prefix, but without delimiter (53-54).
+ {"test-bucket-list-object", "Asia/India/", "", "", 10, resultCases[23], nil, true},
+ {"test-bucket-list-object", "Asia", "", "", 10, resultCases[24], nil, true},
+ // Tests with prefix and delimiter (55-57).
+ // With delimiter the code should not recurse into the sub-directories of prefix Dir.
+ {"test-bucket-list-object", "Asia", "", SlashSeparator, 10, resultCases[25], nil, true},
+ {"test-bucket-list-object", "new", "", SlashSeparator, 10, resultCases[26], nil, true},
+ {"test-bucket-list-object", "Asia/India/", "", SlashSeparator, 10, resultCases[27], nil, true},
+ // Test with marker set as hierarchical value and with delimiter. (58-59)
+ {"test-bucket-list-object", "", "Asia/India/India-summer-photos-1", SlashSeparator, 10, resultCases[28], nil, true},
+ {"test-bucket-list-object", "", "Asia/India/Karnataka/Bangalore/Koramangala/pics", SlashSeparator, 10, resultCases[29], nil, true},
+ // Test with prefix and delimiter set to '/'. (60)
+ {"test-bucket-list-object", SlashSeparator, "", SlashSeparator, 10, resultCases[30], nil, true},
+ // Test with invalid prefix (61)
+ {"test-bucket-list-object", "\\", "", SlashSeparator, 10, ListObjectsInfo{}, nil, true},
+ // Test listing an empty directory in recursive mode (62)
+ {"test-bucket-empty-dir", "", "", "", 10, resultCases[31], nil, true},
+ // Test listing an empty directory in a non recursive mode (63)
+ {"test-bucket-empty-dir", "", "", SlashSeparator, 10, resultCases[32], nil, true},
+ // Test listing a directory which contains an empty directory (64)
+ {"test-bucket-empty-dir", "", "temporary/", "", 10, resultCases[33], nil, true},
+ // Test listing with marker > last object such that response should be empty (65)
+ {"test-bucket-single-object", "", "A/C", "", 1000, resultCases[34], nil, true},
+ // Test listing an object with a trailing slash and a slash delimiter (66)
+ {"test-bucket-list-object", "Asia-maps.png/", "", "/", 1000, resultCases[34], nil, true},
+ // Test listing an object with uncommon delimiter
+ {testBuckets[4], "", "", "guidSplunk", 1000, resultCases[35], nil, true},
+ // Test listing an object with uncommon delimiter and matching prefix
+ {testBuckets[4], "file1/", "", "guidSplunk", 1000, resultCases[35], nil, true},
+ // Test listing at prefix with expected prefix markers
+ {testBuckets[5], "dir/", "", SlashSeparator, 1, resultCases[36], nil, true},
+ // Test listing with prefix match
+ {testBuckets[5], "foo/201910/11", "", "", 1000, resultCases[37], nil, true},
+ {testBuckets[5], "foo/201910", "", "", 1000, resultCases[38], nil, true},
+ // Test listing with prefix match with 'xl.meta'
+ {testBuckets[5], "201910/foo/bar", "", "", 1000, resultCases[39], nil, true},
+ // Test listing with custom prefix
+ {testBuckets[6], "", "", "_", 1000, resultCases[40], nil, true},
+ }
+
+ for i, testCase := range testCases {
+ t.Run(fmt.Sprintf("%s-Test%d", instanceType, i+1), func(t *testing.T) {
+ t.Log("ListObjects, bucket:", testCase.bucketName, "prefix:", testCase.prefix, "marker:", testCase.marker, "delimiter:", testCase.delimiter, "maxkeys:", testCase.maxKeys)
+ result, err := obj.ListObjects(t.Context(), testCase.bucketName,
+ testCase.prefix, testCase.marker, testCase.delimiter, int(testCase.maxKeys))
+ if err != nil && testCase.shouldPass {
+ t.Errorf("Test %d: %s: Expected to pass, but failed with: %s", i+1, instanceType, err.Error())
+ }
+ if err == nil && !testCase.shouldPass {
+ t.Errorf("Test %d: %s: Expected to fail with \"%s\", but passed instead", i+1, instanceType, testCase.err.Error())
+ }
+ // Failed as expected, but does it fail for the expected reason.
+ if err != nil && !testCase.shouldPass {
+ if !strings.Contains(err.Error(), testCase.err.Error()) {
+ t.Errorf("Test %d: %s: Expected to fail with error \"%s\", but instead failed with error \"%s\" instead", i+1, instanceType, testCase.err.Error(), err.Error())
+ }
+ }
+ // Since there are cases for which ListObjects fails, this is
+ // necessary. Test passes as expected, but the output values
+ // are verified for correctness here.
+ if err == nil && testCase.shouldPass {
+ // The length of the expected ListObjectsResult.Objects
+ // should match in both expected result from test cases
+ // and in the output. On failure calling t.Fatalf,
+ // otherwise it may lead to index out of range error in
+ // assertion following this.
+ if len(testCase.result.Objects) != len(result.Objects) {
+ t.Logf("want: %v", objInfoNames(testCase.result.Objects))
+ t.Logf("got: %v", objInfoNames(result.Objects))
+ t.Errorf("Test %d: %s: Expected number of object in the result to be '%d', but found '%d' objects instead", i+1, instanceType, len(testCase.result.Objects), len(result.Objects))
+ }
+ for j := 0; j < len(testCase.result.Objects); j++ {
+ if j >= len(result.Objects) {
+ t.Errorf("Test %d: %s: Expected object name to be \"%s\", but not nothing instead", i+1, instanceType, testCase.result.Objects[j].Name)
+ continue
+ }
+ if testCase.result.Objects[j].Name != result.Objects[j].Name {
+ t.Errorf("Test %d: %s: Expected object name to be \"%s\", but found \"%s\" instead", i+1, instanceType, testCase.result.Objects[j].Name, result.Objects[j].Name)
+ }
+ }
+
+ if len(testCase.result.Prefixes) != len(result.Prefixes) {
+ t.Logf("want: %v", testCase.result.Prefixes)
+ t.Logf("got: %v", result.Prefixes)
+ t.Errorf("Test %d: %s: Expected number of prefixes in the result to be '%d', but found '%d' prefixes instead", i+1, instanceType, len(testCase.result.Prefixes), len(result.Prefixes))
+ }
+ for j := 0; j < len(testCase.result.Prefixes); j++ {
+ if j >= len(result.Prefixes) {
+ t.Errorf("Test %d: %s: Expected prefix name to be \"%s\", but found no result", i+1, instanceType, testCase.result.Prefixes[j])
+ continue
+ }
+ if testCase.result.Prefixes[j] != result.Prefixes[j] {
+ t.Errorf("Test %d: %s: Expected prefix name to be \"%s\", but found \"%s\" instead", i+1, instanceType, testCase.result.Prefixes[j], result.Prefixes[j])
+ }
+ }
+
+ if testCase.result.IsTruncated != result.IsTruncated {
+ // Allow an extra continuation token.
+ if !result.IsTruncated || len(result.Objects) == 0 {
+ t.Errorf("Test %d: %s: Expected IsTruncated flag to be %v, but instead found it to be %v", i+1, instanceType, testCase.result.IsTruncated, result.IsTruncated)
+ }
+ }
+
+ if testCase.result.IsTruncated && result.NextMarker == "" {
+ t.Errorf("Test %d: %s: Expected NextMarker to contain a string since listing is truncated, but instead found it to be empty", i+1, instanceType)
+ }
+
+ if !testCase.result.IsTruncated && result.NextMarker != "" {
+ if !result.IsTruncated || len(result.Objects) == 0 {
+ t.Errorf("Test %d: %s: Expected NextMarker to be empty since listing is not truncated, but instead found `%v`", i+1, instanceType, result.NextMarker)
+ }
+ }
+ }
+ })
+ }
+}
+
+func objInfoNames(o []ObjectInfo) []string {
+ res := make([]string, len(o))
+ for i := range o {
+ res[i] = o[i].Name
+ }
+ return res
+}
+
+func TestDeleteObjectVersionMarker(t *testing.T) {
+ ExecObjectLayerTest(t, testDeleteObjectVersion)
+}
+
+func testDeleteObjectVersion(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
+ t, _ := t1.(*testing.T)
+
+ testBuckets := []string{
+ "bucket-suspended-version",
+ "bucket-suspended-version-id",
+ }
+ for _, bucket := range testBuckets {
+ err := obj.MakeBucket(context.Background(), bucket, MakeBucketOptions{
+ VersioningEnabled: true,
+ })
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err)
+ }
+ meta, err := loadBucketMetadata(context.Background(), obj, bucket)
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err)
+ }
+ meta.VersioningConfigXML = []byte(`Suspended`)
+ if err := meta.Save(context.Background(), obj); err != nil {
+ t.Fatalf("%s : %s", instanceType, err)
+ }
+ globalBucketMetadataSys.Set(bucket, meta)
+ globalNotificationSys.LoadBucketMetadata(context.Background(), bucket)
+ }
+
+ testObjects := []struct {
+ parentBucket string
+ name string
+ content string
+ meta map[string]string
+ versionID string
+ expectDelMarker bool
+ }{
+ {testBuckets[0], "delete-file", "contentstring", nil, "", true},
+ {testBuckets[1], "delete-file", "contentstring", nil, "null", false},
+ }
+ for _, object := range testObjects {
+ md5Bytes := md5.Sum([]byte(object.content))
+ _, err := obj.PutObject(context.Background(), object.parentBucket, object.name,
+ mustGetPutObjReader(t, bytes.NewBufferString(object.content),
+ int64(len(object.content)), hex.EncodeToString(md5Bytes[:]), ""), ObjectOptions{
+ Versioned: globalBucketVersioningSys.PrefixEnabled(object.parentBucket, object.name),
+ VersionSuspended: globalBucketVersioningSys.PrefixSuspended(object.parentBucket, object.name),
+ UserDefined: object.meta,
+ })
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err)
+ }
+ obj, err := obj.DeleteObject(context.Background(), object.parentBucket, object.name, ObjectOptions{
+ Versioned: globalBucketVersioningSys.PrefixEnabled(object.parentBucket, object.name),
+ VersionSuspended: globalBucketVersioningSys.PrefixSuspended(object.parentBucket, object.name),
+ VersionID: object.versionID,
+ })
+ if err != nil {
+ if object.versionID != "" {
+ if !isErrVersionNotFound(err) {
+ t.Fatalf("%s : %s", instanceType, err)
+ }
+ } else {
+ if !isErrObjectNotFound(err) {
+ t.Fatalf("%s : %s", instanceType, err)
+ }
+ }
+ }
+ if obj.DeleteMarker != object.expectDelMarker {
+ t.Fatalf("%s : expected deleted marker %t, found %t", instanceType, object.expectDelMarker, obj.DeleteMarker)
+ }
+ }
+}
+
+// Wrapper for calling ListObjectVersions tests for both Erasure multiple disks and single node setup.
+func TestListObjectVersions(t *testing.T) {
+ ExecObjectLayerTest(t, testListObjectVersions)
+}
+
+// Unit test for ListObjectVersions
+func testListObjectVersions(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
+ t, _ := t1.(*testing.T)
+ testBuckets := []string{
+ // This bucket is used for testing ListObject operations.
+ "test-bucket-list-object",
+ // This bucket will be tested with empty directories
+ "test-bucket-empty-dir",
+ // Will not store any objects in this bucket,
+ // Its to test ListObjects on an empty bucket.
+ "empty-bucket",
+ // Listing the case where the marker > last object.
+ "test-bucket-single-object",
+ // Listing uncommon delimiter.
+ "test-bucket-delimiter",
+ // Listing prefixes > maxKeys
+ "test-bucket-max-keys-prefixes",
+ }
+ for _, bucket := range testBuckets {
+ err := obj.MakeBucket(context.Background(), bucket, MakeBucketOptions{VersioningEnabled: true})
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ }
+
+ var err error
+ testObjects := []struct {
+ parentBucket string
+ name string
+ content string
+ meta map[string]string
+ }{
+ {testBuckets[0], "Asia-maps.png", "asis-maps", map[string]string{"content-type": "image/png"}},
+ {testBuckets[0], "Asia/India/India-summer-photos-1", "contentstring", nil},
+ {testBuckets[0], "Asia/India/Karnataka/Bangalore/Koramangala/pics", "contentstring", nil},
+ {testBuckets[0], "newPrefix0", "newPrefix0", nil},
+ {testBuckets[0], "newPrefix1", "newPrefix1", nil},
+ {testBuckets[0], "newzen/zen/recurse/again/again/again/pics", "recurse", nil},
+ {testBuckets[0], "obj0", "obj0", nil},
+ {testBuckets[0], "obj1", "obj1", nil},
+ {testBuckets[0], "obj2", "obj2", nil},
+ {testBuckets[1], "obj1", "obj1", nil},
+ {testBuckets[1], "obj2", "obj2", nil},
+ {testBuckets[1], "temporary/0/", "", nil},
+ {testBuckets[3], "A/B", "contentstring", nil},
+ {testBuckets[4], "file1/receipt.json", "content", nil},
+ {testBuckets[4], "file1/guidSplunk-aaaa/file", "content", nil},
+ {testBuckets[5], "dir/day_id=2017-10-10/issue", "content", nil},
+ {testBuckets[5], "dir/day_id=2017-10-11/issue", "content", nil},
+ }
+
+ for _, object := range testObjects {
+ md5Bytes := md5.Sum([]byte(object.content))
+ _, err = obj.PutObject(context.Background(), object.parentBucket, object.name, mustGetPutObjReader(t, bytes.NewBufferString(object.content),
+ int64(len(object.content)), hex.EncodeToString(md5Bytes[:]), ""), ObjectOptions{UserDefined: object.meta})
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ }
+
+ // Formulating the result data set to be expected from ListObjects call inside the tests,
+ // This will be used in testCases and used for asserting the correctness of ListObjects output in the tests.
+
+ resultCases := []ListObjectsInfo{
+ // ListObjectsResult-0.
+ // Testing for listing all objects in the bucket, (testCase 20,21,22).
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "Asia-maps.png"},
+ {Name: "Asia/India/India-summer-photos-1"},
+ {Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
+ {Name: "newPrefix0"},
+ {Name: "newPrefix1"},
+ {Name: "newzen/zen/recurse/again/again/again/pics"},
+ {Name: "obj0"},
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-1.
+ // Used for asserting the truncated case, (testCase 23).
+ {
+ IsTruncated: true,
+ Objects: []ObjectInfo{
+ {Name: "Asia-maps.png"},
+ {Name: "Asia/India/India-summer-photos-1"},
+ {Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
+ {Name: "newPrefix0"},
+ {Name: "newPrefix1"},
+ },
+ },
+ // ListObjectsResult-2.
+ // (TestCase 24).
+ {
+ IsTruncated: true,
+ Objects: []ObjectInfo{
+ {Name: "Asia-maps.png"},
+ {Name: "Asia/India/India-summer-photos-1"},
+ {Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
+ {Name: "newPrefix0"},
+ },
+ },
+ // ListObjectsResult-3.
+ // (TestCase 25).
+ {
+ IsTruncated: true,
+ Objects: []ObjectInfo{
+ {Name: "Asia-maps.png"},
+ {Name: "Asia/India/India-summer-photos-1"},
+ {Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
+ },
+ },
+ // ListObjectsResult-4.
+ // Again used for truncated case.
+ // (TestCase 26).
+ {
+ IsTruncated: true,
+ Objects: []ObjectInfo{
+ {Name: "Asia-maps.png"},
+ },
+ },
+ // ListObjectsResult-5.
+ // Used for Asserting prefixes.
+ // Used for test case with prefix "new", (testCase 27-29).
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "newPrefix0"},
+ {Name: "newPrefix1"},
+ {Name: "newzen/zen/recurse/again/again/again/pics"},
+ },
+ },
+ // ListObjectsResult-6.
+ // Used for Asserting prefixes.
+ // Used for test case with prefix = "obj", (testCase 30).
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "obj0"},
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-7.
+ // Used for Asserting prefixes and truncation.
+ // Used for test case with prefix = "new" and maxKeys = 1, (testCase 31).
+ {
+ IsTruncated: true,
+ Objects: []ObjectInfo{
+ {Name: "newPrefix0"},
+ },
+ },
+ // ListObjectsResult-8.
+ // Used for Asserting prefixes.
+ // Used for test case with prefix = "obj" and maxKeys = 2, (testCase 32).
+ {
+ IsTruncated: true,
+ Objects: []ObjectInfo{
+ {Name: "obj0"},
+ {Name: "obj1"},
+ },
+ },
+ // ListObjectsResult-9.
+ // Used for asserting the case with marker, but without prefix.
+ // marker is set to "newPrefix0" in the testCase, (testCase 33).
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "newPrefix1"},
+ {Name: "newzen/zen/recurse/again/again/again/pics"},
+ {Name: "obj0"},
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-10.
+ // marker is set to "newPrefix1" in the testCase, (testCase 34).
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "newzen/zen/recurse/again/again/again/pics"},
+ {Name: "obj0"},
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-11.
+ // marker is set to "obj0" in the testCase, (testCase 35).
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-12.
+ // Marker is set to "obj1" in the testCase, (testCase 36).
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-13.
+ // Marker is set to "man" in the testCase, (testCase37).
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "newPrefix0"},
+ {Name: "newPrefix1"},
+ {Name: "newzen/zen/recurse/again/again/again/pics"},
+ {Name: "obj0"},
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-14.
+ // Marker is set to "Abc" in the testCase, (testCase 39).
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "Asia-maps.png"},
+ {Name: "Asia/India/India-summer-photos-1"},
+ {Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
+ {Name: "newPrefix0"},
+ {Name: "newPrefix1"},
+ {Name: "newzen/zen/recurse/again/again/again/pics"},
+ {Name: "obj0"},
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-15.
+ // Marker is set to "Asia/India/India-summer-photos-1" in the testCase, (testCase 40).
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
+ {Name: "newPrefix0"},
+ {Name: "newPrefix1"},
+ {Name: "newzen/zen/recurse/again/again/again/pics"},
+ {Name: "obj0"},
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-16.
+ // Marker is set to "Asia/India/Karnataka/Bangalore/Koramangala/pics" in the testCase, (testCase 41).
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "newPrefix0"},
+ {Name: "newPrefix1"},
+ {Name: "newzen/zen/recurse/again/again/again/pics"},
+ {Name: "obj0"},
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-17.
+ // Used for asserting the case with marker, without prefix but with truncation.
+ // Marker = "newPrefix0" & maxKeys = 3 in the testCase, (testCase42).
+ // Output truncated to 3 values.
+ {
+ IsTruncated: true,
+ Objects: []ObjectInfo{
+ {Name: "newPrefix1"},
+ {Name: "newzen/zen/recurse/again/again/again/pics"},
+ {Name: "obj0"},
+ },
+ },
+ // ListObjectsResult-18.
+ // Marker = "newPrefix1" & maxkeys = 1 in the testCase, (testCase43).
+ // Output truncated to 1 value.
+ {
+ IsTruncated: true,
+ Objects: []ObjectInfo{
+ {Name: "newzen/zen/recurse/again/again/again/pics"},
+ },
+ },
+ // ListObjectsResult-19.
+ // Marker = "obj0" & maxKeys = 1 in the testCase, (testCase44).
+ // Output truncated to 1 value.
+ {
+ IsTruncated: true,
+ Objects: []ObjectInfo{
+ {Name: "obj1"},
+ },
+ },
+ // ListObjectsResult-20.
+ // Marker = "obj0" & prefix = "obj" in the testCase, (testCase 45).
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-21.
+ // Marker = "obj1" & prefix = "obj" in the testCase, (testCase 46).
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "obj2"},
+ },
+ },
+ // ListObjectsResult-22.
+ // Marker = "newPrefix0" & prefix = "new" in the testCase,, (testCase 47).
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "newPrefix1"},
+ {Name: "newzen/zen/recurse/again/again/again/pics"},
+ },
+ },
+ // ListObjectsResult-23.
+ // Prefix is set to "Asia/India/" in the testCase, and delimiter is not set (testCase 55).
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "Asia/India/India-summer-photos-1"},
+ {Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
+ },
+ },
+
+ // ListObjectsResult-24.
+ // Prefix is set to "Asia" in the testCase, and delimiter is not set (testCase 56).
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "Asia-maps.png"},
+ {Name: "Asia/India/India-summer-photos-1"},
+ {Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
+ },
+ },
+
+ // ListObjectsResult-25.
+ // Prefix is set to "Asia" in the testCase, and delimiter is set (testCase 57).
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "Asia-maps.png"},
+ },
+ Prefixes: []string{"Asia/"},
+ },
+ // ListObjectsResult-26.
+ // prefix = "new" and delimiter is set in the testCase.(testCase 58).
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "newPrefix0"},
+ {Name: "newPrefix1"},
+ },
+ Prefixes: []string{"newzen/"},
+ },
+ // ListObjectsResult-27.
+ // Prefix is set to "Asia/India/" in the testCase, and delimiter is set to forward slash '/' (testCase 59).
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "Asia/India/India-summer-photos-1"},
+ },
+ Prefixes: []string{"Asia/India/Karnataka/"},
+ },
+ // ListObjectsResult-28.
+ // Marker is set to "Asia/India/India-summer-photos-1" and delimiter set in the testCase, (testCase 60).
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "newPrefix0"},
+ {Name: "newPrefix1"},
+ {Name: "obj0"},
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ Prefixes: []string{"newzen/"},
+ },
+ // ListObjectsResult-29.
+ // Marker is set to "Asia/India/Karnataka/Bangalore/Koramangala/pics" in the testCase and delimiter set, (testCase 61).
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "newPrefix0"},
+ {Name: "newPrefix1"},
+ {Name: "obj0"},
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ Prefixes: []string{"newzen/"},
+ },
+ // ListObjectsResult-30.
+ // Prefix and Delimiter is set to '/', (testCase 62).
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{},
+ },
+ // ListObjectsResult-31 Empty directory, recursive listing
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "obj1"},
+ {Name: "obj2"},
+ {Name: "temporary/0/"},
+ },
+ },
+ // ListObjectsResult-32 Empty directory, non recursive listing
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "obj1"},
+ {Name: "obj2"},
+ },
+ Prefixes: []string{"temporary/"},
+ },
+ // ListObjectsResult-33 Listing empty directory only
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "temporary/0/"},
+ },
+ },
+ // ListObjectsResult-34:
+ // * Listing with marker > last object should return empty
+ // * Listing an object with a trailing slash and '/' delimiter
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{},
+ },
+ // ListObjectsResult-35 list with custom uncommon delimiter
+ {
+ IsTruncated: false,
+ Objects: []ObjectInfo{
+ {Name: "file1/receipt.json"},
+ },
+ Prefixes: []string{"file1/guidSplunk"},
+ },
+ // ListObjectsResult-36 list with nextmarker prefix and maxKeys set to 1.
+ {
+ IsTruncated: true,
+ Prefixes: []string{"dir/day_id=2017-10-10/"},
+ },
+ }
+
+ testCases := []struct {
+ // Inputs to ListObjects.
+ bucketName string
+ prefix string
+ marker string
+ delimiter string
+ maxKeys int32
+ // Expected output of ListObjects.
+ result ListObjectsInfo
+ err error
+ // Flag indicating whether the test is expected to pass or not.
+ shouldPass bool
+ }{
+ // Test cases with invalid bucket names ( Test number 1-4).
+ {".test", "", "", "", 0, ListObjectsInfo{}, BucketNameInvalid{Bucket: ".test"}, false},
+ {"Test", "", "", "", 0, ListObjectsInfo{}, BucketNameInvalid{Bucket: "Test"}, false},
+ {"---", "", "", "", 0, ListObjectsInfo{}, BucketNameInvalid{Bucket: "---"}, false},
+ {"ad", "", "", "", 0, ListObjectsInfo{}, BucketNameInvalid{Bucket: "ad"}, false},
+ // Valid bucket names, but they do not exist (6-8).
+ {"volatile-bucket-1", "", "", "", 1000, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-1"}, false},
+ {"volatile-bucket-2", "", "", "", 1000, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-2"}, false},
+ {"volatile-bucket-3", "", "", "", 1000, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-3"}, false},
+ // If marker is *after* the last possible object from the prefix it should return an empty list.
+ {"test-bucket-list-object", "Asia", "europe-object", "", 0, ListObjectsInfo{}, nil, true},
+ // Setting a non-existing directory to be prefix (10-11).
+ {"empty-bucket", "europe/france/", "", "", 1, ListObjectsInfo{}, nil, true},
+ {"empty-bucket", "africa/tunisia/", "", "", 1, ListObjectsInfo{}, nil, true},
+ // Testing on empty bucket, that is, bucket without any objects in it (12).
+ {"empty-bucket", "", "", "", 0, ListObjectsInfo{}, nil, true},
+ // Setting maxKeys to negative value (13-14).
+ {"empty-bucket", "", "", "", -1, ListObjectsInfo{}, nil, true},
+ {"empty-bucket", "", "", "", 1, ListObjectsInfo{}, nil, true},
+ // Setting maxKeys to a very large value (15).
+ {"empty-bucket", "", "", "", 111100000, ListObjectsInfo{}, nil, true},
+ // Testing for all 10 objects in the bucket (16).
+ {"test-bucket-list-object", "", "", "", 10, resultCases[0], nil, true},
+ // Testing for negative value of maxKey, this should set maxKeys to listObjectsLimit (17).
+ {"test-bucket-list-object", "", "", "", -1, resultCases[0], nil, true},
+ // Testing for very large value of maxKey, this should set maxKeys to listObjectsLimit (18).
+ {"test-bucket-list-object", "", "", "", 1234567890, resultCases[0], nil, true},
+ // Testing for truncated value (19-22).
+ {"test-bucket-list-object", "", "", "", 5, resultCases[1], nil, true},
+ {"test-bucket-list-object", "", "", "", 4, resultCases[2], nil, true},
+ {"test-bucket-list-object", "", "", "", 3, resultCases[3], nil, true},
+ {"test-bucket-list-object", "", "", "", 1, resultCases[4], nil, true},
+ // Testing with prefix (23-26).
+ {"test-bucket-list-object", "new", "", "", 3, resultCases[5], nil, true},
+ {"test-bucket-list-object", "new", "", "", 4, resultCases[5], nil, true},
+ {"test-bucket-list-object", "new", "", "", 5, resultCases[5], nil, true},
+ {"test-bucket-list-object", "obj", "", "", 3, resultCases[6], nil, true},
+ // Testing with prefix and truncation (27-28).
+ {"test-bucket-list-object", "new", "", "", 1, resultCases[7], nil, true},
+ {"test-bucket-list-object", "obj", "", "", 2, resultCases[8], nil, true},
+ // Testing with marker, but without prefix and truncation (29-33).
+ {"test-bucket-list-object", "", "newPrefix0", "", 6, resultCases[9], nil, true},
+ {"test-bucket-list-object", "", "newPrefix1", "", 5, resultCases[10], nil, true},
+ {"test-bucket-list-object", "", "obj0", "", 4, resultCases[11], nil, true},
+ {"test-bucket-list-object", "", "obj1", "", 2, resultCases[12], nil, true},
+ {"test-bucket-list-object", "", "man", "", 11, resultCases[13], nil, true},
+ // Marker being set to a value which is greater than and all object names when sorted (34).
+ // Expected to send an empty response in this case.
+ {"test-bucket-list-object", "", "zen", "", 10, ListObjectsInfo{}, nil, true},
+ // Marker being set to a value which is lesser than and all object names when sorted (35).
+ // Expected to send all the objects in the bucket in this case.
+ {"test-bucket-list-object", "", "Abc", "", 10, resultCases[14], nil, true},
+ // Marker is to a hierarchical value (36-37).
+ {"test-bucket-list-object", "", "Asia/India/India-summer-photos-1", "", 10, resultCases[15], nil, true},
+ {"test-bucket-list-object", "", "Asia/India/Karnataka/Bangalore/Koramangala/pics", "", 10, resultCases[16], nil, true},
+ // Testing with marker and truncation, but no prefix (38-40).
+ {"test-bucket-list-object", "", "newPrefix0", "", 3, resultCases[17], nil, true},
+ {"test-bucket-list-object", "", "newPrefix1", "", 1, resultCases[18], nil, true},
+ {"test-bucket-list-object", "", "obj0", "", 1, resultCases[19], nil, true},
+ // Testing with both marker and prefix, but without truncation (41-43).
+ // The valid combination of marker and prefix should satisfy strings.HasPrefix(marker, prefix).
+ {"test-bucket-list-object", "obj", "obj0", "", 2, resultCases[20], nil, true},
+ {"test-bucket-list-object", "obj", "obj1", "", 1, resultCases[21], nil, true},
+ {"test-bucket-list-object", "new", "newPrefix0", "", 2, resultCases[22], nil, true},
+ // Testing with maxKeys set to 0 (44-50).
+ // The parameters have to valid.
+ {"test-bucket-list-object", "", "obj1", "", 0, ListObjectsInfo{}, nil, true},
+ {"test-bucket-list-object", "", "obj0", "", 0, ListObjectsInfo{}, nil, true},
+ {"test-bucket-list-object", "new", "", "", 0, ListObjectsInfo{}, nil, true},
+ {"test-bucket-list-object", "obj", "", "", 0, ListObjectsInfo{}, nil, true},
+ {"test-bucket-list-object", "obj", "obj0", "", 0, ListObjectsInfo{}, nil, true},
+ {"test-bucket-list-object", "obj", "obj1", "", 0, ListObjectsInfo{}, nil, true},
+ {"test-bucket-list-object", "new", "newPrefix0", "", 0, ListObjectsInfo{}, nil, true},
+ // Tests on hierarchical key names as prefix.
+ // Without delimteter the code should recurse into the prefix Dir.
+ // Tests with prefix, but without delimiter (51-52).
+ {"test-bucket-list-object", "Asia/India/", "", "", 10, resultCases[23], nil, true},
+ {"test-bucket-list-object", "Asia", "", "", 10, resultCases[24], nil, true},
+ // Tests with prefix and delimiter (53-55).
+ // With delimiter the code should not recurse into the sub-directories of prefix Dir.
+ {"test-bucket-list-object", "Asia", "", SlashSeparator, 10, resultCases[25], nil, true},
+ {"test-bucket-list-object", "new", "", SlashSeparator, 10, resultCases[26], nil, true},
+ {"test-bucket-list-object", "Asia/India/", "", SlashSeparator, 10, resultCases[27], nil, true},
+ // Test with marker set as hierarchical value and with delimiter. (56-57)
+ {"test-bucket-list-object", "", "Asia/India/India-summer-photos-1", SlashSeparator, 10, resultCases[28], nil, true},
+ {"test-bucket-list-object", "", "Asia/India/Karnataka/Bangalore/Koramangala/pics", SlashSeparator, 10, resultCases[29], nil, true},
+ // Test with prefix and delimiter set to '/'. (58)
+ {"test-bucket-list-object", SlashSeparator, "", SlashSeparator, 10, resultCases[30], nil, true},
+ // Test with invalid prefix (59)
+ {"test-bucket-list-object", "\\", "", SlashSeparator, 10, ListObjectsInfo{}, nil, true},
+ // Test listing an empty directory in recursive mode (60)
+ {"test-bucket-empty-dir", "", "", "", 10, resultCases[31], nil, true},
+ // Test listing an empty directory in a non recursive mode (61)
+ {"test-bucket-empty-dir", "", "", SlashSeparator, 10, resultCases[32], nil, true},
+ // Test listing a directory which contains an empty directory (62)
+ {"test-bucket-empty-dir", "", "temporary/", "", 10, resultCases[33], nil, true},
+ // Test listing with marker > last object such that response should be empty (63)
+ {"test-bucket-single-object", "", "A/C", "", 1000, resultCases[34], nil, true},
+ // Test listing an object with a trailing slash and a slash delimiter (64)
+ {"test-bucket-list-object", "Asia-maps.png/", "", "/", 1000, resultCases[34], nil, true},
+ // Test listing an object with uncommon delimiter
+ {testBuckets[4], "", "", "guidSplunk", 1000, resultCases[35], nil, true},
+ // Test listing an object with uncommon delimiter and matching prefix
+ {testBuckets[4], "file1/", "", "guidSplunk", 1000, resultCases[35], nil, true},
+ // Test listing at prefix with expected prefix markers
+ {testBuckets[5], "dir/", "", SlashSeparator, 1, resultCases[36], nil, true},
+ {"test-bucket-list-object", "Asia", "A", "", 1, resultCases[4], nil, true},
+ }
+
+ for i, testCase := range testCases {
+ t.Run(fmt.Sprintf("%s-Test%d", instanceType, i+1), func(t *testing.T) {
+ result, err := obj.ListObjectVersions(t.Context(), testCase.bucketName,
+ testCase.prefix, testCase.marker, "", testCase.delimiter, int(testCase.maxKeys))
+ if err != nil && testCase.shouldPass {
+ t.Errorf("%s: Expected to pass, but failed with: %s", instanceType, err.Error())
+ }
+ if err == nil && !testCase.shouldPass {
+ t.Errorf("%s: Expected to fail with \"%s\", but passed instead", instanceType, testCase.err.Error())
+ }
+ // Failed as expected, but does it fail for the expected reason.
+ if err != nil && !testCase.shouldPass {
+ if !strings.Contains(err.Error(), testCase.err.Error()) {
+ t.Errorf("%s: Expected to fail with error \"%s\", but instead failed with error \"%s\" instead", instanceType, testCase.err.Error(), err.Error())
+ }
+ }
+ // Since there are cases for which ListObjects fails, this is
+ // necessary. Test passes as expected, but the output values
+ // are verified for correctness here.
+ if err == nil && testCase.shouldPass {
+ // The length of the expected ListObjectsResult.Objects
+ // should match in both expected result from test cases
+ // and in the output. On failure calling t.Fatalf,
+ // otherwise it may lead to index out of range error in
+ // assertion following this.
+ if len(testCase.result.Objects) != len(result.Objects) {
+ t.Fatalf("%s: Expected number of object in the result to be '%d', but found '%d' objects instead", instanceType, len(testCase.result.Objects), len(result.Objects))
+ }
+ for j := 0; j < len(testCase.result.Objects); j++ {
+ if testCase.result.Objects[j].Name != result.Objects[j].Name {
+ t.Errorf("%s: Expected object name to be \"%s\", but found \"%s\" instead", instanceType, testCase.result.Objects[j].Name, result.Objects[j].Name)
+ }
+ }
+
+ if len(testCase.result.Prefixes) != len(result.Prefixes) {
+ t.Log(testCase, testCase.result.Prefixes, result.Prefixes)
+ t.Fatalf("%s: Expected number of prefixes in the result to be '%d', but found '%d' prefixes instead", instanceType, len(testCase.result.Prefixes), len(result.Prefixes))
+ }
+ for j := 0; j < len(testCase.result.Prefixes); j++ {
+ if testCase.result.Prefixes[j] != result.Prefixes[j] {
+ t.Errorf("%s: Expected prefix name to be \"%s\", but found \"%s\" instead", instanceType, testCase.result.Prefixes[j], result.Prefixes[j])
+ }
+ }
+
+ if testCase.result.IsTruncated != result.IsTruncated {
+ // Allow an extra continuation token.
+ if !result.IsTruncated || len(result.Objects) == 0 {
+ t.Errorf("%s: Expected IsTruncated flag to be %v, but instead found it to be %v", instanceType, testCase.result.IsTruncated, result.IsTruncated)
+ }
+ }
+
+ if testCase.result.IsTruncated && result.NextMarker == "" {
+ t.Errorf("%s: Expected NextMarker to contain a string since listing is truncated, but instead found it to be empty", instanceType)
+ }
+
+ if !testCase.result.IsTruncated && result.NextMarker != "" {
+ if !result.IsTruncated || len(result.Objects) == 0 {
+ t.Errorf("%s: Expected NextMarker to be empty since listing is not truncated, but instead found `%v`", instanceType, result.NextMarker)
+ }
+ }
+ }
+ })
+ }
+}
+
+// Wrapper for calling ListObjects continuation tests for both Erasure multiple disks and single node setup.
+func TestListObjectsContinuation(t *testing.T) {
+ ExecObjectLayerTest(t, testListObjectsContinuation)
+}
+
+// Unit test for ListObjects in general.
+func testListObjectsContinuation(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
+ t, _ := t1.(*testing.T)
+ testBuckets := []string{
+ // This bucket is used for testing ListObject operations.
+ "test-bucket-list-object-continuation-1",
+ "test-bucket-list-object-continuation-2",
+ }
+ for _, bucket := range testBuckets {
+ err := obj.MakeBucket(context.Background(), bucket, MakeBucketOptions{})
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ }
+
+ var err error
+ testObjects := []struct {
+ parentBucket string
+ name string
+ content string
+ meta map[string]string
+ }{
+ {testBuckets[0], "a/1.txt", "contentstring", nil},
+ {testBuckets[0], "a-1.txt", "contentstring", nil},
+ {testBuckets[0], "a.txt", "contentstring", nil},
+ {testBuckets[0], "apache2-doc/1.txt", "contentstring", nil},
+ {testBuckets[0], "apache2/1.txt", "contentstring", nil},
+ {testBuckets[0], "apache2/-sub/2.txt", "contentstring", nil},
+ {testBuckets[1], "azerty/1.txt", "contentstring", nil},
+ {testBuckets[1], "apache2-doc/1.txt", "contentstring", nil},
+ {testBuckets[1], "apache2/1.txt", "contentstring", nil},
+ }
+ for _, object := range testObjects {
+ md5Bytes := md5.Sum([]byte(object.content))
+ _, err = obj.PutObject(context.Background(), object.parentBucket, object.name, mustGetPutObjReader(t, bytes.NewBufferString(object.content),
+ int64(len(object.content)), hex.EncodeToString(md5Bytes[:]), ""), ObjectOptions{UserDefined: object.meta})
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ }
+
+ // Formulating the result data set to be expected from ListObjects call inside the tests,
+ // This will be used in testCases and used for asserting the correctness of ListObjects output in the tests.
+ resultCases := []ListObjectsInfo{
+ {
+ Objects: []ObjectInfo{
+ {Name: "a-1.txt"},
+ {Name: "a.txt"},
+ {Name: "a/1.txt"},
+ {Name: "apache2-doc/1.txt"},
+ {Name: "apache2/-sub/2.txt"},
+ {Name: "apache2/1.txt"},
+ },
+ },
+ {
+ Objects: []ObjectInfo{
+ {Name: "apache2-doc/1.txt"},
+ {Name: "apache2/1.txt"},
+ },
+ },
+ {
+ Prefixes: []string{"apache2-doc/", "apache2/", "azerty/"},
+ },
+ }
+
+ testCases := []struct {
+ // Inputs to ListObjects.
+ bucketName string
+ prefix string
+ delimiter string
+ page int
+ // Expected output of ListObjects.
+ result ListObjectsInfo
+ }{
+ {testBuckets[0], "", "", 1, resultCases[0]},
+ {testBuckets[0], "a", "", 1, resultCases[0]},
+ {testBuckets[1], "apache", "", 1, resultCases[1]},
+ {testBuckets[1], "", "/", 1, resultCases[2]},
+ }
+
+ for i, testCase := range testCases {
+ t.Run(fmt.Sprintf("%s-Test%d", instanceType, i+1), func(t *testing.T) {
+ var foundObjects []ObjectInfo
+ var foundPrefixes []string
+ marker := ""
+ for {
+ result, err := obj.ListObjects(t.Context(), testCase.bucketName,
+ testCase.prefix, marker, testCase.delimiter, testCase.page)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Expected to pass, but failed with: %s", i+1, instanceType, err.Error())
+ }
+ foundObjects = append(foundObjects, result.Objects...)
+ foundPrefixes = append(foundPrefixes, result.Prefixes...)
+ if !result.IsTruncated {
+ break
+ }
+ marker = result.NextMarker
+ if len(result.Objects) > 0 {
+ // Discard marker, so it cannot resume listing.
+ marker = result.Objects[len(result.Objects)-1].Name
+ }
+ }
+
+ if len(testCase.result.Objects) != len(foundObjects) {
+ t.Logf("want: %v", objInfoNames(testCase.result.Objects))
+ t.Logf("got: %v", objInfoNames(foundObjects))
+ t.Errorf("Test %d: %s: Expected number of objects in the result to be '%d', but found '%d' objects instead",
+ i+1, instanceType, len(testCase.result.Objects), len(foundObjects))
+ }
+ for j := 0; j < len(testCase.result.Objects); j++ {
+ if j >= len(foundObjects) {
+ t.Errorf("Test %d: %s: Expected object name to be \"%s\", but not nothing instead", i+1, instanceType, testCase.result.Objects[j].Name)
+ continue
+ }
+ if testCase.result.Objects[j].Name != foundObjects[j].Name {
+ t.Errorf("Test %d: %s: Expected object name to be \"%s\", but found \"%s\" instead", i+1, instanceType, testCase.result.Objects[j].Name, foundObjects[j].Name)
+ }
+ }
+
+ if len(testCase.result.Prefixes) != len(foundPrefixes) {
+ t.Logf("want: %v", testCase.result.Prefixes)
+ t.Logf("got: %v", foundPrefixes)
+ t.Errorf("Test %d: %s: Expected number of prefixes in the result to be '%d', but found '%d' prefixes instead",
+ i+1, instanceType, len(testCase.result.Prefixes), len(foundPrefixes))
+ }
+ for j := 0; j < len(testCase.result.Prefixes); j++ {
+ if j >= len(foundPrefixes) {
+ t.Errorf("Test %d: %s: Expected prefix name to be \"%s\", but found no result", i+1, instanceType, testCase.result.Prefixes[j])
+ continue
+ }
+ if testCase.result.Prefixes[j] != foundPrefixes[j] {
+ t.Errorf("Test %d: %s: Expected prefix name to be \"%s\", but found \"%s\" instead", i+1, instanceType, testCase.result.Prefixes[j], foundPrefixes[j])
+ }
+ }
+ })
+ }
+}
+
+// Initialize FS backend for the benchmark.
+func initFSObjectsB(disk string, t *testing.B) (obj ObjectLayer) {
+ obj, _, err := initObjectLayer(context.Background(), mustGetPoolEndpoints(0, disk))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ newTestConfig(globalMinioDefaultRegion, obj)
+
+ initAllSubsystems(GlobalContext)
+ return obj
+}
+
+// BenchmarkListObjects - Run ListObject Repeatedly and benchmark.
+func BenchmarkListObjects(b *testing.B) {
+ // Make a temporary directory to use as the obj.
+ directory := b.TempDir()
+
+ // Create the obj.
+ obj := initFSObjectsB(directory, b)
+
+ bucket := "ls-benchmark-bucket"
+ // Create a bucket.
+ err := obj.MakeBucket(b.Context(), bucket, MakeBucketOptions{})
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ // Insert objects to be listed and benchmarked later.
+ for i := range 20000 {
+ key := "obj" + strconv.Itoa(i)
+ _, err = obj.PutObject(b.Context(), bucket, key, mustGetPutObjReader(b, bytes.NewBufferString(key), int64(len(key)), "", ""), ObjectOptions{})
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+
+ // List the buckets over and over and over.
+ for b.Loop() {
+ _, err = obj.ListObjects(b.Context(), bucket, "", "obj9000", "", -1)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func TestListObjectsWithILM(t *testing.T) {
+ ExecObjectLayerTest(t, testListObjectsWithILM)
+}
+
+func testListObjectsWithILM(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
+ // Prepare lifecycle expiration workers
+ es := newExpiryState(t1.Context(), obj, 0)
+ globalExpiryState = es
+
+ t, _ := t1.(*testing.T)
+
+ objContent := "test-content"
+ objMd5 := md5.Sum([]byte(objContent))
+
+ uploads := []struct {
+ bucket string
+ expired int
+ notExpired int
+ }{
+ {"test-list-ilm-nothing-expired", 0, 6},
+ {"test-list-ilm-all-expired", 6, 0},
+ {"test-list-ilm-all-half-expired", 3, 3},
+ }
+
+ oneWeekAgo := time.Now().Add(-7 * 24 * time.Hour)
+
+ lifecycleBytes := []byte(`
+
+
+ Enabled
+
+ 1
+
+
+
+`)
+
+ lifecycleConfig, err := lifecycle.ParseLifecycleConfig(bytes.NewReader(lifecycleBytes))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for i, upload := range uploads {
+ err := obj.MakeBucket(context.Background(), upload.bucket, MakeBucketOptions{})
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ metadata, err := globalBucketMetadataSys.Get(upload.bucket)
+ if err != nil {
+ t.Fatal(err)
+ }
+ metadata.lifecycleConfig = lifecycleConfig
+ globalBucketMetadataSys.Set(upload.bucket, metadata)
+ defer globalBucketMetadataSys.Remove(upload.bucket)
+
+ // Upload objects which modtime as one week ago, supposed to be expired by ILM
+ for range upload.expired {
+ _, err := obj.PutObject(context.Background(), upload.bucket, randString(32),
+ mustGetPutObjReader(t,
+ bytes.NewBufferString(objContent),
+ int64(len(objContent)),
+ hex.EncodeToString(objMd5[:]),
+ ""),
+ ObjectOptions{MTime: oneWeekAgo},
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ // Upload objects which current time as modtime, not expired by ILM
+ for range upload.notExpired {
+ _, err := obj.PutObject(context.Background(), upload.bucket, randString(32),
+ mustGetPutObjReader(t,
+ bytes.NewBufferString(objContent),
+ int64(len(objContent)),
+ hex.EncodeToString(objMd5[:]),
+ ""),
+ ObjectOptions{},
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ for _, maxKeys := range []int{1, 10, 49} {
+ // Test ListObjects V2
+ totalObjs, didRuns := 0, 0
+ marker := ""
+ for {
+ didRuns++
+ if didRuns > 1000 {
+ t.Fatal("too many runs")
+ return
+ }
+ result, err := obj.ListObjectsV2(context.Background(), upload.bucket, "", marker, "", maxKeys, false, "")
+ if err != nil {
+ t.Fatalf("Test %d: %s: Expected to pass, but failed with: %s", i, instanceType, err.Error())
+ }
+ totalObjs += len(result.Objects)
+ if !result.IsTruncated {
+ break
+ }
+ if marker != "" && marker == result.NextContinuationToken {
+ t.Fatalf("infinite loop marker: %s", result.NextContinuationToken)
+ }
+ marker = result.NextContinuationToken
+ }
+
+ if totalObjs != upload.notExpired {
+ t.Fatalf("Test %d: %s: max-keys=%d, %d objects are expected to be seen, but %d found instead (didRuns=%d)",
+ i+1, instanceType, maxKeys, upload.notExpired, totalObjs, didRuns)
+ }
+ }
+ }
+}
diff --git a/cmd/object-api-multipart_test.go b/cmd/object-api-multipart_test.go
new file mode 100644
index 00000000..d888df71
--- /dev/null
+++ b/cmd/object-api-multipart_test.go
@@ -0,0 +1,2238 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "math/rand"
+ "reflect"
+ "runtime"
+ "strings"
+ "testing"
+
+ "github.com/dustin/go-humanize"
+ "github.com/minio/minio/internal/config/storageclass"
+ "github.com/minio/minio/internal/hash"
+ "github.com/minio/minio/internal/ioutil"
+)
+
+// Wrapper for calling NewMultipartUpload tests for both Erasure multiple disks and single node setup.
+func TestObjectNewMultipartUpload(t *testing.T) {
+ if runtime.GOOS == globalWindowsOSName {
+ t.Skip()
+ }
+ ExecObjectLayerTest(t, testObjectNewMultipartUpload)
+}
+
+// Tests validate creation of new multipart upload instance.
+func testObjectNewMultipartUpload(obj ObjectLayer, instanceType string, t TestErrHandler) {
+ bucket := "minio-bucket"
+ object := "minio-object"
+ opts := ObjectOptions{}
+ _, err := obj.NewMultipartUpload(context.Background(), "--", object, opts)
+ if err == nil {
+ t.Fatalf("%s: Expected to fail since bucket name is invalid.", instanceType)
+ }
+
+ errMsg := "Bucket not found: minio-bucket"
+ // operation expected to fail since the bucket on which NewMultipartUpload is being initiated doesn't exist.
+ _, err = obj.NewMultipartUpload(context.Background(), bucket, object, opts)
+ if err == nil {
+ t.Fatalf("%s: Expected to fail since the NewMultipartUpload is initialized on a non-existent bucket.", instanceType)
+ }
+ if errMsg != err.Error() {
+ t.Errorf("%s, Expected to fail with Error \"%s\", but instead found \"%s\".", instanceType, errMsg, err.Error())
+ }
+
+ // Create bucket before initiating NewMultipartUpload.
+ err = obj.MakeBucket(context.Background(), bucket, MakeBucketOptions{})
+ if err != nil {
+ // failed to create newbucket, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ res, err := obj.NewMultipartUpload(context.Background(), bucket, "\\", opts)
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ err = obj.AbortMultipartUpload(context.Background(), bucket, "\\", res.UploadID, opts)
+ if err != nil {
+ switch err.(type) {
+ case InvalidUploadID:
+ t.Fatalf("%s: New Multipart upload failed to create uuid file.", instanceType)
+ default:
+ t.Fatal(err.Error())
+ }
+ }
+}
+
+// Wrapper for calling AbortMultipartUpload tests for both Erasure multiple disks and single node setup.
+func TestObjectAbortMultipartUpload(t *testing.T) {
+ ExecObjectLayerTest(t, testObjectAbortMultipartUpload)
+}
+
+// Tests validate creation of abort multipart upload instance.
+func testObjectAbortMultipartUpload(obj ObjectLayer, instanceType string, t TestErrHandler) {
+ bucket := "minio-bucket"
+ object := "minio-object"
+ opts := ObjectOptions{}
+ // Create bucket before initiating NewMultipartUpload.
+ err := obj.MakeBucket(context.Background(), bucket, MakeBucketOptions{})
+ if err != nil {
+ // failed to create newbucket, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ res, err := obj.NewMultipartUpload(context.Background(), bucket, object, opts)
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ uploadID := res.UploadID
+
+ abortTestCases := []struct {
+ bucketName string
+ objName string
+ uploadID string
+ expectedErrType error
+ }{
+ {"--", object, uploadID, BucketNameInvalid{}},
+ {"foo", object, uploadID, BucketNotFound{}},
+ {bucket, object, "foo-foo", InvalidUploadID{}},
+ {bucket, object, uploadID, nil},
+ }
+
+ if runtime.GOOS != globalWindowsOSName {
+ abortTestCases = append(abortTestCases, struct {
+ bucketName string
+ objName string
+ uploadID string
+ expectedErrType error
+ }{bucket, "\\", uploadID, InvalidUploadID{}})
+ }
+
+ // Iterating over creatPartCases to generate multipart chunks.
+ for i, testCase := range abortTestCases {
+ err = obj.AbortMultipartUpload(context.Background(), testCase.bucketName, testCase.objName, testCase.uploadID, opts)
+ if testCase.expectedErrType == nil && err != nil {
+ t.Errorf("Test %d, unexpected err is received: %v, expected:%v\n", i+1, err, testCase.expectedErrType)
+ }
+ if testCase.expectedErrType != nil && !isSameType(err, testCase.expectedErrType) {
+ t.Errorf("Test %d, unexpected err is received: %v, expected:%v\n", i+1, err, testCase.expectedErrType)
+ }
+ }
+}
+
+// Wrapper for calling isUploadIDExists tests for both Erasure multiple disks and single node setup.
+func TestObjectAPIIsUploadIDExists(t *testing.T) {
+ ExecObjectLayerTest(t, testObjectAPIIsUploadIDExists)
+}
+
+// Tests validates the validator for existence of uploadID.
+func testObjectAPIIsUploadIDExists(obj ObjectLayer, instanceType string, t TestErrHandler) {
+ bucket := "minio-bucket"
+ object := "minio-object"
+
+ // Create bucket before initiating NewMultipartUpload.
+ err := obj.MakeBucket(context.Background(), bucket, MakeBucketOptions{})
+ if err != nil {
+ // Failed to create newbucket, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ _, err = obj.NewMultipartUpload(context.Background(), bucket, object, ObjectOptions{})
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ opts := ObjectOptions{}
+ err = obj.AbortMultipartUpload(context.Background(), bucket, object, "abc", opts)
+ switch err.(type) {
+ case InvalidUploadID:
+ default:
+ t.Fatalf("%s: Expected uploadIDPath to exist.", instanceType)
+ }
+}
+
+// Wrapper for calling PutObjectPart tests for both Erasure multiple disks and single node setup.
+func TestObjectAPIPutObjectPart(t *testing.T) {
+ ExecExtendedObjectLayerTest(t, testObjectAPIPutObjectPart)
+}
+
+// Tests validate correctness of PutObjectPart.
+func testObjectAPIPutObjectPart(obj ObjectLayer, instanceType string, t TestErrHandler) {
+ // Generating cases for which the PutObjectPart fails.
+ bucket := "minio-bucket"
+ object := "minio-object"
+ opts := ObjectOptions{}
+ // Create bucket before initiating NewMultipartUpload.
+ err := obj.MakeBucket(context.Background(), bucket, MakeBucketOptions{})
+ if err != nil {
+ // Failed to create newbucket, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ // Initiate Multipart Upload on the above created bucket.
+ res, err := obj.NewMultipartUpload(context.Background(), bucket, object, opts)
+ if err != nil {
+ // Failed to create NewMultipartUpload, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ err = obj.MakeBucket(context.Background(), "abc", MakeBucketOptions{})
+ if err != nil {
+ // Failed to create newbucket, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ resN, err := obj.NewMultipartUpload(context.Background(), "abc", "def", opts)
+ if err != nil {
+ // Failed to create NewMultipartUpload, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ uploadID := res.UploadID
+ // Creating a dummy bucket for tests.
+ err = obj.MakeBucket(context.Background(), "unused-bucket", MakeBucketOptions{})
+ if err != nil {
+ // Failed to create newbucket, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ obj.DeleteBucket(context.Background(), "abc", DeleteBucketOptions{})
+
+ // Collection of non-exhaustive PutObjectPart test cases, valid errors
+ // and success responses.
+ testCases := []struct {
+ bucketName string
+ objName string
+ uploadID string
+ PartID int
+ inputReaderData string
+ inputMd5 string
+ inputSHA256 string
+ inputDataSize int64
+ // flag indicating whether the test should pass.
+ shouldPass bool
+ // expected error output.
+ expectedMd5 string
+ expectedError error
+ }{
+ // Test case 1-4.
+ // Cases with invalid bucket name.
+ {bucketName: ".test", objName: "obj", PartID: 1, expectedError: fmt.Errorf("%s", "Bucket name invalid: .test")},
+ {bucketName: "------", objName: "obj", PartID: 1, expectedError: fmt.Errorf("%s", "Bucket name invalid: ------")},
+ {
+ bucketName: "$this-is-not-valid-too", objName: "obj", PartID: 1,
+ expectedError: fmt.Errorf("%s", "Bucket name invalid: $this-is-not-valid-too"),
+ },
+ {bucketName: "a", objName: "obj", PartID: 1, expectedError: fmt.Errorf("%s", "Bucket name invalid: a")},
+ // Test case - 5.
+ // Case with invalid object names.
+ {bucketName: bucket, PartID: 1, expectedError: fmt.Errorf("%s", "Object name invalid: minio-bucket/")},
+ // Test case - 6.
+ // Valid object and bucket names but non-existent bucket.
+ {bucketName: "abc", objName: "def", uploadID: resN.UploadID, PartID: 1, expectedError: fmt.Errorf("%s", "Bucket not found: abc")},
+ // Test Case - 7.
+ // Existing bucket, but using a bucket on which NewMultipartUpload is not Initiated.
+ {bucketName: "unused-bucket", objName: "def", uploadID: "xyz", PartID: 1, expectedError: fmt.Errorf("%s", "Invalid upload id xyz")},
+ // Test Case - 8.
+ // Existing bucket, object name different from which NewMultipartUpload is constructed from.
+ // Expecting "Invalid upload id".
+ {bucketName: bucket, objName: "def", uploadID: "xyz", PartID: 1, expectedError: fmt.Errorf("%s", "Invalid upload id xyz")},
+ // Test Case - 9.
+ // Existing bucket, bucket and object name are the ones from which NewMultipartUpload is constructed from.
+ // But the uploadID is invalid.
+ // Expecting "Invalid upload id".
+ {bucketName: bucket, objName: object, uploadID: "xyz", PartID: 1, expectedError: fmt.Errorf("%s", "Invalid upload id xyz")},
+ // Test Case - 10.
+ // Case with valid UploadID, existing bucket name.
+ // But using the bucket name from which NewMultipartUpload is not constructed from.
+ {bucketName: "unused-bucket", objName: object, uploadID: uploadID, PartID: 1, expectedError: fmt.Errorf("%s", "Invalid upload id "+uploadID)},
+ // Test Case - 11.
+ // Case with valid UploadID, existing bucket name.
+ // But using the object name from which NewMultipartUpload is not constructed from.
+ {bucketName: bucket, objName: "none-object", uploadID: uploadID, PartID: 1, expectedError: fmt.Errorf("%s", "Invalid upload id "+uploadID)},
+ // Test case - 12.
+ // Input to replicate Md5 mismatch.
+ {
+ bucketName: bucket, objName: object, uploadID: uploadID, PartID: 1, inputMd5: "d41d8cd98f00b204e9800998ecf8427f",
+ expectedError: hash.BadDigest{ExpectedMD5: "d41d8cd98f00b204e9800998ecf8427f", CalculatedMD5: "d41d8cd98f00b204e9800998ecf8427e"},
+ },
+ // Test case - 13.
+ // When incorrect sha256 is provided.
+ {
+ bucketName: bucket, objName: object, uploadID: uploadID, PartID: 1, inputSHA256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b854",
+ expectedError: hash.SHA256Mismatch{
+ ExpectedSHA256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b854",
+ CalculatedSHA256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ },
+ },
+ // Test case - 14.
+ // Input with size more than the size of actual data inside the reader.
+ {
+ bucketName: bucket, objName: object, uploadID: uploadID, PartID: 1, inputReaderData: "abcd", inputMd5: "e2fc714c4727ee9395f324cd2e7f3335", inputDataSize: int64(len("abcd") + 1),
+ expectedError: hash.BadDigest{ExpectedMD5: "e2fc714c4727ee9395f324cd2e7f3335", CalculatedMD5: "e2fc714c4727ee9395f324cd2e7f331f"},
+ },
+ // Test case - 15.
+ // Input with size less than the size of actual data inside the reader.
+ {
+ bucketName: bucket, objName: object, uploadID: uploadID, PartID: 1, inputReaderData: "abcd", inputMd5: "900150983cd24fb0d6963f7d28e17f73", inputDataSize: int64(len("abcd") - 1),
+ expectedError: ioutil.ErrOverread,
+ },
+
+ // Test case - 16-19.
+ // Validating for success cases.
+ {bucketName: bucket, objName: object, uploadID: uploadID, PartID: 1, inputReaderData: "abcd", inputMd5: "e2fc714c4727ee9395f324cd2e7f331f", inputSHA256: "88d4266fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031589", inputDataSize: int64(len("abcd")), shouldPass: true},
+ {bucketName: bucket, objName: object, uploadID: uploadID, PartID: 2, inputReaderData: "efgh", inputMd5: "1f7690ebdd9b4caf8fab49ca1757bf27", inputSHA256: "e5e088a0b66163a0a26a5e053d2a4496dc16ab6e0e3dd1adf2d16aa84a078c9d", inputDataSize: int64(len("efgh")), shouldPass: true},
+ {bucketName: bucket, objName: object, uploadID: uploadID, PartID: 3, inputReaderData: "ijkl", inputMd5: "09a0877d04abf8759f99adec02baf579", inputSHA256: "005c19658919186b85618c5870463eec8d9b8c1a9d00208a5352891ba5bbe086", inputDataSize: int64(len("abcd")), shouldPass: true},
+ {bucketName: bucket, objName: object, uploadID: uploadID, PartID: 4, inputReaderData: "mnop", inputMd5: "e132e96a5ddad6da8b07bba6f6131fef", inputSHA256: "f1afc31479522d6cff1ed068f93998f05a8cd3b22f5c37d7f307084f62d1d270", inputDataSize: int64(len("abcd")), shouldPass: true},
+ }
+
+ // Validate all the test cases.
+ for i, testCase := range testCases {
+ actualInfo, actualErr := obj.PutObjectPart(context.Background(), testCase.bucketName, testCase.objName, testCase.uploadID, testCase.PartID, mustGetPutObjReader(t, bytes.NewBufferString(testCase.inputReaderData), testCase.inputDataSize, testCase.inputMd5, testCase.inputSHA256), opts)
+ // All are test cases above are expected to fail.
+ if actualErr != nil && testCase.shouldPass {
+ t.Errorf("Test %d: %s: Expected to pass, but failed with: %s.", i+1, instanceType, actualErr.Error())
+ }
+ if actualErr == nil && !testCase.shouldPass {
+ t.Errorf("Test %d: %s: Expected to fail with \"%s\", but passed instead.", i+1, instanceType, testCase.expectedError.Error())
+ }
+ // Failed as expected, but does it fail for the expected reason.
+ if actualErr != nil && !testCase.shouldPass {
+ if testCase.expectedError.Error() != actualErr.Error() {
+ t.Errorf("Test %d: %s: Expected to fail with error \"%s\", but instead failed with error \"%s\" instead.", i+1, instanceType, testCase.expectedError.Error(), actualErr.Error())
+ }
+ }
+ // Test passes as expected, but the output values are verified for correctness here.
+ if actualErr == nil && testCase.shouldPass {
+ // Asserting whether the md5 output is correct.
+ if testCase.inputMd5 != actualInfo.ETag {
+ t.Errorf("Test %d: %s: Calculated Md5 different from the actual one %s.", i+1, instanceType, actualInfo.ETag)
+ }
+ }
+ }
+}
+
+// Wrapper for calling TestListMultipartUploads tests for both Erasure multiple disks and single node setup.
+func TestListMultipartUploads(t *testing.T) {
+ ExecExtendedObjectLayerTest(t, testListMultipartUploads)
+}
+
+// testListMultipartUploads - Tests validate listing of multipart uploads.
+func testListMultipartUploads(obj ObjectLayer, instanceType string, t TestErrHandler) {
+ bucketNames := []string{"minio-bucket", "minio-2-bucket", "minio-3-bucket"}
+ objectNames := []string{"minio-object-1.txt", "minio-object.txt", "neymar-1.jpeg", "neymar.jpeg", "parrot-1.png", "parrot.png"}
+ uploadIDs := []string{}
+ opts := ObjectOptions{}
+ // bucketnames[0].
+ // objectNames[0].
+ // uploadIds [0].
+ // Create bucket before initiating NewMultipartUpload.
+ err := obj.MakeBucket(context.Background(), bucketNames[0], MakeBucketOptions{})
+ if err != nil {
+ // Failed to create newbucket, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ // Initiate Multipart Upload on the above created bucket.
+ res, err := obj.NewMultipartUpload(context.Background(), bucketNames[0], objectNames[0], opts)
+ if err != nil {
+ // Failed to create NewMultipartUpload, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ uploadIDs = append(uploadIDs, res.UploadID)
+
+ // bucketnames[1].
+ // objectNames[0].
+ // uploadIds [1-3].
+ // Bucket to test for multiple upload Id's for a given object.
+ err = obj.MakeBucket(context.Background(), bucketNames[1], MakeBucketOptions{})
+ if err != nil {
+ // Failed to create newbucket, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ for range 3 {
+ // Initiate Multipart Upload on bucketNames[1] for the same object 3 times.
+ // Used to test the listing for the case of multiple uploadID's for a given object.
+ res, err = obj.NewMultipartUpload(context.Background(), bucketNames[1], objectNames[0], opts)
+ if err != nil {
+ // Failed to create NewMultipartUpload, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ uploadIDs = append(uploadIDs, res.UploadID)
+ }
+
+ // Bucket to test for multiple objects, each with unique UUID.
+ // bucketnames[2].
+ // objectNames[0-2].
+ // uploadIds [4-9].
+ err = obj.MakeBucket(context.Background(), bucketNames[2], MakeBucketOptions{})
+ if err != nil {
+ // Failed to create newbucket, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ // Initiate Multipart Upload on bucketNames[2].
+ // Used to test the listing for the case of multiple objects for a given bucket.
+ for i := range 6 {
+ res, err = obj.NewMultipartUpload(context.Background(), bucketNames[2], objectNames[i], opts)
+ if err != nil {
+ // Failed to create NewMultipartUpload, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ // uploadIds [4-9].
+ uploadIDs = append(uploadIDs, res.UploadID)
+ }
+ // Create multipart parts.
+ // Need parts to be uploaded before MultipartLists can be called and tested.
+ createPartCases := []struct {
+ bucketName string
+ objName string
+ uploadID string
+ PartID int
+ inputReaderData string
+ inputMd5 string
+ inputDataSize int64
+ expectedMd5 string
+ }{
+ // Case 1-4.
+ // Creating sequence of parts for same uploadID.
+ // Used to ensure that the ListMultipartResult produces one output for the four parts uploaded below for the given upload ID.
+ {bucketNames[0], objectNames[0], uploadIDs[0], 1, "abcd", "e2fc714c4727ee9395f324cd2e7f331f", int64(len("abcd")), "e2fc714c4727ee9395f324cd2e7f331f"},
+ {bucketNames[0], objectNames[0], uploadIDs[0], 2, "efgh", "1f7690ebdd9b4caf8fab49ca1757bf27", int64(len("efgh")), "1f7690ebdd9b4caf8fab49ca1757bf27"},
+ {bucketNames[0], objectNames[0], uploadIDs[0], 3, "ijkl", "09a0877d04abf8759f99adec02baf579", int64(len("abcd")), "09a0877d04abf8759f99adec02baf579"},
+ {bucketNames[0], objectNames[0], uploadIDs[0], 4, "mnop", "e132e96a5ddad6da8b07bba6f6131fef", int64(len("abcd")), "e132e96a5ddad6da8b07bba6f6131fef"},
+ // Cases 5-7.
+ // Create parts with 3 uploadID's for the same object.
+ // Testing for listing of all the uploadID's for given object.
+ // Insertion with 3 different uploadID's are done for same bucket and object.
+ {bucketNames[1], objectNames[0], uploadIDs[1], 1, "abcd", "e2fc714c4727ee9395f324cd2e7f331f", int64(len("abcd")), "e2fc714c4727ee9395f324cd2e7f331f"},
+ {bucketNames[1], objectNames[0], uploadIDs[2], 1, "abcd", "e2fc714c4727ee9395f324cd2e7f331f", int64(len("abcd")), "e2fc714c4727ee9395f324cd2e7f331f"},
+ {bucketNames[1], objectNames[0], uploadIDs[3], 1, "abcd", "e2fc714c4727ee9395f324cd2e7f331f", int64(len("abcd")), "e2fc714c4727ee9395f324cd2e7f331f"},
+ // Case 8-13.
+ // Generating parts for different objects.
+ {bucketNames[2], objectNames[0], uploadIDs[4], 1, "abcd", "e2fc714c4727ee9395f324cd2e7f331f", int64(len("abcd")), "e2fc714c4727ee9395f324cd2e7f331f"},
+ {bucketNames[2], objectNames[1], uploadIDs[5], 1, "abcd", "e2fc714c4727ee9395f324cd2e7f331f", int64(len("abcd")), "e2fc714c4727ee9395f324cd2e7f331f"},
+ {bucketNames[2], objectNames[2], uploadIDs[6], 1, "abcd", "e2fc714c4727ee9395f324cd2e7f331f", int64(len("abcd")), "e2fc714c4727ee9395f324cd2e7f331f"},
+ {bucketNames[2], objectNames[3], uploadIDs[7], 1, "abcd", "e2fc714c4727ee9395f324cd2e7f331f", int64(len("abcd")), "e2fc714c4727ee9395f324cd2e7f331f"},
+ {bucketNames[2], objectNames[4], uploadIDs[8], 1, "abcd", "e2fc714c4727ee9395f324cd2e7f331f", int64(len("abcd")), "e2fc714c4727ee9395f324cd2e7f331f"},
+ {bucketNames[2], objectNames[5], uploadIDs[9], 1, "abcd", "e2fc714c4727ee9395f324cd2e7f331f", int64(len("abcd")), "e2fc714c4727ee9395f324cd2e7f331f"},
+ }
+ sha256sum := ""
+ // Iterating over creatPartCases to generate multipart chunks.
+ for _, testCase := range createPartCases {
+ _, err := obj.PutObjectPart(context.Background(), testCase.bucketName, testCase.objName, testCase.uploadID, testCase.PartID, mustGetPutObjReader(t, bytes.NewBufferString(testCase.inputReaderData), testCase.inputDataSize, testCase.inputMd5, sha256sum), opts)
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ }
+
+ // Expected Results set for asserting ListObjectMultipart test.
+ listMultipartResults := []ListMultipartsInfo{
+ // listMultipartResults - 1.
+ // Used to check that the result produces only one output for the 4 parts uploaded in cases 1-4 of createPartCases above.
+ // ListMultipartUploads doesn't list the parts.
+ {
+ MaxUploads: 100,
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[0],
+ },
+ },
+ },
+ // listMultipartResults - 2.
+ // Used to check that the result produces if keyMarker is set to the only available object.
+ // `KeyMarker` is set.
+ // ListMultipartUploads doesn't list the parts.
+ {
+ MaxUploads: 100,
+ KeyMarker: "minio-object-1.txt",
+ },
+ // listMultipartResults - 3.
+ // `KeyMarker` is set, no MultipartInfo expected.
+ // ListMultipartUploads doesn't list the parts.
+ // `Maxupload` value is asserted.
+ {
+ MaxUploads: 100,
+ KeyMarker: "orange",
+ },
+ // listMultipartResults - 4.
+ // `KeyMarker` is set, no MultipartInfo expected.
+ // Maxupload value is asserted.
+ {
+ MaxUploads: 1,
+ KeyMarker: "orange",
+ },
+ // listMultipartResults - 5.
+ // `KeyMarker` is set. It contains part of the objectname as `KeyPrefix`.
+ // Expecting the result to contain one MultipartInfo entry and Istruncated to be false.
+ {
+ MaxUploads: 10,
+ KeyMarker: "min",
+ IsTruncated: false,
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[0],
+ },
+ },
+ },
+ // listMultipartResults - 6.
+ // `KeyMarker` is set. It contains part of the objectname as `KeyPrefix`.
+ // `MaxUploads` is set equal to the number of meta data entries in the result, the result contains only one entry.
+ // Expecting the result to contain one MultipartInfo entry and IsTruncated to be false.
+ {
+ MaxUploads: 1,
+ KeyMarker: "min",
+ IsTruncated: false,
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[0],
+ },
+ },
+ },
+ // listMultipartResults - 7.
+ // `KeyMarker` is set. It contains part of the objectname as `KeyPrefix`.
+ // Testing for the case with `MaxUploads` set to 0.
+ // Expecting the result to contain no MultipartInfo entry since `MaxUploads` is set to 0.
+ // Expecting `IsTruncated` to be true.
+ {
+ MaxUploads: 0,
+ KeyMarker: "min",
+ IsTruncated: true,
+ },
+ // listMultipartResults - 8.
+ // `KeyMarker` is set. It contains part of the objectname as KeyPrefix.
+ // Testing for the case with `MaxUploads` set to 0.
+ // Expecting the result to contain no MultipartInfo entry since `MaxUploads` is set to 0.
+ // Expecting `isTruncated` to be true.
+ {
+ MaxUploads: 0,
+ KeyMarker: "min",
+ IsTruncated: true,
+ },
+ // listMultipartResults - 9.
+ // `KeyMarker` is set. It contains part of the objectname as KeyPrefix.
+ // `KeyMarker` is set equal to the object name in the result.
+ // Expecting the result to contain one MultipartInfo entry and IsTruncated to be false.
+ {
+ MaxUploads: 2,
+ KeyMarker: "minio-object",
+ IsTruncated: false,
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[0],
+ },
+ },
+ },
+ // listMultipartResults - 10.
+ // Prefix is set. It is set equal to the object name.
+ // MaxUploads is set more than number of meta data entries in the result.
+ // Expecting the result to contain one MultipartInfo entry and IsTruncated to be false.
+ {
+ MaxUploads: 2,
+ Prefix: "minio-object-1.txt",
+ IsTruncated: false,
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[0],
+ },
+ },
+ },
+ // listMultipartResults - 11.
+ // Setting `Prefix` to contain the object name as its prefix.
+ // MaxUploads is set more than number of meta data entries in the result.
+ // Expecting the result to contain one MultipartInfo entry and IsTruncated to be false.
+ {
+ MaxUploads: 2,
+ Prefix: "min",
+ IsTruncated: false,
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[0],
+ },
+ },
+ },
+ // listMultipartResults - 12.
+ // Setting `Prefix` to contain the object name as its prefix.
+ // MaxUploads is set equal to number of meta data entries in the result.
+ // Expecting the result to contain one MultipartInfo entry and IsTruncated to be false.
+ {
+ MaxUploads: 1,
+ Prefix: "min",
+ IsTruncated: false,
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[0],
+ },
+ },
+ },
+ // listMultipartResults - 13.
+ // `Prefix` is set. It doesn't contain object name as its preifx.
+ // MaxUploads is set more than number of meta data entries in the result.
+ // Expecting no `Uploads` metadata.
+ {
+ MaxUploads: 2,
+ Prefix: "orange",
+ IsTruncated: false,
+ },
+ // listMultipartResults - 14.
+ // `Prefix` is set. It doesn't contain object name as its preifx.
+ // MaxUploads is set more than number of meta data entries in the result.
+ // Expecting the result to contain 0 uploads and isTruncated to false.
+ {
+ MaxUploads: 2,
+ Prefix: "Asia",
+ IsTruncated: false,
+ },
+ // listMultipartResults - 15.
+ // Setting `Delimiter`.
+ // MaxUploads is set more than number of meta data entries in the result.
+ // Expecting the result to contain one MultipartInfo entry and IsTruncated to be false.
+ {
+ MaxUploads: 2,
+ Delimiter: SlashSeparator,
+ Prefix: "",
+ IsTruncated: false,
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[0],
+ },
+ },
+ },
+ // listMultipartResults - 16.
+ // Testing for listing of 3 uploadID's for a given object.
+ // Will be used to list on bucketNames[1].
+ {
+ MaxUploads: 100,
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[1],
+ },
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[2],
+ },
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[3],
+ },
+ },
+ },
+ // listMultipartResults - 17.
+ // Testing for listing of 3 uploadID's (uploadIDs[1-3]) for a given object with uploadID Marker set.
+ // uploadIDs[1] is set as UploadMarker, Expecting it to be skipped in the result.
+ // uploadIDs[2] and uploadIDs[3] are expected to be in the result.
+ // Istruncted is expected to be false.
+ // Will be used to list on bucketNames[1].
+ {
+ MaxUploads: 100,
+ KeyMarker: "minio-object-1.txt",
+ UploadIDMarker: uploadIDs[1],
+ IsTruncated: false,
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[2],
+ },
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[3],
+ },
+ },
+ },
+ // listMultipartResults - 18.
+ // Testing for listing of 3 uploadID's (uploadIDs[1-3]) for a given object with uploadID Marker set.
+ // uploadIDs[2] is set as UploadMarker, Expecting it to be skipped in the result.
+ // Only uploadIDs[3] are expected to be in the result.
+ // Istruncted is expected to be false.
+ // Will be used to list on bucketNames[1].
+ {
+ MaxUploads: 100,
+ KeyMarker: "minio-object-1.txt",
+ UploadIDMarker: uploadIDs[2],
+ IsTruncated: false,
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[3],
+ },
+ },
+ },
+ // listMultipartResults - 19.
+ // Testing for listing of 3 uploadID's for a given object, setting maxKeys to be 2.
+ // There are 3 MultipartInfo in the result (uploadIDs[1-3]), it should be truncated to 2.
+ // Since there is only single object for bucketNames[1], the NextKeyMarker is set to its name.
+ // The last entry in the result, uploadIDs[2], that is should be set as NextUploadIDMarker.
+ // Will be used to list on bucketNames[1].
+ {
+ MaxUploads: 2,
+ IsTruncated: true,
+ NextKeyMarker: objectNames[0],
+ NextUploadIDMarker: uploadIDs[2],
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[1],
+ },
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[2],
+ },
+ },
+ },
+ // listMultipartResults - 20.
+ // Testing for listing of 3 uploadID's for a given object, setting maxKeys to be 1.
+ // There are 3 MultipartInfo in the result (uploadIDs[1-3]), it should be truncated to 1.
+ // The last entry in the result, uploadIDs[1], that is should be set as NextUploadIDMarker.
+ // Will be used to list on bucketNames[1].
+ {
+ MaxUploads: 1,
+ IsTruncated: true,
+ NextKeyMarker: objectNames[0],
+ NextUploadIDMarker: uploadIDs[1],
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[1],
+ },
+ },
+ },
+ // listMultipartResults - 21.
+ // Testing for listing of 3 uploadID's for a given object, setting maxKeys to be 3.
+ // There are 3 MultipartInfo in the result (uploadIDs[1-3]), hence no truncation is expected.
+ // Since all the MultipartInfo is listed, expecting no values for NextUploadIDMarker and NextKeyMarker.
+ // Will be used to list on bucketNames[1].
+ {
+ MaxUploads: 3,
+ IsTruncated: false,
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[1],
+ },
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[2],
+ },
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[3],
+ },
+ },
+ },
+ // listMultipartResults - 22.
+ // Testing for listing of 3 uploadID's for a given object, setting `prefix` to be "min".
+ // Will be used to list on bucketNames[1].
+ {
+ MaxUploads: 10,
+ IsTruncated: false,
+ Prefix: "min",
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[1],
+ },
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[2],
+ },
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[3],
+ },
+ },
+ },
+ // listMultipartResults - 23.
+ // Testing for listing of 3 uploadID's for a given object
+ // setting `prefix` to be "orange".
+ // Will be used to list on bucketNames[1].
+ {
+ MaxUploads: 10,
+ IsTruncated: false,
+ Prefix: "orange",
+ },
+ // listMultipartResults - 24.
+ // Testing for listing of 3 uploadID's for a given object.
+ // setting `prefix` to be "Asia".
+ // Will be used to list on bucketNames[1].
+ {
+ MaxUploads: 10,
+ IsTruncated: false,
+ Prefix: "Asia",
+ },
+ // listMultipartResults - 25.
+ // Testing for listing of 3 uploadID's for a given object.
+ // setting `prefix` and uploadIDMarker.
+ // Will be used to list on bucketNames[1].
+ {
+ MaxUploads: 10,
+ KeyMarker: "minio-object-1.txt",
+ IsTruncated: false,
+ Prefix: "min",
+ UploadIDMarker: uploadIDs[1],
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[2],
+ },
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[3],
+ },
+ },
+ },
+
+ // Operations on bucket 2.
+ // listMultipartResults - 26.
+ // checking listing everything.
+ {
+ MaxUploads: 100,
+ IsTruncated: false,
+
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[4],
+ },
+ {
+ Object: objectNames[1],
+ UploadID: uploadIDs[5],
+ },
+ {
+ Object: objectNames[2],
+ UploadID: uploadIDs[6],
+ },
+ {
+ Object: objectNames[3],
+ UploadID: uploadIDs[7],
+ },
+ {
+ Object: objectNames[4],
+ UploadID: uploadIDs[8],
+ },
+ {
+ Object: objectNames[5],
+ UploadID: uploadIDs[9],
+ },
+ },
+ },
+ // listMultipartResults - 27.
+ // listing with `prefix` "min".
+ {
+ MaxUploads: 100,
+ IsTruncated: false,
+ Prefix: "min",
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[4],
+ },
+ {
+ Object: objectNames[1],
+ UploadID: uploadIDs[5],
+ },
+ },
+ },
+ // listMultipartResults - 28.
+ // listing with `prefix` "ney".
+ {
+ MaxUploads: 100,
+ IsTruncated: false,
+ Prefix: "ney",
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[2],
+ UploadID: uploadIDs[6],
+ },
+ {
+ Object: objectNames[3],
+ UploadID: uploadIDs[7],
+ },
+ },
+ },
+ // listMultipartResults - 29.
+ // listing with `prefix` "parrot".
+ {
+ MaxUploads: 100,
+ IsTruncated: false,
+ Prefix: "parrot",
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[4],
+ UploadID: uploadIDs[8],
+ },
+ {
+ Object: objectNames[5],
+ UploadID: uploadIDs[9],
+ },
+ },
+ },
+ // listMultipartResults - 30.
+ // listing with `prefix` "neymar.jpeg".
+ // prefix set to object name.
+ {
+ MaxUploads: 100,
+ IsTruncated: false,
+ Prefix: "neymar.jpeg",
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[3],
+ UploadID: uploadIDs[7],
+ },
+ },
+ },
+
+ // listMultipartResults - 31.
+ // checking listing with marker set to 3.
+ // `NextUploadIDMarker` is expected to be set on last uploadID in the result.
+ // `NextKeyMarker` is expected to be set on the last object key in the list.
+ {
+ MaxUploads: 3,
+ IsTruncated: true,
+ NextUploadIDMarker: uploadIDs[6],
+ NextKeyMarker: objectNames[2],
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[4],
+ },
+ {
+ Object: objectNames[1],
+ UploadID: uploadIDs[5],
+ },
+ {
+ Object: objectNames[2],
+ UploadID: uploadIDs[6],
+ },
+ },
+ },
+ // listMultipartResults - 32.
+ // checking listing with marker set to no of objects in the list.
+ // `NextUploadIDMarker` is expected to be empty since all results are listed.
+ // `NextKeyMarker` is expected to be empty since all results are listed.
+ {
+ MaxUploads: 6,
+ IsTruncated: false,
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[0],
+ UploadID: uploadIDs[4],
+ },
+ {
+ Object: objectNames[1],
+ UploadID: uploadIDs[5],
+ },
+ {
+ Object: objectNames[2],
+ UploadID: uploadIDs[6],
+ },
+ {
+ Object: objectNames[3],
+ UploadID: uploadIDs[7],
+ },
+ {
+ Object: objectNames[4],
+ UploadID: uploadIDs[8],
+ },
+ {
+ Object: objectNames[5],
+ UploadID: uploadIDs[9],
+ },
+ },
+ },
+ // listMultipartResults - 33.
+ // checking listing with `UploadIDMarker` set.
+ {
+ MaxUploads: 10,
+ IsTruncated: false,
+ UploadIDMarker: uploadIDs[6],
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[3],
+ UploadID: uploadIDs[7],
+ },
+ {
+ Object: objectNames[4],
+ UploadID: uploadIDs[8],
+ },
+ {
+ Object: objectNames[5],
+ UploadID: uploadIDs[9],
+ },
+ },
+ },
+ // listMultipartResults - 34.
+ // checking listing with `KeyMarker` set.
+ {
+ MaxUploads: 10,
+ IsTruncated: false,
+ KeyMarker: objectNames[3],
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[4],
+ UploadID: uploadIDs[8],
+ },
+ {
+ Object: objectNames[5],
+ UploadID: uploadIDs[9],
+ },
+ },
+ },
+ // listMultipartResults - 35.
+ // Checking listing with `Prefix` and `KeyMarker`.
+ // No upload MultipartInfo in the result expected since KeyMarker is set to last Key in the result.
+ {
+ MaxUploads: 10,
+ IsTruncated: false,
+ Prefix: "minio-object",
+ KeyMarker: objectNames[1],
+ },
+ // listMultipartResults - 36.
+ // checking listing with `Prefix` and `UploadIDMarker` set.
+ {
+ MaxUploads: 10,
+ IsTruncated: false,
+ Prefix: "minio",
+ UploadIDMarker: uploadIDs[4],
+ Uploads: []MultipartInfo{
+ {
+ Object: objectNames[1],
+ UploadID: uploadIDs[5],
+ },
+ },
+ },
+ // listMultipartResults - 37.
+ // Checking listing with `KeyMarker` and `UploadIDMarker` set.
+ {
+ MaxUploads: 10,
+ IsTruncated: false,
+ KeyMarker: "minio-object.txt",
+ UploadIDMarker: uploadIDs[5],
+ },
+ }
+
+ // Collection of non-exhaustive ListMultipartUploads test cases, valid errors
+ // and success responses.
+ testCases := []struct {
+ // Inputs to ListMultipartUploads.
+ bucket string
+ prefix string
+ keyMarker string
+ uploadIDMarker string
+ delimiter string
+ maxUploads int
+ // Expected output of ListMultipartUploads.
+ expectedResult ListMultipartsInfo
+ expectedErr error
+ // Flag indicating whether the test is expected to pass or not.
+ shouldPass bool
+ }{
+ // Test cases with invalid bucket names ( Test number 1-4 ).
+ {".test", "", "", "", "", 0, ListMultipartsInfo{}, BucketNameInvalid{Bucket: ".test"}, false},
+ {"Test", "", "", "", "", 0, ListMultipartsInfo{}, BucketNameInvalid{Bucket: "Test"}, false},
+ {"---", "", "", "", "", 0, ListMultipartsInfo{}, BucketNameInvalid{Bucket: "---"}, false},
+ {"ad", "", "", "", "", 0, ListMultipartsInfo{}, BucketNameInvalid{Bucket: "ad"}, false},
+ // Valid bucket names, but they do not exist (Test number 5-7).
+ {"volatile-bucket-1", "", "", "", "", 0, ListMultipartsInfo{}, BucketNotFound{Bucket: "volatile-bucket-1"}, false},
+ {"volatile-bucket-2", "", "", "", "", 0, ListMultipartsInfo{}, BucketNotFound{Bucket: "volatile-bucket-2"}, false},
+ {"volatile-bucket-3", "", "", "", "", 0, ListMultipartsInfo{}, BucketNotFound{Bucket: "volatile-bucket-3"}, false},
+ // Valid, existing bucket, delimiter not supported, returns empty values (Test number 8-9).
+ {bucketNames[0], "", "", "", "*", 0, ListMultipartsInfo{Delimiter: "*"}, nil, true},
+ {bucketNames[0], "", "", "", "-", 0, ListMultipartsInfo{Delimiter: "-"}, nil, true},
+ // If marker is *after* the last possible object from the prefix it should return an empty list.
+ {
+ bucketNames[0], "Asia", "europe-object", "", "", 0,
+ ListMultipartsInfo{KeyMarker: "europe-object", Prefix: "Asia", IsTruncated: false},
+ nil, true,
+ },
+ // Setting an invalid combination of uploadIDMarker and Marker (Test number 11-12).
+ {
+ bucketNames[0], "asia", "asia/europe/", "abc", "", 0,
+ ListMultipartsInfo{},
+ fmt.Errorf("Invalid combination of uploadID marker '%s' and marker '%s'", "abc", "asia/europe/"), false,
+ },
+ {
+ // Contains a base64 padding character
+ bucketNames[0], "asia", "asia/europe", "abc=", "", 0,
+ ListMultipartsInfo{},
+ fmt.Errorf("Malformed upload id %s", "abc="), false,
+ },
+
+ // Setting up valid case of ListMultiPartUploads.
+ // Test case with multiple parts for a single uploadID (Test number 13).
+ {bucketNames[0], "", "", "", "", 100, listMultipartResults[0], nil, true},
+ // Test with a KeyMarker (Test number 14-17).
+ {bucketNames[0], "", "minio-object-1.txt", "", "", 100, listMultipartResults[1], nil, true},
+ {bucketNames[0], "", "orange", "", "", 100, listMultipartResults[2], nil, true},
+ {bucketNames[0], "", "orange", "", "", 1, listMultipartResults[3], nil, true},
+ {bucketNames[0], "", "min", "", "", 10, listMultipartResults[4], nil, true},
+ // Test case with keyMarker set equal to number of parts in the result. (Test number 18).
+ {bucketNames[0], "", "min", "", "", 1, listMultipartResults[5], nil, true},
+ // Test case with keyMarker set to 0. (Test number 19).
+ {bucketNames[0], "", "min", "", "", 0, listMultipartResults[6], nil, true},
+ // Test case with keyMarker less than 0. (Test number 20).
+ // {bucketNames[0], "", "min", "", "", -1, listMultipartResults[7], nil, true},
+ // The result contains only one entry. The KeyPrefix is set to the object name in the result.
+ // Expecting the result to skip the KeyPrefix entry in the result (Test number 21).
+ {bucketNames[0], "", "minio-object", "", "", 2, listMultipartResults[8], nil, true},
+ // Test case containing prefix values.
+ // Setting prefix to be equal to object name.(Test number 22).
+ {bucketNames[0], "minio-object-1.txt", "", "", "", 2, listMultipartResults[9], nil, true},
+ // Setting `prefix` to contain the object name as its prefix (Test number 23).
+ {bucketNames[0], "min", "", "", "", 2, listMultipartResults[10], nil, true},
+ // Setting `prefix` to contain the object name as its prefix (Test number 24).
+ {bucketNames[0], "min", "", "", "", 1, listMultipartResults[11], nil, true},
+ // Setting `prefix` to not to contain the object name as its prefix (Test number 25-26).
+ {bucketNames[0], "orange", "", "", "", 2, listMultipartResults[12], nil, true},
+ {bucketNames[0], "Asia", "", "", "", 2, listMultipartResults[13], nil, true},
+ // setting delimiter (Test number 27).
+ {bucketNames[0], "", "", "", SlashSeparator, 2, listMultipartResults[14], nil, true},
+ // Test case with multiple uploadID listing for given object (Test number 28).
+ {bucketNames[1], "", "", "", "", 100, listMultipartResults[15], nil, true},
+ // Test case with multiple uploadID listing for given object, but uploadID marker set.
+ // Testing whether the marker entry is skipped (Test number 29-30).
+ {bucketNames[1], "", "minio-object-1.txt", uploadIDs[1], "", 100, listMultipartResults[16], nil, true},
+ {bucketNames[1], "", "minio-object-1.txt", uploadIDs[2], "", 100, listMultipartResults[17], nil, true},
+ // Test cases with multiple uploadID listing for a given object (Test number 31-32).
+ // MaxKeys set to values lesser than the number of entries in the MultipartInfo.
+ // IsTruncated is expected to be true.
+ {bucketNames[1], "", "", "", "", 2, listMultipartResults[18], nil, true},
+ {bucketNames[1], "", "", "", "", 1, listMultipartResults[19], nil, true},
+ // MaxKeys set to the value which is equal to no of entries in the MultipartInfo (Test number 33).
+ // In case of bucketNames[1], there are 3 entries.
+ // Since all available entries are listed, IsTruncated is expected to be false
+ // and NextMarkers are expected to empty.
+ {bucketNames[1], "", "", "", "", 3, listMultipartResults[20], nil, true},
+ // Adding prefix (Test number 34-36).
+ {bucketNames[1], "min", "", "", "", 10, listMultipartResults[21], nil, true},
+ {bucketNames[1], "orange", "", "", "", 10, listMultipartResults[22], nil, true},
+ {bucketNames[1], "Asia", "", "", "", 10, listMultipartResults[23], nil, true},
+ // Test case with `Prefix` and `UploadIDMarker` (Test number 37).
+ {bucketNames[1], "min", "minio-object-1.txt", uploadIDs[1], "", 10, listMultipartResults[24], nil, true},
+ // Test case for bucket with multiple objects in it.
+ // Bucket used : `bucketNames[2]`.
+ // Objects used: `objectNames[1-5]`.
+ // UploadId's used: uploadIds[4-8].
+ // (Test number 39).
+ {bucketNames[2], "", "", "", "", 100, listMultipartResults[25], nil, true},
+ // Test cases with prefixes.
+ // Testing listing with prefix set to "min" (Test number 40) .
+ {bucketNames[2], "min", "", "", "", 100, listMultipartResults[26], nil, true},
+ // Testing listing with prefix set to "ney" (Test number 41).
+ {bucketNames[2], "ney", "", "", "", 100, listMultipartResults[27], nil, true},
+ // Testing listing with prefix set to "par" (Test number 42).
+ {bucketNames[2], "parrot", "", "", "", 100, listMultipartResults[28], nil, true},
+ // Testing listing with prefix set to object name "neymar.jpeg" (Test number 43).
+ {bucketNames[2], "neymar.jpeg", "", "", "", 100, listMultipartResults[29], nil, true},
+ // Testing listing with `MaxUploads` set to 3 (Test number 44).
+ {bucketNames[2], "", "", "", "", 3, listMultipartResults[30], nil, true},
+ // In case of bucketNames[2], there are 6 entries (Test number 45).
+ // Since all available entries are listed, IsTruncated is expected to be false
+ // and NextMarkers are expected to empty.
+ {bucketNames[2], "", "", "", "", 6, listMultipartResults[31], nil, true},
+ // Test case with `KeyMarker` (Test number 47).
+ {bucketNames[2], "", objectNames[3], "", "", 10, listMultipartResults[33], nil, true},
+ // Test case with `prefix` and `KeyMarker` (Test number 48).
+ {bucketNames[2], "minio-object", objectNames[1], "", "", 10, listMultipartResults[34], nil, true},
+ }
+
+ for i, testCase := range testCases {
+ // fmt.Println(i+1, testCase) // uncomment to peek into the test cases.
+ actualResult, actualErr := obj.ListMultipartUploads(context.Background(), testCase.bucket, testCase.prefix, testCase.keyMarker, testCase.uploadIDMarker, testCase.delimiter, testCase.maxUploads)
+ if actualErr != nil && testCase.shouldPass {
+ t.Errorf("Test %d: %s: Expected to pass, but failed with: %s", i+1, instanceType, actualErr.Error())
+ }
+ if actualErr == nil && !testCase.shouldPass {
+ t.Errorf("Test %d: %s: Expected to fail with \"%s\", but passed instead", i+1, instanceType, testCase.expectedErr.Error())
+ }
+ // Failed as expected, but does it fail for the expected reason.
+ if actualErr != nil && !testCase.shouldPass {
+ if !strings.Contains(actualErr.Error(), testCase.expectedErr.Error()) {
+ t.Errorf("Test %d: %s: Expected to fail with error \"%s\", but instead failed with error \"%s\" instead", i+1, instanceType, testCase.expectedErr.Error(), actualErr.Error())
+ }
+ }
+ // Passes as expected, but asserting the results.
+ if actualErr == nil && testCase.shouldPass {
+ expectedResult := testCase.expectedResult
+ // Asserting the MaxUploads.
+ if actualResult.MaxUploads != expectedResult.MaxUploads {
+ t.Errorf("Test %d: %s: Expected the MaxUploads to be %d, but instead found it to be %d", i+1, instanceType, expectedResult.MaxUploads, actualResult.MaxUploads)
+ }
+ // Asserting Prefix.
+ if actualResult.Prefix != expectedResult.Prefix {
+ t.Errorf("Test %d: %s: Expected Prefix to be \"%s\", but instead found it to be \"%s\"", i+1, instanceType, expectedResult.Prefix, actualResult.Prefix)
+ }
+ // Asserting Delimiter.
+ if actualResult.Delimiter != expectedResult.Delimiter {
+ t.Errorf("Test %d: %s: Expected Delimiter to be \"%s\", but instead found it to be \"%s\"", i+1, instanceType, expectedResult.Delimiter, actualResult.Delimiter)
+ }
+ // Asserting the keyMarker.
+ if actualResult.KeyMarker != expectedResult.KeyMarker {
+ t.Errorf("Test %d: %s: Expected keyMarker to be \"%s\", but instead found it to be \"%s\"", i+1, instanceType, expectedResult.KeyMarker, actualResult.KeyMarker)
+ }
+ }
+ }
+}
+
+// Wrapper for calling TestListObjectPartsStale tests for both Erasure multiple disks and single node setup.
+func TestListObjectPartsStale(t *testing.T) {
+ ExecObjectLayerDiskAlteredTest(t, testListObjectPartsStale)
+}
+
+// testListObjectPartsStale - Tests validate listing of object parts when parts are stale
+func testListObjectPartsStale(obj ObjectLayer, instanceType string, disks []string, t *testing.T) {
+ bucketNames := []string{"minio-bucket", "minio-2-bucket"}
+ objectNames := []string{"minio-object-1.txt"}
+ uploadIDs := []string{}
+
+ globalStorageClass.Update(storageclass.Config{
+ RRS: storageclass.StorageClass{
+ Parity: 2,
+ },
+ Standard: storageclass.StorageClass{
+ Parity: 4,
+ },
+ })
+
+ // bucketnames[0].
+ // objectNames[0].
+ // uploadIds [0].
+ // Create bucket before initiating NewMultipartUpload.
+ err := obj.MakeBucket(context.Background(), bucketNames[0], MakeBucketOptions{})
+ if err != nil {
+ // Failed to create newbucket, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ opts := ObjectOptions{}
+ // Initiate Multipart Upload on the above created bucket.
+ res, err := obj.NewMultipartUpload(context.Background(), bucketNames[0], objectNames[0], opts)
+ if err != nil {
+ // Failed to create NewMultipartUpload, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ z := obj.(*erasureServerPools)
+ er := z.serverPools[0].sets[0]
+
+ uploadIDs = append(uploadIDs, res.UploadID)
+
+ // Create multipart parts.
+ // Need parts to be uploaded before MultipartLists can be called and tested.
+ createPartCases := []struct {
+ bucketName string
+ objName string
+ uploadID string
+ PartID int
+ inputReaderData string
+ inputMd5 string
+ inputDataSize int64
+ expectedMd5 string
+ }{
+ // Case 1-4.
+ // Creating sequence of parts for same uploadID.
+ // Used to ensure that the ListMultipartResult produces one output for the four parts uploaded below for the given upload ID.
+ {bucketNames[0], objectNames[0], uploadIDs[0], 1, "abcd", "e2fc714c4727ee9395f324cd2e7f331f", int64(len("abcd")), "e2fc714c4727ee9395f324cd2e7f331f"},
+ {bucketNames[0], objectNames[0], uploadIDs[0], 2, "efgh", "1f7690ebdd9b4caf8fab49ca1757bf27", int64(len("efgh")), "1f7690ebdd9b4caf8fab49ca1757bf27"},
+ {bucketNames[0], objectNames[0], uploadIDs[0], 3, "ijkl", "09a0877d04abf8759f99adec02baf579", int64(len("abcd")), "09a0877d04abf8759f99adec02baf579"},
+ {bucketNames[0], objectNames[0], uploadIDs[0], 4, "mnop", "e132e96a5ddad6da8b07bba6f6131fef", int64(len("abcd")), "e132e96a5ddad6da8b07bba6f6131fef"},
+ }
+ sha256sum := ""
+ // Iterating over creatPartCases to generate multipart chunks.
+ for _, testCase := range createPartCases {
+ _, err := obj.PutObjectPart(context.Background(), testCase.bucketName, testCase.objName, testCase.uploadID, testCase.PartID, mustGetPutObjReader(t, bytes.NewBufferString(testCase.inputReaderData), testCase.inputDataSize, testCase.inputMd5, sha256sum), opts)
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ }
+
+ erasureDisks := er.getDisks()
+ uploadIDPath := er.getUploadIDDir(bucketNames[0], objectNames[0], uploadIDs[0])
+ dataDirs, err := erasureDisks[0].ListDir(context.Background(), minioMetaMultipartBucket, minioMetaMultipartBucket, uploadIDPath, -1)
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ var dataDir string
+ for _, folder := range dataDirs {
+ if strings.HasSuffix(folder, SlashSeparator) {
+ dataDir = folder
+ break
+ }
+ }
+
+ toDel := (len(erasureDisks) / 2) + 1
+ for _, disk := range erasureDisks[:toDel] {
+ disk.DeleteBulk(context.Background(), minioMetaMultipartBucket, []string{pathJoin(uploadIDPath, dataDir, "part.2")}...)
+ }
+
+ partInfos := []ListPartsInfo{
+ // partinfos - 0.
+ {
+ Bucket: bucketNames[0],
+ Object: objectNames[0],
+ MaxParts: 10,
+ UploadID: uploadIDs[0],
+ Parts: []PartInfo{
+ {
+ PartNumber: 1,
+ Size: 4,
+ ETag: "e2fc714c4727ee9395f324cd2e7f331f",
+ },
+ {
+ PartNumber: 3,
+ Size: 4,
+ ETag: "09a0877d04abf8759f99adec02baf579",
+ },
+ {
+ PartNumber: 4,
+ Size: 4,
+ ETag: "e132e96a5ddad6da8b07bba6f6131fef",
+ },
+ },
+ },
+ // partinfos - 1.
+ {
+ Bucket: bucketNames[0],
+ Object: objectNames[0],
+ MaxParts: 3,
+ UploadID: uploadIDs[0],
+ Parts: []PartInfo{
+ {
+ PartNumber: 1,
+ Size: 4,
+ ETag: "e2fc714c4727ee9395f324cd2e7f331f",
+ },
+ {
+ PartNumber: 3,
+ Size: 4,
+ ETag: "09a0877d04abf8759f99adec02baf579",
+ },
+ {
+ PartNumber: 4,
+ Size: 4,
+ ETag: "e132e96a5ddad6da8b07bba6f6131fef",
+ },
+ },
+ },
+ // partinfos - 2.
+ {
+ Bucket: bucketNames[0],
+ Object: objectNames[0],
+ MaxParts: 2,
+ NextPartNumberMarker: 3,
+ IsTruncated: true,
+ UploadID: uploadIDs[0],
+ Parts: []PartInfo{
+ {
+ PartNumber: 1,
+ Size: 4,
+ ETag: "e2fc714c4727ee9395f324cd2e7f331f",
+ },
+ {
+ PartNumber: 3,
+ Size: 4,
+ ETag: "09a0877d04abf8759f99adec02baf579",
+ },
+ },
+ },
+ // partinfos - 3.
+ {
+ Bucket: bucketNames[0],
+ Object: objectNames[0],
+ MaxParts: 2,
+ IsTruncated: false,
+ UploadID: uploadIDs[0],
+ PartNumberMarker: 3,
+ Parts: []PartInfo{
+ {
+ PartNumber: 4,
+ Size: 4,
+ ETag: "e132e96a5ddad6da8b07bba6f6131fef",
+ },
+ },
+ },
+ // partinfos - 4.
+ {
+ Bucket: bucketNames[0],
+ Object: objectNames[0],
+ MaxParts: 2,
+ IsTruncated: false,
+ UploadID: uploadIDs[0],
+ PartNumberMarker: 4,
+ },
+ // partinfos - 5.
+ {
+ Bucket: bucketNames[0],
+ Object: objectNames[0],
+ MaxParts: 2,
+ IsTruncated: false,
+ UploadID: uploadIDs[0],
+ PartNumberMarker: 100,
+ },
+ }
+
+ // Collection of non-exhaustive ListObjectParts test cases, valid errors
+ // and success responses.
+ testCases := []struct {
+ bucket string
+ object string
+ uploadID string
+ partNumberMarker int
+ maxParts int
+ // Expected output of ListPartsInfo.
+ expectedResult ListPartsInfo
+ expectedErr error
+ // Flag indicating whether the test is expected to pass or not.
+ shouldPass bool
+ }{
+ // Test cases with invalid bucket names (Test number 1-4).
+ {".test", "", "", 0, 0, ListPartsInfo{}, BucketNameInvalid{Bucket: ".test"}, false},
+ {"Test", "", "", 0, 0, ListPartsInfo{}, BucketNameInvalid{Bucket: "Test"}, false},
+ {"---", "", "", 0, 0, ListPartsInfo{}, BucketNameInvalid{Bucket: "---"}, false},
+ {"ad", "", "", 0, 0, ListPartsInfo{}, BucketNameInvalid{Bucket: "ad"}, false},
+ // Test cases for listing uploadID with single part.
+ // Valid bucket names, but they do not exist (Test number 5-7).
+ {"volatile-bucket-1", "test1", "", 0, 0, ListPartsInfo{}, BucketNotFound{Bucket: "volatile-bucket-1"}, false},
+ {"volatile-bucket-2", "test1", "", 0, 0, ListPartsInfo{}, BucketNotFound{Bucket: "volatile-bucket-2"}, false},
+ {"volatile-bucket-3", "test1", "", 0, 0, ListPartsInfo{}, BucketNotFound{Bucket: "volatile-bucket-3"}, false},
+ // Test case for Asserting for invalid objectName (Test number 8).
+ {bucketNames[0], "", "", 0, 0, ListPartsInfo{}, ObjectNameInvalid{Bucket: bucketNames[0]}, false},
+ // Asserting for Invalid UploadID (Test number 9).
+ {bucketNames[0], objectNames[0], "abc", 0, 0, ListPartsInfo{}, InvalidUploadID{UploadID: "abc"}, false},
+ // Test case for uploadID with multiple parts (Test number 12).
+ {bucketNames[0], objectNames[0], uploadIDs[0], 0, 10, partInfos[0], nil, true},
+ // Test case with maxParts set to less than number of parts (Test number 13).
+ {bucketNames[0], objectNames[0], uploadIDs[0], 0, 3, partInfos[1], nil, true},
+ // Test case with partNumberMarker set (Test number 14).
+ {bucketNames[0], objectNames[0], uploadIDs[0], 0, 2, partInfos[2], nil, true},
+ // Test case with partNumberMarker set (Test number 15).
+ {bucketNames[0], objectNames[0], uploadIDs[0], 3, 2, partInfos[3], nil, true},
+ // Test case with partNumberMarker set (Test number 16).
+ {bucketNames[0], objectNames[0], uploadIDs[0], 4, 2, partInfos[4], nil, true},
+ // Test case with partNumberMarker set (Test number 17).
+ {bucketNames[0], objectNames[0], uploadIDs[0], 100, 2, partInfos[5], nil, true},
+ }
+
+ for i, testCase := range testCases {
+ actualResult, actualErr := obj.ListObjectParts(context.Background(), testCase.bucket, testCase.object, testCase.uploadID, testCase.partNumberMarker, testCase.maxParts, ObjectOptions{})
+ if actualErr != nil && testCase.shouldPass {
+ t.Errorf("Test %d: %s: Expected to pass, but failed with: %s", i+1, instanceType, actualErr.Error())
+ }
+ if actualErr == nil && !testCase.shouldPass {
+ t.Errorf("Test %d: %s: Expected to fail with \"%s\", but passed instead", i+1, instanceType, testCase.expectedErr.Error())
+ }
+ // Failed as expected, but does it fail for the expected reason.
+ if actualErr != nil && !testCase.shouldPass {
+ if !strings.Contains(actualErr.Error(), testCase.expectedErr.Error()) {
+ t.Errorf("Test %d: %s: Expected to fail with error \"%s\", but instead failed with error \"%s\" instead", i+1, instanceType, testCase.expectedErr, actualErr)
+ }
+ }
+ // Passes as expected, but asserting the results.
+ if actualErr == nil && testCase.shouldPass {
+ expectedResult := testCase.expectedResult
+ // Asserting the MaxParts.
+ if actualResult.MaxParts != expectedResult.MaxParts {
+ t.Errorf("Test %d: %s: Expected the MaxParts to be %d, but instead found it to be %d", i+1, instanceType, expectedResult.MaxParts, actualResult.MaxParts)
+ }
+ // Asserting Object Name.
+ if actualResult.Object != expectedResult.Object {
+ t.Errorf("Test %d: %s: Expected Object name to be \"%s\", but instead found it to be \"%s\"", i+1, instanceType, expectedResult.Object, actualResult.Object)
+ }
+ // Asserting UploadID.
+ if actualResult.UploadID != expectedResult.UploadID {
+ t.Errorf("Test %d: %s: Expected UploadID to be \"%s\", but instead found it to be \"%s\"", i+1, instanceType, expectedResult.UploadID, actualResult.UploadID)
+ }
+ // Asserting NextPartNumberMarker.
+ if actualResult.NextPartNumberMarker != expectedResult.NextPartNumberMarker {
+ t.Errorf("Test %d: %s: Expected NextPartNumberMarker to be \"%d\", but instead found it to be \"%d\"", i+1, instanceType, expectedResult.NextPartNumberMarker, actualResult.NextPartNumberMarker)
+ }
+ // Asserting PartNumberMarker.
+ if actualResult.PartNumberMarker != expectedResult.PartNumberMarker {
+ t.Errorf("Test %d: %s: Expected PartNumberMarker to be \"%d\", but instead found it to be \"%d\"", i+1, instanceType, expectedResult.PartNumberMarker, actualResult.PartNumberMarker)
+ }
+ // Asserting the BucketName.
+ if actualResult.Bucket != expectedResult.Bucket {
+ t.Errorf("Test %d: %s: Expected Bucket to be \"%s\", but instead found it to be \"%s\"", i+1, instanceType, expectedResult.Bucket, actualResult.Bucket)
+ }
+ // Asserting IsTruncated.
+ if actualResult.IsTruncated != testCase.expectedResult.IsTruncated {
+ t.Errorf("Test %d: %s: Expected IsTruncated to be \"%v\", but found it to \"%v\"", i+1, instanceType, expectedResult.IsTruncated, actualResult.IsTruncated)
+ }
+ // Asserting the number of Parts.
+ if len(expectedResult.Parts) != len(actualResult.Parts) {
+ t.Errorf("Test %d: %s: Expected the result to contain info of %d Parts, but found %d instead", i+1, instanceType, len(expectedResult.Parts), len(actualResult.Parts))
+ } else {
+ // Iterating over the partInfos and asserting the fields.
+ for j, actualMetaData := range actualResult.Parts {
+ // Asserting the PartNumber in the PartInfo.
+ if actualMetaData.PartNumber != expectedResult.Parts[j].PartNumber {
+ t.Errorf("Test %d: %s: Part %d: Expected PartNumber to be \"%d\", but instead found \"%d\"", i+1, instanceType, j+1, expectedResult.Parts[j].PartNumber, actualMetaData.PartNumber)
+ }
+ // Asserting the Size in the PartInfo.
+ if actualMetaData.Size != expectedResult.Parts[j].Size {
+ t.Errorf("Test %d: %s: Part %d: Expected Part Size to be \"%d\", but instead found \"%d\"", i+1, instanceType, j+1, expectedResult.Parts[j].Size, actualMetaData.Size)
+ }
+ // Asserting the ETag in the PartInfo.
+ if actualMetaData.ETag != expectedResult.Parts[j].ETag {
+ t.Errorf("Test %d: %s: Part %d: Expected Etag to be \"%s\", but instead found \"%s\"", i+1, instanceType, j+1, expectedResult.Parts[j].ETag, actualMetaData.ETag)
+ }
+ }
+ }
+ }
+ }
+}
+
+// Wrapper for calling TestListObjectPartsDiskNotFound tests for both Erasure multiple disks and single node setup.
+func TestListObjectPartsDiskNotFound(t *testing.T) {
+ ExecObjectLayerDiskAlteredTest(t, testListObjectPartsDiskNotFound)
+}
+
+// testListObjectParts - Tests validate listing of object parts when disks go offline.
+func testListObjectPartsDiskNotFound(obj ObjectLayer, instanceType string, disks []string, t *testing.T) {
+ bucketNames := []string{"minio-bucket", "minio-2-bucket"}
+ objectNames := []string{"minio-object-1.txt"}
+ uploadIDs := []string{}
+
+ globalStorageClass.Update(storageclass.Config{
+ RRS: storageclass.StorageClass{
+ Parity: 2,
+ },
+ Standard: storageclass.StorageClass{
+ Parity: 4,
+ },
+ })
+
+ // bucketnames[0].
+ // objectNames[0].
+ // uploadIds [0].
+ // Create bucket before initiating NewMultipartUpload.
+ err := obj.MakeBucket(context.Background(), bucketNames[0], MakeBucketOptions{})
+ if err != nil {
+ // Failed to create newbucket, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ opts := ObjectOptions{}
+ // Initiate Multipart Upload on the above created bucket.
+ res, err := obj.NewMultipartUpload(context.Background(), bucketNames[0], objectNames[0], opts)
+ if err != nil {
+ // Failed to create NewMultipartUpload, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ z := obj.(*erasureServerPools)
+ er := z.serverPools[0].sets[0]
+
+ erasureDisks := er.getDisks()
+ ridx := rand.Intn(len(erasureDisks))
+
+ z.serverPools[0].erasureDisksMu.Lock()
+ er.getDisks = func() []StorageAPI {
+ erasureDisks[ridx] = newNaughtyDisk(erasureDisks[ridx], nil, errFaultyDisk)
+ return erasureDisks
+ }
+ z.serverPools[0].erasureDisksMu.Unlock()
+
+ uploadIDs = append(uploadIDs, res.UploadID)
+
+ // Create multipart parts.
+ // Need parts to be uploaded before MultipartLists can be called and tested.
+ createPartCases := []struct {
+ bucketName string
+ objName string
+ uploadID string
+ PartID int
+ inputReaderData string
+ inputMd5 string
+ inputDataSize int64
+ expectedMd5 string
+ }{
+ // Case 1-4.
+ // Creating sequence of parts for same uploadID.
+ // Used to ensure that the ListMultipartResult produces one output for the four parts uploaded below for the given upload ID.
+ {bucketNames[0], objectNames[0], uploadIDs[0], 1, "abcd", "e2fc714c4727ee9395f324cd2e7f331f", int64(len("abcd")), "e2fc714c4727ee9395f324cd2e7f331f"},
+ {bucketNames[0], objectNames[0], uploadIDs[0], 2, "efgh", "1f7690ebdd9b4caf8fab49ca1757bf27", int64(len("efgh")), "1f7690ebdd9b4caf8fab49ca1757bf27"},
+ {bucketNames[0], objectNames[0], uploadIDs[0], 3, "ijkl", "09a0877d04abf8759f99adec02baf579", int64(len("abcd")), "09a0877d04abf8759f99adec02baf579"},
+ {bucketNames[0], objectNames[0], uploadIDs[0], 4, "mnop", "e132e96a5ddad6da8b07bba6f6131fef", int64(len("abcd")), "e132e96a5ddad6da8b07bba6f6131fef"},
+ }
+ sha256sum := ""
+ // Iterating over creatPartCases to generate multipart chunks.
+ for _, testCase := range createPartCases {
+ _, err := obj.PutObjectPart(context.Background(), testCase.bucketName, testCase.objName, testCase.uploadID, testCase.PartID, mustGetPutObjReader(t, bytes.NewBufferString(testCase.inputReaderData), testCase.inputDataSize, testCase.inputMd5, sha256sum), opts)
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ }
+
+ partInfos := []ListPartsInfo{
+ // partinfos - 0.
+ {
+ Bucket: bucketNames[0],
+ Object: objectNames[0],
+ MaxParts: 10,
+ UploadID: uploadIDs[0],
+ Parts: []PartInfo{
+ {
+ PartNumber: 1,
+ Size: 4,
+ ETag: "e2fc714c4727ee9395f324cd2e7f331f",
+ },
+ {
+ PartNumber: 2,
+ Size: 4,
+ ETag: "1f7690ebdd9b4caf8fab49ca1757bf27",
+ },
+ {
+ PartNumber: 3,
+ Size: 4,
+ ETag: "09a0877d04abf8759f99adec02baf579",
+ },
+ {
+ PartNumber: 4,
+ Size: 4,
+ ETag: "e132e96a5ddad6da8b07bba6f6131fef",
+ },
+ },
+ },
+ // partinfos - 1.
+ {
+ Bucket: bucketNames[0],
+ Object: objectNames[0],
+ MaxParts: 3,
+ NextPartNumberMarker: 3,
+ IsTruncated: true,
+ UploadID: uploadIDs[0],
+ Parts: []PartInfo{
+ {
+ PartNumber: 1,
+ Size: 4,
+ ETag: "e2fc714c4727ee9395f324cd2e7f331f",
+ },
+ {
+ PartNumber: 2,
+ Size: 4,
+ ETag: "1f7690ebdd9b4caf8fab49ca1757bf27",
+ },
+ {
+ PartNumber: 3,
+ Size: 4,
+ ETag: "09a0877d04abf8759f99adec02baf579",
+ },
+ },
+ },
+ // partinfos - 2.
+ {
+ Bucket: bucketNames[0],
+ Object: objectNames[0],
+ MaxParts: 2,
+ IsTruncated: false,
+ UploadID: uploadIDs[0],
+ PartNumberMarker: 3,
+ Parts: []PartInfo{
+ {
+ PartNumber: 4,
+ Size: 4,
+ ETag: "e132e96a5ddad6da8b07bba6f6131fef",
+ },
+ },
+ },
+ }
+
+ // Collection of non-exhaustive ListObjectParts test cases, valid errors
+ // and success responses.
+ testCases := []struct {
+ bucket string
+ object string
+ uploadID string
+ partNumberMarker int
+ maxParts int
+ // Expected output of ListPartsInfo.
+ expectedResult ListPartsInfo
+ expectedErr error
+ // Flag indicating whether the test is expected to pass or not.
+ shouldPass bool
+ }{
+ // Test cases with invalid bucket names (Test number 1-4).
+ {".test", "", "", 0, 0, ListPartsInfo{}, BucketNameInvalid{Bucket: ".test"}, false},
+ {"Test", "", "", 0, 0, ListPartsInfo{}, BucketNameInvalid{Bucket: "Test"}, false},
+ {"---", "", "", 0, 0, ListPartsInfo{}, BucketNameInvalid{Bucket: "---"}, false},
+ {"ad", "", "", 0, 0, ListPartsInfo{}, BucketNameInvalid{Bucket: "ad"}, false},
+ // Test cases for listing uploadID with single part.
+ // Valid bucket names, but they do not exist (Test number 5-7).
+ {"volatile-bucket-1", "test1", "", 0, 0, ListPartsInfo{}, BucketNotFound{Bucket: "volatile-bucket-1"}, false},
+ {"volatile-bucket-2", "test1", "", 0, 0, ListPartsInfo{}, BucketNotFound{Bucket: "volatile-bucket-2"}, false},
+ {"volatile-bucket-3", "test1", "", 0, 0, ListPartsInfo{}, BucketNotFound{Bucket: "volatile-bucket-3"}, false},
+ // Test case for Asserting for invalid objectName (Test number 8).
+ {bucketNames[0], "", "", 0, 0, ListPartsInfo{}, ObjectNameInvalid{Bucket: bucketNames[0]}, false},
+ // Asserting for Invalid UploadID (Test number 9).
+ {bucketNames[0], objectNames[0], "abc", 0, 0, ListPartsInfo{}, InvalidUploadID{UploadID: "abc"}, false},
+ // Test case for uploadID with multiple parts (Test number 12).
+ {bucketNames[0], objectNames[0], uploadIDs[0], 0, 10, partInfos[0], nil, true},
+ // Test case with maxParts set to less than number of parts (Test number 13).
+ {bucketNames[0], objectNames[0], uploadIDs[0], 0, 3, partInfos[1], nil, true},
+ // Test case with partNumberMarker set (Test number 14)-.
+ {bucketNames[0], objectNames[0], uploadIDs[0], 3, 2, partInfos[2], nil, true},
+ }
+
+ for i, testCase := range testCases {
+ actualResult, actualErr := obj.ListObjectParts(context.Background(), testCase.bucket, testCase.object, testCase.uploadID, testCase.partNumberMarker, testCase.maxParts, ObjectOptions{})
+ if actualErr != nil && testCase.shouldPass {
+ t.Errorf("Test %d: %s: Expected to pass, but failed with: %s", i+1, instanceType, actualErr.Error())
+ }
+ if actualErr == nil && !testCase.shouldPass {
+ t.Errorf("Test %d: %s: Expected to fail with \"%s\", but passed instead", i+1, instanceType, testCase.expectedErr.Error())
+ }
+ // Failed as expected, but does it fail for the expected reason.
+ if actualErr != nil && !testCase.shouldPass {
+ if !strings.Contains(actualErr.Error(), testCase.expectedErr.Error()) {
+ t.Errorf("Test %d: %s: Expected to fail with error \"%s\", but instead failed with error \"%s\" instead", i+1, instanceType, testCase.expectedErr, actualErr)
+ }
+ }
+ // Passes as expected, but asserting the results.
+ if actualErr == nil && testCase.shouldPass {
+ expectedResult := testCase.expectedResult
+ // Asserting the MaxParts.
+ if actualResult.MaxParts != expectedResult.MaxParts {
+ t.Errorf("Test %d: %s: Expected the MaxParts to be %d, but instead found it to be %d", i+1, instanceType, expectedResult.MaxParts, actualResult.MaxParts)
+ }
+ // Asserting Object Name.
+ if actualResult.Object != expectedResult.Object {
+ t.Errorf("Test %d: %s: Expected Object name to be \"%s\", but instead found it to be \"%s\"", i+1, instanceType, expectedResult.Object, actualResult.Object)
+ }
+ // Asserting UploadID.
+ if actualResult.UploadID != expectedResult.UploadID {
+ t.Errorf("Test %d: %s: Expected UploadID to be \"%s\", but instead found it to be \"%s\"", i+1, instanceType, expectedResult.UploadID, actualResult.UploadID)
+ }
+ // Asserting NextPartNumberMarker.
+ if actualResult.NextPartNumberMarker != expectedResult.NextPartNumberMarker {
+ t.Errorf("Test %d: %s: Expected NextPartNumberMarker to be \"%d\", but instead found it to be \"%d\"", i+1, instanceType, expectedResult.NextPartNumberMarker, actualResult.NextPartNumberMarker)
+ }
+ // Asserting PartNumberMarker.
+ if actualResult.PartNumberMarker != expectedResult.PartNumberMarker {
+ t.Errorf("Test %d: %s: Expected PartNumberMarker to be \"%d\", but instead found it to be \"%d\"", i+1, instanceType, expectedResult.PartNumberMarker, actualResult.PartNumberMarker)
+ }
+ // Asserting the BucketName.
+ if actualResult.Bucket != expectedResult.Bucket {
+ t.Errorf("Test %d: %s: Expected Bucket to be \"%s\", but instead found it to be \"%s\"", i+1, instanceType, expectedResult.Bucket, actualResult.Bucket)
+ }
+ // Asserting IsTruncated.
+ if actualResult.IsTruncated != testCase.expectedResult.IsTruncated {
+ t.Errorf("Test %d: %s: Expected IsTruncated to be \"%v\", but found it to \"%v\"", i+1, instanceType, expectedResult.IsTruncated, actualResult.IsTruncated)
+ }
+ // Asserting the number of Parts.
+ if len(expectedResult.Parts) != len(actualResult.Parts) {
+ t.Errorf("Test %d: %s: Expected the result to contain info of %d Parts, but found %d instead", i+1, instanceType, len(expectedResult.Parts), len(actualResult.Parts))
+ } else {
+ // Iterating over the partInfos and asserting the fields.
+ for j, actualMetaData := range actualResult.Parts {
+ // Asserting the PartNumber in the PartInfo.
+ if actualMetaData.PartNumber != expectedResult.Parts[j].PartNumber {
+ t.Errorf("Test %d: %s: Part %d: Expected PartNumber to be \"%d\", but instead found \"%d\"", i+1, instanceType, j+1, expectedResult.Parts[j].PartNumber, actualMetaData.PartNumber)
+ }
+ // Asserting the Size in the PartInfo.
+ if actualMetaData.Size != expectedResult.Parts[j].Size {
+ t.Errorf("Test %d: %s: Part %d: Expected Part Size to be \"%d\", but instead found \"%d\"", i+1, instanceType, j+1, expectedResult.Parts[j].Size, actualMetaData.Size)
+ }
+ // Asserting the ETag in the PartInfo.
+ if actualMetaData.ETag != expectedResult.Parts[j].ETag {
+ t.Errorf("Test %d: %s: Part %d: Expected Etag to be \"%s\", but instead found \"%s\"", i+1, instanceType, j+1, expectedResult.Parts[j].ETag, actualMetaData.ETag)
+ }
+ }
+ }
+ }
+ }
+}
+
+// Wrapper for calling TestListObjectParts tests for both Erasure multiple disks and single node setup.
+func TestListObjectParts(t *testing.T) {
+ ExecObjectLayerTest(t, testListObjectParts)
+}
+
+// testListObjectParts - test validate listing of object parts.
+func testListObjectParts(obj ObjectLayer, instanceType string, t TestErrHandler) {
+ bucketNames := []string{"minio-bucket", "minio-2-bucket"}
+ objectNames := []string{"minio-object-1.txt"}
+ uploadIDs := []string{}
+ opts := ObjectOptions{}
+ // bucketnames[0].
+ // objectNames[0].
+ // uploadIds [0].
+ // Create bucket before initiating NewMultipartUpload.
+ err := obj.MakeBucket(context.Background(), bucketNames[0], MakeBucketOptions{})
+ if err != nil {
+ // Failed to create newbucket, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ // Initiate Multipart Upload on the above created bucket.
+ res, err := obj.NewMultipartUpload(context.Background(), bucketNames[0], objectNames[0], opts)
+ if err != nil {
+ // Failed to create NewMultipartUpload, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ uploadIDs = append(uploadIDs, res.UploadID)
+
+ // Create multipart parts.
+ // Need parts to be uploaded before MultipartLists can be called and tested.
+ createPartCases := []struct {
+ bucketName string
+ objName string
+ uploadID string
+ PartID int
+ inputReaderData string
+ inputMd5 string
+ inputDataSize int64
+ expectedMd5 string
+ }{
+ // Case 1-4.
+ // Creating sequence of parts for same uploadID.
+ // Used to ensure that the ListMultipartResult produces one output for the four parts uploaded below for the given upload ID.
+ {bucketNames[0], objectNames[0], uploadIDs[0], 1, "abcd", "e2fc714c4727ee9395f324cd2e7f331f", int64(len("abcd")), "e2fc714c4727ee9395f324cd2e7f331f"},
+ {bucketNames[0], objectNames[0], uploadIDs[0], 2, "efgh", "1f7690ebdd9b4caf8fab49ca1757bf27", int64(len("efgh")), "1f7690ebdd9b4caf8fab49ca1757bf27"},
+ {bucketNames[0], objectNames[0], uploadIDs[0], 3, "ijkl", "09a0877d04abf8759f99adec02baf579", int64(len("abcd")), "09a0877d04abf8759f99adec02baf579"},
+ {bucketNames[0], objectNames[0], uploadIDs[0], 4, "mnop", "e132e96a5ddad6da8b07bba6f6131fef", int64(len("abcd")), "e132e96a5ddad6da8b07bba6f6131fef"},
+ }
+ sha256sum := ""
+ // Iterating over creatPartCases to generate multipart chunks.
+ for _, testCase := range createPartCases {
+ _, err := obj.PutObjectPart(context.Background(), testCase.bucketName, testCase.objName, testCase.uploadID, testCase.PartID, mustGetPutObjReader(t, bytes.NewBufferString(testCase.inputReaderData), testCase.inputDataSize, testCase.inputMd5, sha256sum), opts)
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ }
+
+ partInfos := []ListPartsInfo{
+ // partinfos - 0.
+ {
+ Bucket: bucketNames[0],
+ Object: objectNames[0],
+ MaxParts: 10,
+ UploadID: uploadIDs[0],
+ Parts: []PartInfo{
+ {
+ PartNumber: 1,
+ Size: 4,
+ ETag: "e2fc714c4727ee9395f324cd2e7f331f",
+ },
+ {
+ PartNumber: 2,
+ Size: 4,
+ ETag: "1f7690ebdd9b4caf8fab49ca1757bf27",
+ },
+ {
+ PartNumber: 3,
+ Size: 4,
+ ETag: "09a0877d04abf8759f99adec02baf579",
+ },
+ {
+ PartNumber: 4,
+ Size: 4,
+ ETag: "e132e96a5ddad6da8b07bba6f6131fef",
+ },
+ },
+ },
+ // partinfos - 1.
+ {
+ Bucket: bucketNames[0],
+ Object: objectNames[0],
+ MaxParts: 3,
+ NextPartNumberMarker: 3,
+ IsTruncated: true,
+ UploadID: uploadIDs[0],
+ Parts: []PartInfo{
+ {
+ PartNumber: 1,
+ Size: 4,
+ ETag: "e2fc714c4727ee9395f324cd2e7f331f",
+ },
+ {
+ PartNumber: 2,
+ Size: 4,
+ ETag: "1f7690ebdd9b4caf8fab49ca1757bf27",
+ },
+ {
+ PartNumber: 3,
+ Size: 4,
+ ETag: "09a0877d04abf8759f99adec02baf579",
+ },
+ },
+ },
+ // partinfos - 2.
+ {
+ Bucket: bucketNames[0],
+ Object: objectNames[0],
+ MaxParts: 2,
+ IsTruncated: false,
+ UploadID: uploadIDs[0],
+ PartNumberMarker: 3,
+ Parts: []PartInfo{
+ {
+ PartNumber: 4,
+ Size: 4,
+ ETag: "e132e96a5ddad6da8b07bba6f6131fef",
+ },
+ },
+ },
+ }
+
+ // Collection of non-exhaustive ListObjectParts test cases, valid errors
+ // and success responses.
+ testCases := []struct {
+ bucket string
+ object string
+ uploadID string
+ partNumberMarker int
+ maxParts int
+ // Expected output of ListPartsInfo.
+ expectedResult ListPartsInfo
+ expectedErr error
+ // Flag indicating whether the test is expected to pass or not.
+ shouldPass bool
+ }{
+ // Test cases with invalid bucket names (Test number 1-4).
+ {".test", "", "", 0, 0, ListPartsInfo{}, BucketNameInvalid{Bucket: ".test"}, false},
+ {"Test", "", "", 0, 0, ListPartsInfo{}, BucketNameInvalid{Bucket: "Test"}, false},
+ {"---", "", "", 0, 0, ListPartsInfo{}, BucketNameInvalid{Bucket: "---"}, false},
+ {"ad", "", "", 0, 0, ListPartsInfo{}, BucketNameInvalid{Bucket: "ad"}, false},
+ // Test cases for listing uploadID with single part.
+ // Valid bucket names, but they do not exist (Test number 5-7).
+ {"volatile-bucket-1", "test1", "", 0, 0, ListPartsInfo{}, BucketNotFound{Bucket: "volatile-bucket-1"}, false},
+ {"volatile-bucket-2", "test1", "", 0, 0, ListPartsInfo{}, BucketNotFound{Bucket: "volatile-bucket-2"}, false},
+ {"volatile-bucket-3", "test1", "", 0, 0, ListPartsInfo{}, BucketNotFound{Bucket: "volatile-bucket-3"}, false},
+ // Test case for Asserting for invalid objectName (Test number 8).
+ {bucketNames[0], "", "", 0, 0, ListPartsInfo{}, ObjectNameInvalid{Bucket: bucketNames[0]}, false},
+ // Asserting for Invalid UploadID (Test number 9).
+ {bucketNames[0], objectNames[0], "abc", 0, 0, ListPartsInfo{}, InvalidUploadID{UploadID: "abc"}, false},
+ // Test case for uploadID with multiple parts (Test number 12).
+ {bucketNames[0], objectNames[0], uploadIDs[0], 0, 10, partInfos[0], nil, true},
+ // Test case with maxParts set to less than number of parts (Test number 13).
+ {bucketNames[0], objectNames[0], uploadIDs[0], 0, 3, partInfos[1], nil, true},
+ // Test case with partNumberMarker set (Test number 14)-.
+ {bucketNames[0], objectNames[0], uploadIDs[0], 3, 2, partInfos[2], nil, true},
+ }
+
+ for i, testCase := range testCases {
+ actualResult, actualErr := obj.ListObjectParts(context.Background(), testCase.bucket, testCase.object, testCase.uploadID, testCase.partNumberMarker, testCase.maxParts, opts)
+ if actualErr != nil && testCase.shouldPass {
+ t.Errorf("Test %d: %s: Expected to pass, but failed with: %s", i+1, instanceType, actualErr.Error())
+ }
+ if actualErr == nil && !testCase.shouldPass {
+ t.Errorf("Test %d: %s: Expected to fail with \"%s\", but passed instead", i+1, instanceType, testCase.expectedErr.Error())
+ }
+ // Failed as expected, but does it fail for the expected reason.
+ if actualErr != nil && !testCase.shouldPass {
+ if !strings.Contains(actualErr.Error(), testCase.expectedErr.Error()) {
+ t.Errorf("Test %d: %s: Expected to fail with error \"%s\", but instead failed with error \"%s\" instead", i+1, instanceType, testCase.expectedErr.Error(), actualErr.Error())
+ }
+ }
+ // Passes as expected, but asserting the results.
+ if actualErr == nil && testCase.shouldPass {
+ expectedResult := testCase.expectedResult
+ // Asserting the MaxParts.
+ if actualResult.MaxParts != expectedResult.MaxParts {
+ t.Errorf("Test %d: %s: Expected the MaxParts to be %d, but instead found it to be %d", i+1, instanceType, expectedResult.MaxParts, actualResult.MaxParts)
+ }
+ // Asserting Object Name.
+ if actualResult.Object != expectedResult.Object {
+ t.Errorf("Test %d: %s: Expected Object name to be \"%s\", but instead found it to be \"%s\"", i+1, instanceType, expectedResult.Object, actualResult.Object)
+ }
+ // Asserting UploadID.
+ if actualResult.UploadID != expectedResult.UploadID {
+ t.Errorf("Test %d: %s: Expected UploadID to be \"%s\", but instead found it to be \"%s\"", i+1, instanceType, expectedResult.UploadID, actualResult.UploadID)
+ }
+ // Asserting PartNumberMarker.
+ if actualResult.PartNumberMarker != expectedResult.PartNumberMarker {
+ t.Errorf("Test %d: %s: Expected PartNumberMarker to be \"%d\", but instead found it to be \"%d\"", i+1, instanceType, expectedResult.PartNumberMarker, actualResult.PartNumberMarker)
+ }
+ // Asserting the BucketName.
+ if actualResult.Bucket != expectedResult.Bucket {
+ t.Errorf("Test %d: %s: Expected Bucket to be \"%s\", but instead found it to be \"%s\"", i+1, instanceType, expectedResult.Bucket, actualResult.Bucket)
+ }
+
+ // Asserting IsTruncated.
+ if actualResult.IsTruncated != testCase.expectedResult.IsTruncated {
+ t.Errorf("Test %d: %s: Expected IsTruncated to be \"%v\", but found it to \"%v\"", i+1, instanceType, expectedResult.IsTruncated, actualResult.IsTruncated)
+ continue
+ }
+ // Asserting NextPartNumberMarker.
+ if actualResult.NextPartNumberMarker != expectedResult.NextPartNumberMarker {
+ t.Errorf("Test %d: %s: Expected NextPartNumberMarker to be \"%d\", but instead found it to be \"%d\"", i+1, instanceType, expectedResult.NextPartNumberMarker, actualResult.NextPartNumberMarker)
+ continue
+ }
+ // Asserting the number of Parts.
+ if len(expectedResult.Parts) != len(actualResult.Parts) {
+ t.Errorf("Test %d: %s: Expected the result to contain info of %d Parts, but found %d instead", i+1, instanceType, len(expectedResult.Parts), len(actualResult.Parts))
+ continue
+ }
+ // Iterating over the partInfos and asserting the fields.
+ for j, actualMetaData := range actualResult.Parts {
+ // Asserting the PartNumber in the PartInfo.
+ if actualMetaData.PartNumber != expectedResult.Parts[j].PartNumber {
+ t.Errorf("Test %d: %s: Part %d: Expected PartNumber to be \"%d\", but instead found \"%d\"", i+1, instanceType, j+1, expectedResult.Parts[j].PartNumber, actualMetaData.PartNumber)
+ }
+ // Asserting the Size in the PartInfo.
+ if actualMetaData.Size != expectedResult.Parts[j].Size {
+ t.Errorf("Test %d: %s: Part %d: Expected Part Size to be \"%d\", but instead found \"%d\"", i+1, instanceType, j+1, expectedResult.Parts[j].Size, actualMetaData.Size)
+ }
+ // Asserting the ETag in the PartInfo.
+ if actualMetaData.ETag != expectedResult.Parts[j].ETag {
+ t.Errorf("Test %d: %s: Part %d: Expected Etag to be \"%s\", but instead found \"%s\"", i+1, instanceType, j+1, expectedResult.Parts[j].ETag, actualMetaData.ETag)
+ }
+ }
+ }
+ }
+}
+
+// Test for validating complete Multipart upload.
+func TestObjectCompleteMultipartUpload(t *testing.T) {
+ ExecExtendedObjectLayerTest(t, testObjectCompleteMultipartUpload)
+}
+
+// Tests validate CompleteMultipart functionality.
+func testObjectCompleteMultipartUpload(obj ObjectLayer, instanceType string, t TestErrHandler) {
+ var err error
+ bucketNames := []string{"minio-bucket", "minio-2-bucket"}
+ objectNames := []string{"minio-object-1.txt"}
+ uploadIDs := []string{}
+
+ // bucketnames[0].
+ // objectNames[0].
+ // uploadIds [0].
+ // Create bucket before initiating NewMultipartUpload.
+ err = obj.MakeBucket(context.Background(), bucketNames[0], MakeBucketOptions{})
+ if err != nil {
+ // Failed to create newbucket, abort.
+ t.Fatalf("%s : %s", instanceType, err)
+ }
+ // Initiate Multipart Upload on the above created bucket.
+ res, err := obj.NewMultipartUpload(context.Background(), bucketNames[0], objectNames[0], ObjectOptions{UserDefined: map[string]string{"X-Amz-Meta-Id": "id"}})
+ if err != nil {
+ // Failed to create NewMultipartUpload, abort.
+ t.Fatalf("%s : %s", instanceType, err)
+ }
+
+ uploadIDs = append(uploadIDs, res.UploadID)
+ // Parts with size greater than 5 MiB.
+ // Generating a 6MiB byte array.
+ validPart := bytes.Repeat([]byte("abcdef"), 1*humanize.MiByte)
+ validPartMD5 := getMD5Hash(validPart)
+ // Create multipart parts.
+ // Need parts to be uploaded before CompleteMultiPartUpload can be called tested.
+ parts := []struct {
+ bucketName string
+ objName string
+ uploadID string
+ PartID int
+ inputReaderData string
+ inputMd5 string
+ inputDataSize int64
+ }{
+ // Case 1-4.
+ // Creating sequence of parts for same uploadID.
+ {bucketNames[0], objectNames[0], uploadIDs[0], 1, "abcd", "e2fc714c4727ee9395f324cd2e7f331f", int64(len("abcd"))},
+ {bucketNames[0], objectNames[0], uploadIDs[0], 2, "efgh", "1f7690ebdd9b4caf8fab49ca1757bf27", int64(len("efgh"))},
+ {bucketNames[0], objectNames[0], uploadIDs[0], 3, "ijkl", "09a0877d04abf8759f99adec02baf579", int64(len("abcd"))},
+ {bucketNames[0], objectNames[0], uploadIDs[0], 4, "mnop", "e132e96a5ddad6da8b07bba6f6131fef", int64(len("abcd"))},
+ // Part with size larger than 5Mb.
+ {bucketNames[0], objectNames[0], uploadIDs[0], 5, string(validPart), validPartMD5, int64(len(validPart))},
+ {bucketNames[0], objectNames[0], uploadIDs[0], 6, string(validPart), validPartMD5, int64(len(validPart))},
+ {bucketNames[0], objectNames[0], uploadIDs[0], 7, string(validPart), validPartMD5, int64(len(validPart))},
+ }
+ sha256sum := ""
+ var opts ObjectOptions
+ // Iterating over creatPartCases to generate multipart chunks.
+ for _, part := range parts {
+ _, err = obj.PutObjectPart(context.Background(), part.bucketName, part.objName, part.uploadID, part.PartID, mustGetPutObjReader(t, bytes.NewBufferString(part.inputReaderData), part.inputDataSize, part.inputMd5, sha256sum), opts)
+ if err != nil {
+ t.Fatalf("%s : %s", instanceType, err)
+ }
+ }
+ // Parts to be sent as input for CompleteMultipartUpload.
+ inputParts := []struct {
+ parts []CompletePart
+ }{
+ // inputParts - 0.
+ // Case for replicating ETag mismatch.
+ {
+ []CompletePart{
+ {ETag: "abcd", PartNumber: 1},
+ },
+ },
+ // inputParts - 1.
+ // should error out with part too small.
+ {
+ []CompletePart{
+ {ETag: "e2fc714c4727ee9395f324cd2e7f331f", PartNumber: 1},
+ {ETag: "1f7690ebdd9b4caf8fab49ca1757bf27", PartNumber: 2},
+ },
+ },
+ // inputParts - 2.
+ // Case with invalid Part number.
+ {
+ []CompletePart{
+ {ETag: "e2fc714c4727ee9395f324cd2e7f331f", PartNumber: 10},
+ },
+ },
+ // inputParts - 3.
+ // Case with valid part.
+ // Part size greater than 5MB.
+ {
+ []CompletePart{
+ {ETag: fmt.Sprintf("\"\"\"\"\"%s\"\"\"", validPartMD5), PartNumber: 5},
+ },
+ },
+ // inputParts - 4.
+ // Used to verify that the other remaining parts are deleted after
+ // a successful call to CompleteMultipartUpload.
+ {
+ []CompletePart{
+ {ETag: validPartMD5, PartNumber: 6},
+ },
+ },
+ }
+ s3MD5 := getCompleteMultipartMD5(inputParts[3].parts)
+
+ // Test cases with sample input values for CompleteMultipartUpload.
+ testCases := []struct {
+ bucket string
+ object string
+ uploadID string
+ parts []CompletePart
+ // Expected output of CompleteMultipartUpload.
+ expectedS3MD5 string
+ expectedErr error
+ // Flag indicating whether the test is expected to pass or not.
+ shouldPass bool
+ }{
+ // Test cases with invalid bucket names (Test number 1-4).
+ {".test", "", "", []CompletePart{}, "", BucketNameInvalid{Bucket: ".test"}, false},
+ {"Test", "", "", []CompletePart{}, "", BucketNameInvalid{Bucket: "Test"}, false},
+ {"---", "", "", []CompletePart{}, "", BucketNameInvalid{Bucket: "---"}, false},
+ {"ad", "", "", []CompletePart{}, "", BucketNameInvalid{Bucket: "ad"}, false},
+ // Test cases for listing uploadID with single part.
+ // Valid bucket names, but they do not exist (Test number 5-7).
+ {"volatile-bucket-1", "test1", "", []CompletePart{}, "", BucketNotFound{Bucket: "volatile-bucket-1"}, false},
+ {"volatile-bucket-2", "test1", "", []CompletePart{}, "", BucketNotFound{Bucket: "volatile-bucket-2"}, false},
+ {"volatile-bucket-3", "test1", "", []CompletePart{}, "", BucketNotFound{Bucket: "volatile-bucket-3"}, false},
+ // Test case for Asserting for invalid objectName (Test number 8).
+ {bucketNames[0], "", "", []CompletePart{}, "", ObjectNameInvalid{Bucket: bucketNames[0]}, false},
+ // Asserting for Invalid UploadID (Test number 9).
+ {bucketNames[0], objectNames[0], "abc", []CompletePart{}, "", InvalidUploadID{UploadID: "abc"}, false},
+ // Test case with invalid Part Etag (Test number 10-11).
+ {bucketNames[0], objectNames[0], uploadIDs[0], []CompletePart{{ETag: "abc"}}, "", InvalidPart{}, false},
+ {bucketNames[0], objectNames[0], uploadIDs[0], []CompletePart{{ETag: "abcz"}}, "", InvalidPart{}, false},
+ // Part number 0 doesn't exist, expecting InvalidPart error (Test number 12).
+ {bucketNames[0], objectNames[0], uploadIDs[0], []CompletePart{{ETag: "abcd", PartNumber: 0}}, "", InvalidPart{}, false},
+ // // Upload and PartNumber exists, But a deliberate ETag mismatch is introduced (Test number 13).
+ {bucketNames[0], objectNames[0], uploadIDs[0], inputParts[0].parts, "", InvalidPart{}, false},
+ // Test case with non existent object name (Test number 14).
+ {bucketNames[0], "my-object", uploadIDs[0], []CompletePart{{ETag: "abcd", PartNumber: 1}}, "", InvalidUploadID{UploadID: uploadIDs[0]}, false},
+ // Testing for Part being too small (Test number 15).
+ {bucketNames[0], objectNames[0], uploadIDs[0], inputParts[1].parts, "", PartTooSmall{PartNumber: 1}, false},
+ // TestCase with invalid Part Number (Test number 16).
+ // Should error with Invalid Part .
+ {bucketNames[0], objectNames[0], uploadIDs[0], inputParts[2].parts, "", InvalidPart{}, false},
+ // Test case with unsorted parts (Test number 17).
+ {bucketNames[0], objectNames[0], uploadIDs[0], inputParts[3].parts, s3MD5, nil, true},
+ // The other parts will be flushed after a successful CompletePart (Test number 18).
+ // the case above successfully completes CompleteMultipartUpload, the remaining Parts will be flushed.
+ // Expecting to fail with Invalid UploadID.
+ {bucketNames[0], objectNames[0], uploadIDs[0], inputParts[4].parts, "", InvalidUploadID{UploadID: uploadIDs[0]}, false},
+ }
+
+ for _, testCase := range testCases {
+ t.(*testing.T).Run("", func(t *testing.T) {
+ opts = ObjectOptions{}
+ actualResult, actualErr := obj.CompleteMultipartUpload(t.Context(), testCase.bucket, testCase.object, testCase.uploadID, testCase.parts, ObjectOptions{})
+ if actualErr != nil && testCase.shouldPass {
+ t.Errorf("%s: Expected to pass, but failed with: %s", instanceType, actualErr)
+ }
+ if actualErr == nil && !testCase.shouldPass {
+ t.Errorf("%s: Expected to fail with \"%s\", but passed instead", instanceType, testCase.expectedErr)
+ }
+ // Failed as expected, but does it fail for the expected reason.
+ if actualErr != nil && !testCase.shouldPass {
+ if reflect.TypeOf(actualErr) != reflect.TypeOf(testCase.expectedErr) {
+ t.Errorf("%s: Expected to fail with error \"%s\", but instead failed with error \"%s\"", instanceType, testCase.expectedErr, actualErr)
+ }
+ }
+ // Passes as expected, but asserting the results.
+ if actualErr == nil && testCase.shouldPass {
+ // Asserting IsTruncated.
+ if actualResult.ETag != testCase.expectedS3MD5 {
+ t.Errorf("%s: Expected the result to be \"%v\", but found it to \"%v\"", instanceType, testCase.expectedS3MD5, actualResult)
+ }
+ }
+ })
+ }
+}
+
+// Benchmarks for ObjectLayer.PutObjectPart().
+// The intent is to benchmark PutObjectPart for various sizes ranging from few bytes to 100MB.
+// Also each of these Benchmarks are run both Erasure and FS backends.
+
+// BenchmarkPutObjectPart5MbFS - Benchmark FS.PutObjectPart() for object size of 5MB.
+func BenchmarkPutObjectPart5MbFS(b *testing.B) {
+ benchmarkPutObjectPart(b, "FS", 5*humanize.MiByte)
+}
+
+// BenchmarkPutObjectPart5MbErasure - Benchmark Erasure.PutObjectPart() for object size of 5MB.
+func BenchmarkPutObjectPart5MbErasure(b *testing.B) {
+ benchmarkPutObjectPart(b, "Erasure", 5*humanize.MiByte)
+}
+
+// BenchmarkPutObjectPart10MbFS - Benchmark FS.PutObjectPart() for object size of 10MB.
+func BenchmarkPutObjectPart10MbFS(b *testing.B) {
+ benchmarkPutObjectPart(b, "FS", 10*humanize.MiByte)
+}
+
+// BenchmarkPutObjectPart10MbErasure - Benchmark Erasure.PutObjectPart() for object size of 10MB.
+func BenchmarkPutObjectPart10MbErasure(b *testing.B) {
+ benchmarkPutObjectPart(b, "Erasure", 10*humanize.MiByte)
+}
+
+// BenchmarkPutObjectPart25MbFS - Benchmark FS.PutObjectPart() for object size of 25MB.
+func BenchmarkPutObjectPart25MbFS(b *testing.B) {
+ benchmarkPutObjectPart(b, "FS", 25*humanize.MiByte)
+}
+
+// BenchmarkPutObjectPart25MbErasure - Benchmark Erasure.PutObjectPart() for object size of 25MB.
+func BenchmarkPutObjectPart25MbErasure(b *testing.B) {
+ benchmarkPutObjectPart(b, "Erasure", 25*humanize.MiByte)
+}
+
+// BenchmarkPutObjectPart50MbFS - Benchmark FS.PutObjectPart() for object size of 50MB.
+func BenchmarkPutObjectPart50MbFS(b *testing.B) {
+ benchmarkPutObjectPart(b, "FS", 50*humanize.MiByte)
+}
+
+// BenchmarkPutObjectPart50MbErasure - Benchmark Erasure.PutObjectPart() for object size of 50MB.
+func BenchmarkPutObjectPart50MbErasure(b *testing.B) {
+ benchmarkPutObjectPart(b, "Erasure", 50*humanize.MiByte)
+}
diff --git a/cmd/object-api-options.go b/cmd/object-api-options.go
new file mode 100644
index 00000000..6482f200
--- /dev/null
+++ b/cmd/object-api-options.go
@@ -0,0 +1,496 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/minio/minio-go/v7/pkg/encrypt"
+ "github.com/minio/minio/internal/crypto"
+ "github.com/minio/minio/internal/hash"
+ xhttp "github.com/minio/minio/internal/http"
+)
+
+func getDefaultOpts(header http.Header, copySource bool, metadata map[string]string) (opts ObjectOptions, err error) {
+ var clientKey [32]byte
+ var sse encrypt.ServerSide
+
+ opts = ObjectOptions{UserDefined: metadata}
+ if v, ok := header[xhttp.MinIOSourceProxyRequest]; ok {
+ opts.ProxyHeaderSet = true
+ opts.ProxyRequest = strings.Join(v, "") == "true"
+ }
+ if _, ok := header[xhttp.MinIOSourceReplicationRequest]; ok {
+ opts.ReplicationRequest = true
+ }
+ opts.Speedtest = header.Get(globalObjectPerfUserMetadata) != ""
+
+ if copySource {
+ if crypto.SSECopy.IsRequested(header) {
+ clientKey, err = crypto.SSECopy.ParseHTTP(header)
+ if err != nil {
+ return opts, err
+ }
+ if sse, err = encrypt.NewSSEC(clientKey[:]); err != nil {
+ return opts, err
+ }
+ opts.ServerSideEncryption = encrypt.SSECopy(sse)
+ return opts, err
+ }
+ return opts, err
+ }
+
+ if crypto.SSEC.IsRequested(header) {
+ clientKey, err = crypto.SSEC.ParseHTTP(header)
+ if err != nil {
+ return opts, err
+ }
+ if sse, err = encrypt.NewSSEC(clientKey[:]); err != nil {
+ return opts, err
+ }
+ opts.ServerSideEncryption = sse
+ return opts, err
+ }
+ if crypto.S3.IsRequested(header) || (metadata != nil && crypto.S3.IsEncrypted(metadata)) {
+ opts.ServerSideEncryption = encrypt.NewSSE()
+ }
+
+ return opts, err
+}
+
+// get ObjectOptions for GET calls from encryption headers
+func getOpts(ctx context.Context, r *http.Request, bucket, object string) (ObjectOptions, error) {
+ var opts ObjectOptions
+
+ var partNumber int
+ var err error
+ if pn := r.Form.Get(xhttp.PartNumber); pn != "" {
+ partNumber, err = strconv.Atoi(pn)
+ if err != nil {
+ return opts, err
+ }
+ if isMaxPartID(partNumber) {
+ return opts, errInvalidMaxParts
+ }
+ if partNumber <= 0 {
+ return opts, errInvalidArgument
+ }
+ }
+
+ vid := strings.TrimSpace(r.Form.Get(xhttp.VersionID))
+ if vid != "" && vid != nullVersionID {
+ if _, err := uuid.Parse(vid); err != nil {
+ return opts, InvalidVersionID{
+ Bucket: bucket,
+ Object: object,
+ VersionID: vid,
+ }
+ }
+ }
+
+ // default case of passing encryption headers to backend
+ opts, err = getDefaultOpts(r.Header, false, nil)
+ if err != nil {
+ return opts, err
+ }
+ opts.PartNumber = partNumber
+ opts.VersionID = vid
+
+ delMarker, err := parseBoolHeader(bucket, object, r.Header, xhttp.MinIOSourceDeleteMarker)
+ if err != nil {
+ return opts, err
+ }
+ opts.DeleteMarker = delMarker
+
+ replReadyCheck, err := parseBoolHeader(bucket, object, r.Header, xhttp.MinIOCheckDMReplicationReady)
+ if err != nil {
+ return opts, err
+ }
+ opts.CheckDMReplicationReady = replReadyCheck
+
+ opts.Tagging = r.Header.Get(xhttp.AmzTagDirective) == accessDirective
+ opts.Versioned = globalBucketVersioningSys.PrefixEnabled(bucket, object)
+ opts.VersionSuspended = globalBucketVersioningSys.PrefixSuspended(bucket, object)
+ return opts, nil
+}
+
+func getAndValidateAttributesOpts(ctx context.Context, w http.ResponseWriter, r *http.Request, bucket, object string) (opts ObjectOptions, valid bool) {
+ var argumentName string
+ var argumentValue string
+ var apiErr APIError
+ var err error
+ valid = true
+
+ defer func() {
+ if valid {
+ return
+ }
+
+ errResp := objectAttributesErrorResponse{
+ ArgumentName: &argumentName,
+ ArgumentValue: &argumentValue,
+ APIErrorResponse: getAPIErrorResponse(
+ ctx,
+ apiErr,
+ r.URL.Path,
+ w.Header().Get(xhttp.AmzRequestID),
+ w.Header().Get(xhttp.AmzRequestHostID),
+ ),
+ }
+
+ writeResponse(w, apiErr.HTTPStatusCode, encodeResponse(errResp), mimeXML)
+ }()
+
+ opts, err = getOpts(ctx, r, bucket, object)
+ if err != nil {
+ switch vErr := err.(type) {
+ case InvalidVersionID:
+ apiErr = toAPIError(ctx, vErr)
+ argumentName = strings.ToLower("versionId")
+ argumentValue = vErr.VersionID
+ default:
+ apiErr = toAPIError(ctx, vErr)
+ }
+ valid = false
+ return opts, valid
+ }
+
+ opts.MaxParts, err = parseIntHeader(bucket, object, r.Header, xhttp.AmzMaxParts)
+ if err != nil {
+ apiErr = toAPIError(ctx, err)
+ argumentName = strings.ToLower(xhttp.AmzMaxParts)
+ valid = false
+ return opts, valid
+ }
+
+ if opts.MaxParts == 0 {
+ opts.MaxParts = maxPartsList
+ }
+
+ opts.PartNumberMarker, err = parseIntHeader(bucket, object, r.Header, xhttp.AmzPartNumberMarker)
+ if err != nil {
+ apiErr = toAPIError(ctx, err)
+ argumentName = strings.ToLower(xhttp.AmzPartNumberMarker)
+ valid = false
+ return opts, valid
+ }
+
+ opts.ObjectAttributes = parseObjectAttributes(r.Header)
+ if len(opts.ObjectAttributes) < 1 {
+ apiErr = errorCodes.ToAPIErr(ErrInvalidAttributeName)
+ argumentName = strings.ToLower(xhttp.AmzObjectAttributes)
+ valid = false
+ return opts, valid
+ }
+
+ for tag := range opts.ObjectAttributes {
+ switch tag {
+ case xhttp.ETag:
+ case xhttp.Checksum:
+ case xhttp.StorageClass:
+ case xhttp.ObjectSize:
+ case xhttp.ObjectParts:
+ default:
+ apiErr = errorCodes.ToAPIErr(ErrInvalidAttributeName)
+ argumentName = strings.ToLower(xhttp.AmzObjectAttributes)
+ argumentValue = tag
+ valid = false
+ return opts, valid
+ }
+ }
+
+ return opts, valid
+}
+
+func parseObjectAttributes(h http.Header) (attributes map[string]struct{}) {
+ attributes = make(map[string]struct{})
+ for _, headerVal := range h.Values(xhttp.AmzObjectAttributes) {
+ for v := range strings.SplitSeq(strings.TrimSpace(headerVal), ",") {
+ if v != "" {
+ attributes[v] = struct{}{}
+ }
+ }
+ }
+
+ return attributes
+}
+
+func parseIntHeader(bucket, object string, h http.Header, headerName string) (value int, err error) {
+ stringInt := strings.TrimSpace(h.Get(headerName))
+ if stringInt == "" {
+ return value, err
+ }
+ value, err = strconv.Atoi(stringInt)
+ if err != nil {
+ return 0, InvalidArgument{
+ Bucket: bucket,
+ Object: object,
+ Err: fmt.Errorf("Unable to parse %s, value should be an integer", headerName),
+ }
+ }
+ return value, err
+}
+
+func parseBoolHeader(bucket, object string, h http.Header, headerName string) (bool, error) {
+ value := strings.TrimSpace(h.Get(headerName))
+ if value != "" {
+ switch value {
+ case "true":
+ return true, nil
+ case "false":
+ default:
+ return false, InvalidArgument{
+ Bucket: bucket,
+ Object: object,
+ Err: fmt.Errorf("Unable to parse %s, value should be either 'true' or 'false'", headerName),
+ }
+ }
+ }
+ return false, nil
+}
+
+func delOpts(ctx context.Context, r *http.Request, bucket, object string) (opts ObjectOptions, err error) {
+ opts, err = getOpts(ctx, r, bucket, object)
+ if err != nil {
+ return opts, err
+ }
+
+ deletePrefix := false
+ if d := r.Header.Get(xhttp.MinIOForceDelete); d != "" {
+ if b, err := strconv.ParseBool(d); err == nil {
+ deletePrefix = b
+ } else {
+ return opts, err
+ }
+ }
+
+ opts.DeletePrefix = deletePrefix
+ opts.Versioned = globalBucketVersioningSys.PrefixEnabled(bucket, object)
+ // Objects matching prefixes should not leave delete markers,
+ // dramatically reduces namespace pollution while keeping the
+ // benefits of replication, make sure to apply version suspension
+ // only at bucket level instead.
+ opts.VersionSuspended = globalBucketVersioningSys.Suspended(bucket)
+ // For directory objects, delete `null` version permanently.
+ if isDirObject(object) && opts.VersionID == "" {
+ opts.VersionID = nullVersionID
+ }
+
+ delMarker, err := parseBoolHeader(bucket, object, r.Header, xhttp.MinIOSourceDeleteMarker)
+ if err != nil {
+ return opts, err
+ }
+ opts.DeleteMarker = delMarker
+
+ mtime := strings.TrimSpace(r.Header.Get(xhttp.MinIOSourceMTime))
+ if mtime != "" {
+ opts.MTime, err = time.Parse(time.RFC3339Nano, mtime)
+ if err != nil {
+ return opts, InvalidArgument{
+ Bucket: bucket,
+ Object: object,
+ Err: fmt.Errorf("Unable to parse %s, failed with %w", xhttp.MinIOSourceMTime, err),
+ }
+ }
+ }
+ return opts, nil
+}
+
+// get ObjectOptions for PUT calls from encryption headers and metadata
+func putOptsFromReq(ctx context.Context, r *http.Request, bucket, object string, metadata map[string]string) (opts ObjectOptions, err error) {
+ return putOpts(ctx, bucket, object, r.Form.Get(xhttp.VersionID), r.Header, metadata)
+}
+
+func putOpts(ctx context.Context, bucket, object, vid string, hdrs http.Header, metadata map[string]string) (opts ObjectOptions, err error) {
+ versioned := globalBucketVersioningSys.PrefixEnabled(bucket, object)
+ versionSuspended := globalBucketVersioningSys.PrefixSuspended(bucket, object)
+
+ vid = strings.TrimSpace(vid)
+ if vid != "" && vid != nullVersionID {
+ _, err := uuid.Parse(vid)
+ if err != nil {
+ return opts, InvalidVersionID{
+ Bucket: bucket,
+ Object: object,
+ VersionID: vid,
+ }
+ }
+ if !versioned {
+ return opts, InvalidArgument{
+ Bucket: bucket,
+ Object: object,
+ Err: fmt.Errorf("VersionID specified %s, but versioning not enabled on bucket=%s", opts.VersionID, bucket),
+ }
+ }
+ }
+ opts, err = putOptsFromHeaders(ctx, hdrs, metadata)
+ if err != nil {
+ return opts, InvalidArgument{
+ Bucket: bucket,
+ Object: object,
+ Err: err,
+ }
+ }
+
+ opts.VersionID = vid
+ opts.Versioned = versioned
+ opts.VersionSuspended = versionSuspended
+
+ // For directory objects skip creating new versions.
+ if isDirObject(object) && vid == "" {
+ opts.VersionID = nullVersionID
+ }
+
+ return opts, nil
+}
+
+func putOptsFromHeaders(ctx context.Context, hdr http.Header, metadata map[string]string) (opts ObjectOptions, err error) {
+ mtimeStr := strings.TrimSpace(hdr.Get(xhttp.MinIOSourceMTime))
+ var mtime time.Time
+ if mtimeStr != "" {
+ mtime, err = time.Parse(time.RFC3339Nano, mtimeStr)
+ if err != nil {
+ return opts, fmt.Errorf("Unable to parse %s, failed with %w", xhttp.MinIOSourceMTime, err)
+ }
+ }
+ retaintimeStr := strings.TrimSpace(hdr.Get(xhttp.MinIOSourceObjectRetentionTimestamp))
+ var retaintimestmp time.Time
+ if retaintimeStr != "" {
+ retaintimestmp, err = time.Parse(time.RFC3339, retaintimeStr)
+ if err != nil {
+ return opts, fmt.Errorf("Unable to parse %s, failed with %w", xhttp.MinIOSourceObjectRetentionTimestamp, err)
+ }
+ }
+
+ lholdtimeStr := strings.TrimSpace(hdr.Get(xhttp.MinIOSourceObjectLegalHoldTimestamp))
+ var lholdtimestmp time.Time
+ if lholdtimeStr != "" {
+ lholdtimestmp, err = time.Parse(time.RFC3339, lholdtimeStr)
+ if err != nil {
+ return opts, fmt.Errorf("Unable to parse %s, failed with %w", xhttp.MinIOSourceObjectLegalHoldTimestamp, err)
+ }
+ }
+ tagtimeStr := strings.TrimSpace(hdr.Get(xhttp.MinIOSourceTaggingTimestamp))
+ var taggingtimestmp time.Time
+ if tagtimeStr != "" {
+ taggingtimestmp, err = time.Parse(time.RFC3339, tagtimeStr)
+ if err != nil {
+ return opts, fmt.Errorf("Unable to parse %s, failed with %w", xhttp.MinIOSourceTaggingTimestamp, err)
+ }
+ }
+
+ if metadata == nil {
+ metadata = make(map[string]string)
+ }
+
+ etag := strings.TrimSpace(hdr.Get(xhttp.MinIOSourceETag))
+ if crypto.S3KMS.IsRequested(hdr) {
+ keyID, context, err := crypto.S3KMS.ParseHTTP(hdr)
+ if err != nil {
+ return ObjectOptions{}, err
+ }
+ sseKms, err := encrypt.NewSSEKMS(keyID, context)
+ if err != nil {
+ return ObjectOptions{}, err
+ }
+ op := ObjectOptions{
+ ServerSideEncryption: sseKms,
+ UserDefined: metadata,
+ MTime: mtime,
+ PreserveETag: etag,
+ }
+ return op, nil
+ }
+ // default case of passing encryption headers and UserDefined metadata to backend
+ opts, err = getDefaultOpts(hdr, false, metadata)
+ if err != nil {
+ return opts, err
+ }
+
+ opts.MTime = mtime
+ opts.ReplicationSourceLegalholdTimestamp = lholdtimestmp
+ opts.ReplicationSourceRetentionTimestamp = retaintimestmp
+ opts.ReplicationSourceTaggingTimestamp = taggingtimestmp
+ opts.PreserveETag = etag
+
+ return opts, nil
+}
+
+// get ObjectOptions for Copy calls with encryption headers provided on the target side and source side metadata
+func copyDstOpts(ctx context.Context, r *http.Request, bucket, object string, metadata map[string]string) (opts ObjectOptions, err error) {
+ return putOptsFromReq(ctx, r, bucket, object, metadata)
+}
+
+// get ObjectOptions for Copy calls with encryption headers provided on the source side
+func copySrcOpts(ctx context.Context, r *http.Request, bucket, object string) (ObjectOptions, error) {
+ var opts ObjectOptions
+
+ // default case of passing encryption headers to backend
+ opts, err := getDefaultOpts(r.Header, false, nil)
+ if err != nil {
+ return opts, err
+ }
+ return opts, nil
+}
+
+// get ObjectOptions for CompleteMultipart calls
+func completeMultipartOpts(ctx context.Context, r *http.Request, bucket, object string) (opts ObjectOptions, err error) {
+ mtimeStr := strings.TrimSpace(r.Header.Get(xhttp.MinIOSourceMTime))
+ var mtime time.Time
+ if mtimeStr != "" {
+ mtime, err = time.Parse(time.RFC3339Nano, mtimeStr)
+ if err != nil {
+ return opts, InvalidArgument{
+ Bucket: bucket,
+ Object: object,
+ Err: fmt.Errorf("Unable to parse %s, failed with %w", xhttp.MinIOSourceMTime, err),
+ }
+ }
+ }
+
+ opts.WantChecksum, err = hash.GetContentChecksum(r.Header)
+ if err != nil {
+ return opts, err
+ }
+ opts.MTime = mtime
+ opts.UserDefined = make(map[string]string)
+ // Transfer SSEC key in opts.EncryptFn
+ if crypto.SSEC.IsRequested(r.Header) {
+ key, err := ParseSSECustomerRequest(r)
+ if err == nil {
+ // Set EncryptFn to return SSEC key
+ opts.EncryptFn = func(baseKey string, data []byte) []byte {
+ return key
+ }
+ }
+ }
+ if _, ok := r.Header[xhttp.MinIOSourceReplicationRequest]; ok {
+ opts.ReplicationRequest = true
+ opts.UserDefined[ReservedMetadataPrefix+"Actual-Object-Size"] = r.Header.Get(xhttp.MinIOReplicationActualObjectSize)
+ }
+ if r.Header.Get(ReplicationSsecChecksumHeader) != "" {
+ opts.UserDefined[ReplicationSsecChecksumHeader] = r.Header.Get(ReplicationSsecChecksumHeader)
+ }
+ return opts, nil
+}
diff --git a/cmd/object-api-options_test.go b/cmd/object-api-options_test.go
new file mode 100644
index 00000000..661372cd
--- /dev/null
+++ b/cmd/object-api-options_test.go
@@ -0,0 +1,78 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "reflect"
+ "testing"
+
+ xhttp "github.com/minio/minio/internal/http"
+)
+
+// TestGetAndValidateAttributesOpts is currently minimal and covers a subset of getAndValidateAttributesOpts(),
+// it is intended to be expanded when the function is worked on in the future.
+func TestGetAndValidateAttributesOpts(t *testing.T) {
+ globalBucketVersioningSys = &BucketVersioningSys{}
+ bucket := minioMetaBucket
+ ctx := t.Context()
+ testCases := []struct {
+ name string
+ headers http.Header
+ wantObjectAttrs map[string]struct{}
+ }{
+ {
+ name: "empty header",
+ headers: http.Header{},
+ wantObjectAttrs: map[string]struct{}{},
+ },
+ {
+ name: "single header line",
+ headers: http.Header{
+ xhttp.AmzObjectAttributes: []string{"test1,test2"},
+ },
+ wantObjectAttrs: map[string]struct{}{
+ "test1": {}, "test2": {},
+ },
+ },
+ {
+ name: "multiple header lines with some duplicates",
+ headers: http.Header{
+ xhttp.AmzObjectAttributes: []string{"test1,test2", "test3,test4", "test4,test3"},
+ },
+ wantObjectAttrs: map[string]struct{}{
+ "test1": {}, "test2": {}, "test3": {}, "test4": {},
+ },
+ },
+ }
+
+ for _, testCase := range testCases {
+ t.Run(testCase.name, func(t *testing.T) {
+ rec := httptest.NewRecorder()
+ req := httptest.NewRequest("GET", "/test", nil)
+ req.Header = testCase.headers
+
+ opts, _ := getAndValidateAttributesOpts(ctx, rec, req, bucket, "testobject")
+
+ if !reflect.DeepEqual(opts.ObjectAttributes, testCase.wantObjectAttrs) {
+ t.Errorf("want opts %v, got %v", testCase.wantObjectAttrs, opts.ObjectAttributes)
+ }
+ })
+ }
+}
diff --git a/cmd/object-api-putobject_test.go b/cmd/object-api-putobject_test.go
new file mode 100644
index 00000000..0c449320
--- /dev/null
+++ b/cmd/object-api-putobject_test.go
@@ -0,0 +1,614 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "crypto/md5"
+ "encoding/hex"
+ "errors"
+ "os"
+ "path"
+ "testing"
+
+ "github.com/dustin/go-humanize"
+ "github.com/minio/minio/internal/hash"
+ "github.com/minio/minio/internal/ioutil"
+)
+
+func md5Header(data []byte) map[string]string {
+ return map[string]string{"etag": getMD5Hash(data)}
+}
+
+// Wrapper for calling PutObject tests for both Erasure multiple disks and single node setup.
+func TestObjectAPIPutObjectSingle(t *testing.T) {
+ ExecExtendedObjectLayerTest(t, testObjectAPIPutObject)
+}
+
+// Tests validate correctness of PutObject.
+func testObjectAPIPutObject(obj ObjectLayer, instanceType string, t TestErrHandler) {
+ // Generating cases for which the PutObject fails.
+ bucket := "minio-bucket"
+ object := "minio-object"
+
+ // Create bucket.
+ err := obj.MakeBucket(context.Background(), bucket, MakeBucketOptions{})
+ if err != nil {
+ // Failed to create newbucket, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ // Creating a dummy bucket for tests.
+ err = obj.MakeBucket(context.Background(), "unused-bucket", MakeBucketOptions{})
+ if err != nil {
+ // Failed to create newbucket, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ var (
+ nilBytes []byte
+ data = []byte("hello")
+ fiveMBBytes = bytes.Repeat([]byte("a"), 5*humanize.MiByte)
+ )
+ invalidMD5 := getMD5Hash([]byte("meh"))
+ invalidMD5Header := md5Header([]byte("meh"))
+
+ testCases := []struct {
+ bucketName string
+ objName string
+ inputData []byte
+ inputMeta map[string]string
+ inputSHA256 string
+ inputDataSize int64
+ // expected error output.
+ expectedMd5 string
+ expectedError error
+ }{
+ // Cases with invalid bucket name.
+ 0: {bucketName: ".test", objName: "obj", inputData: []byte(""), expectedError: BucketNameInvalid{Bucket: ".test"}},
+ 1: {bucketName: "------", objName: "obj", inputData: []byte(""), expectedError: BucketNameInvalid{Bucket: "------"}},
+ 2: {
+ bucketName: "$this-is-not-valid-too", objName: "obj", inputData: []byte(""),
+ expectedError: BucketNameInvalid{Bucket: "$this-is-not-valid-too"},
+ },
+ 3: {bucketName: "a", objName: "obj", inputData: []byte(""), expectedError: BucketNameInvalid{Bucket: "a"}},
+
+ // Case with invalid object names.
+ 4: {bucketName: bucket, inputData: []byte(""), expectedError: ObjectNameInvalid{Bucket: bucket, Object: ""}},
+
+ // Valid object and bucket names but non-existent bucket.
+ 5: {bucketName: "abc", objName: "def", inputData: []byte(""), expectedError: BucketNotFound{Bucket: "abc"}},
+
+ // Input to replicate Md5 mismatch.
+ 6: {
+ bucketName: bucket, objName: object, inputData: []byte(""),
+ inputMeta: map[string]string{"etag": "d41d8cd98f00b204e9800998ecf8427f"},
+ expectedError: hash.BadDigest{ExpectedMD5: "d41d8cd98f00b204e9800998ecf8427f", CalculatedMD5: "d41d8cd98f00b204e9800998ecf8427e"},
+ },
+
+ // With incorrect sha256.
+ 7: {
+ bucketName: bucket, objName: object, inputData: []byte("abcd"),
+ inputMeta: map[string]string{"etag": "e2fc714c4727ee9395f324cd2e7f331f"},
+ inputSHA256: "88d4266fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031580", inputDataSize: int64(len("abcd")),
+ expectedError: hash.SHA256Mismatch{
+ ExpectedSHA256: "88d4266fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031580",
+ CalculatedSHA256: "88d4266fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031589",
+ },
+ },
+
+ // Input with size more than the size of actual data inside the reader.
+ 8: {
+ bucketName: bucket, objName: object, inputData: []byte("abcd"),
+ inputMeta: map[string]string{"etag": "e2fc714c4727ee9395f324cd2e7f331e"}, inputDataSize: int64(len("abcd") + 1),
+ expectedError: hash.BadDigest{ExpectedMD5: "e2fc714c4727ee9395f324cd2e7f331e", CalculatedMD5: "e2fc714c4727ee9395f324cd2e7f331f"},
+ },
+
+ // Input with size less than the size of actual data inside the reader.
+ 9: {
+ bucketName: bucket, objName: object, inputData: []byte("abcd"),
+ inputMeta: map[string]string{"etag": "900150983cd24fb0d6963f7d28e17f73"}, inputDataSize: int64(len("abcd") - 1),
+ expectedError: ioutil.ErrOverread,
+ },
+
+ // Validating for success cases.
+ 10: {bucketName: bucket, objName: object, inputData: []byte("abcd"), inputMeta: map[string]string{"etag": "e2fc714c4727ee9395f324cd2e7f331f"}, inputDataSize: int64(len("abcd"))},
+ 11: {bucketName: bucket, objName: object, inputData: []byte("efgh"), inputMeta: map[string]string{"etag": "1f7690ebdd9b4caf8fab49ca1757bf27"}, inputDataSize: int64(len("efgh"))},
+ 12: {bucketName: bucket, objName: object, inputData: []byte("ijkl"), inputMeta: map[string]string{"etag": "09a0877d04abf8759f99adec02baf579"}, inputDataSize: int64(len("ijkl"))},
+ 13: {bucketName: bucket, objName: object, inputData: []byte("mnop"), inputMeta: map[string]string{"etag": "e132e96a5ddad6da8b07bba6f6131fef"}, inputDataSize: int64(len("mnop"))},
+
+ // With no metadata
+ 14: {bucketName: bucket, objName: object, inputData: data, inputDataSize: int64(len(data)), expectedMd5: getMD5Hash(data)},
+ 15: {bucketName: bucket, objName: object, inputData: nilBytes, inputDataSize: int64(len(nilBytes)), expectedMd5: getMD5Hash(nilBytes)},
+ 16: {bucketName: bucket, objName: object, inputData: fiveMBBytes, inputDataSize: int64(len(fiveMBBytes)), expectedMd5: getMD5Hash(fiveMBBytes)},
+
+ // With arbitrary metadata
+ 17: {bucketName: bucket, objName: object, inputData: data, inputMeta: map[string]string{"answer": "42"}, inputDataSize: int64(len(data)), expectedMd5: getMD5Hash(data)},
+ 18: {bucketName: bucket, objName: object, inputData: nilBytes, inputMeta: map[string]string{"answer": "42"}, inputDataSize: int64(len(nilBytes)), expectedMd5: getMD5Hash(nilBytes)},
+ 19: {bucketName: bucket, objName: object, inputData: fiveMBBytes, inputMeta: map[string]string{"answer": "42"}, inputDataSize: int64(len(fiveMBBytes)), expectedMd5: getMD5Hash(fiveMBBytes)},
+
+ // With valid md5sum and sha256.
+ 20: {bucketName: bucket, objName: object, inputData: data, inputMeta: md5Header(data), inputSHA256: getSHA256Hash(data), inputDataSize: int64(len(data)), expectedMd5: getMD5Hash(data)},
+ 21: {bucketName: bucket, objName: object, inputData: nilBytes, inputMeta: md5Header(nilBytes), inputSHA256: getSHA256Hash(nilBytes), inputDataSize: int64(len(nilBytes)), expectedMd5: getMD5Hash(nilBytes)},
+ 22: {bucketName: bucket, objName: object, inputData: fiveMBBytes, inputMeta: md5Header(fiveMBBytes), inputSHA256: getSHA256Hash(fiveMBBytes), inputDataSize: int64(len(fiveMBBytes)), expectedMd5: getMD5Hash(fiveMBBytes)},
+
+ // data with invalid md5sum in header
+ 23: {
+ bucketName: bucket, objName: object, inputData: data, inputMeta: invalidMD5Header, inputDataSize: int64(len(data)), expectedMd5: getMD5Hash(data),
+ expectedError: hash.BadDigest{ExpectedMD5: invalidMD5, CalculatedMD5: getMD5Hash(data)},
+ },
+ 24: {
+ bucketName: bucket, objName: object, inputData: nilBytes, inputMeta: invalidMD5Header, inputDataSize: int64(len(nilBytes)), expectedMd5: getMD5Hash(nilBytes),
+ expectedError: hash.BadDigest{ExpectedMD5: invalidMD5, CalculatedMD5: getMD5Hash(nilBytes)},
+ },
+ 25: {
+ bucketName: bucket, objName: object, inputData: fiveMBBytes, inputMeta: invalidMD5Header, inputDataSize: int64(len(fiveMBBytes)), expectedMd5: getMD5Hash(fiveMBBytes),
+ expectedError: hash.BadDigest{ExpectedMD5: invalidMD5, CalculatedMD5: getMD5Hash(fiveMBBytes)},
+ },
+
+ // data with size different from the actual number of bytes available in the reader
+ 26: {bucketName: bucket, objName: object, inputData: data, inputDataSize: int64(len(data) - 1), expectedMd5: getMD5Hash(data[:len(data)-1]), expectedError: ioutil.ErrOverread},
+ 27: {bucketName: bucket, objName: object, inputData: nilBytes, inputDataSize: int64(len(nilBytes) + 1), expectedMd5: getMD5Hash(nilBytes), expectedError: IncompleteBody{Bucket: bucket, Object: object}},
+ 28: {bucketName: bucket, objName: object, inputData: fiveMBBytes, expectedMd5: getMD5Hash(fiveMBBytes), expectedError: ioutil.ErrOverread},
+
+ // valid data with X-Amz-Meta- meta
+ 29: {bucketName: bucket, objName: object, inputData: data, inputMeta: map[string]string{"X-Amz-Meta-AppID": "a42"}, inputDataSize: int64(len(data)), expectedMd5: getMD5Hash(data)},
+
+ // Put an empty object with a trailing slash
+ 30: {bucketName: bucket, objName: "emptydir/", inputData: []byte{}, expectedMd5: getMD5Hash([]byte{})},
+ // Put an object inside the empty directory
+ 31: {bucketName: bucket, objName: "emptydir/" + object, inputData: data, inputDataSize: int64(len(data)), expectedMd5: getMD5Hash(data)},
+ // Put the empty object with a trailing slash again (refer to Test case 30), this needs to succeed
+ 32: {bucketName: bucket, objName: "emptydir/", inputData: []byte{}, expectedMd5: getMD5Hash([]byte{})},
+
+ // With invalid crc32.
+ 33: {
+ bucketName: bucket, objName: object, inputData: []byte("abcd"),
+ inputMeta: map[string]string{"etag": "e2fc714c4727ee9395f324cd2e7f331f", "x-amz-checksum-crc32": "abcd"},
+ inputDataSize: int64(len("abcd")),
+ },
+ }
+ for i, testCase := range testCases {
+ in := mustGetPutObjReader(t, bytes.NewReader(testCase.inputData), testCase.inputDataSize, testCase.inputMeta["etag"], testCase.inputSHA256)
+ objInfo, actualErr := obj.PutObject(context.Background(), testCase.bucketName, testCase.objName, in, ObjectOptions{UserDefined: testCase.inputMeta})
+ if actualErr != nil && testCase.expectedError == nil {
+ t.Errorf("Test %d: %s: Expected to pass, but failed with: error %s.", i, instanceType, actualErr.Error())
+ continue
+ }
+ if actualErr == nil && testCase.expectedError != nil {
+ t.Errorf("Test %d: %s: Expected to fail with error \"%s\", but passed instead.", i, instanceType, testCase.expectedError.Error())
+ continue
+ }
+ // Failed as expected, but does it fail for the expected reason.
+ if actualErr != nil && actualErr != testCase.expectedError {
+ t.Errorf("Test %d: %s: Expected to fail with error \"%v\", but instead failed with error \"%v\" instead.", i, instanceType, testCase.expectedError, actualErr)
+ continue
+ }
+ // Test passes as expected, but the output values are verified for correctness here.
+ if actualErr == nil {
+ // Asserting whether the md5 output is correct.
+ if expectedMD5, ok := testCase.inputMeta["etag"]; ok && expectedMD5 != objInfo.ETag {
+ t.Errorf("Test %d: %s: Calculated Md5 different from the actual one %s.", i, instanceType, objInfo.ETag)
+ continue
+ }
+ }
+ }
+}
+
+// Wrapper for calling PutObject tests for both Erasure multiple disks case
+// when quorum is not available.
+func TestObjectAPIPutObjectDiskNotFound(t *testing.T) {
+ ExecObjectLayerDiskAlteredTest(t, testObjectAPIPutObjectDiskNotFound)
+}
+
+// Tests validate correctness of PutObject.
+func testObjectAPIPutObjectDiskNotFound(obj ObjectLayer, instanceType string, disks []string, t *testing.T) {
+ // Generating cases for which the PutObject fails.
+ bucket := "minio-bucket"
+ object := "minio-object"
+
+ // Create bucket.
+ err := obj.MakeBucket(context.Background(), bucket, MakeBucketOptions{})
+ if err != nil {
+ // Failed to create newbucket, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ // Creating a dummy bucket for tests.
+ err = obj.MakeBucket(context.Background(), "unused-bucket", MakeBucketOptions{})
+ if err != nil {
+ // Failed to create newbucket, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ // Take 4 disks down, one more we loose quorum on 16 disk node.
+ for _, disk := range disks[:4] {
+ os.RemoveAll(disk)
+ }
+
+ testCases := []struct {
+ bucketName string
+ objName string
+ inputData []byte
+ inputMeta map[string]string
+ inputDataSize int64
+ // flag indicating whether the test should pass.
+ shouldPass bool
+ // expected error output.
+ expectedMd5 string
+ expectedError error
+ }{
+ // Validating for success cases.
+ {bucket, object, []byte("abcd"), map[string]string{"etag": "e2fc714c4727ee9395f324cd2e7f331f"}, int64(len("abcd")), true, "", nil},
+ {bucket, object, []byte("efgh"), map[string]string{"etag": "1f7690ebdd9b4caf8fab49ca1757bf27"}, int64(len("efgh")), true, "", nil},
+ {bucket, object, []byte("ijkl"), map[string]string{"etag": "09a0877d04abf8759f99adec02baf579"}, int64(len("ijkl")), true, "", nil},
+ {bucket, object, []byte("mnop"), map[string]string{"etag": "e132e96a5ddad6da8b07bba6f6131fef"}, int64(len("mnop")), true, "", nil},
+ }
+
+ sha256sum := ""
+ for i, testCase := range testCases {
+ objInfo, actualErr := obj.PutObject(context.Background(), testCase.bucketName, testCase.objName, mustGetPutObjReader(t, bytes.NewReader(testCase.inputData), testCase.inputDataSize, testCase.inputMeta["etag"], sha256sum), ObjectOptions{UserDefined: testCase.inputMeta})
+ if actualErr != nil && testCase.shouldPass {
+ t.Errorf("Test %d: %s: Expected to pass, but failed with: %s.", i+1, instanceType, actualErr.Error())
+ }
+ if actualErr == nil && !testCase.shouldPass {
+ t.Errorf("Test %d: %s: Expected to fail with \"%s\", but passed instead.", i+1, instanceType, testCase.expectedError.Error())
+ }
+ // Failed as expected, but does it fail for the expected reason.
+ if actualErr != nil && !testCase.shouldPass {
+ if testCase.expectedError.Error() != actualErr.Error() {
+ t.Errorf("Test %d: %s: Expected to fail with error \"%s\", but instead failed with error \"%s\" instead.", i+1,
+ instanceType, testCase.expectedError.Error(), actualErr.Error())
+ }
+ }
+ // Test passes as expected, but the output values are verified for correctness here.
+ if actualErr == nil && testCase.shouldPass {
+ // Asserting whether the md5 output is correct.
+ if testCase.inputMeta["etag"] != objInfo.ETag {
+ t.Errorf("Test %d: %s: Calculated Md5 different from the actual one %s.", i+1, instanceType, objInfo.ETag)
+ }
+ }
+ }
+
+ // This causes quorum failure verify.
+ os.RemoveAll(disks[len(disks)-1])
+
+ // Validate the last test.
+ testCase := struct {
+ bucketName string
+ objName string
+ inputData []byte
+ inputMeta map[string]string
+ inputDataSize int64
+ // flag indicating whether the test should pass.
+ shouldPass bool
+ // expected error output.
+ expectedMd5 string
+ expectedError error
+ }{
+ bucket,
+ object,
+ []byte("mnop"),
+ map[string]string{"etag": "e132e96a5ddad6da8b07bba6f6131fef"},
+ int64(len("mnop")),
+ false,
+ "",
+ errErasureWriteQuorum,
+ }
+
+ _, actualErr := obj.PutObject(context.Background(), testCase.bucketName, testCase.objName, mustGetPutObjReader(t, bytes.NewReader(testCase.inputData), testCase.inputDataSize, testCase.inputMeta["etag"], sha256sum), ObjectOptions{UserDefined: testCase.inputMeta})
+ if actualErr != nil && testCase.shouldPass {
+ t.Errorf("Test %d: %s: Expected to pass, but failed with: %s.", len(testCases)+1, instanceType, actualErr.Error())
+ }
+ // Failed as expected, but does it fail for the expected reason.
+ if actualErr != nil && !testCase.shouldPass {
+ if !errors.Is(actualErr, testCase.expectedError) {
+ t.Errorf("Test %d: %s: Expected to fail with error \"%s\", but instead failed with error \"%s\" instead.", len(testCases)+1, instanceType, testCase.expectedError.Error(), actualErr.Error())
+ }
+ }
+}
+
+// Wrapper for calling PutObject tests for both Erasure multiple disks and single node setup.
+func TestObjectAPIPutObjectStaleFiles(t *testing.T) {
+ ExecObjectLayerStaleFilesTest(t, testObjectAPIPutObjectStaleFiles)
+}
+
+// Tests validate correctness of PutObject.
+func testObjectAPIPutObjectStaleFiles(obj ObjectLayer, instanceType string, disks []string, t *testing.T) {
+ // Generating cases for which the PutObject fails.
+ bucket := "minio-bucket"
+ object := "minio-object"
+
+ // Create bucket.
+ err := obj.MakeBucket(context.Background(), bucket, MakeBucketOptions{})
+ if err != nil {
+ // Failed to create newbucket, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ data := []byte("hello, world")
+ // Create object.
+ _, err = obj.PutObject(context.Background(), bucket, object, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), ObjectOptions{})
+ if err != nil {
+ // Failed to create object, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ for _, disk := range disks {
+ tmpMetaDir := path.Join(disk, minioMetaTmpBucket)
+ files, err := os.ReadDir(tmpMetaDir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var found bool
+ for _, fi := range files {
+ if fi.Name() == ".trash" {
+ continue
+ }
+ found = true
+ }
+ if found {
+ t.Fatalf("%s: expected: empty, got: non-empty %#v", minioMetaTmpBucket, files)
+ }
+ }
+}
+
+// Wrapper for calling Multipart PutObject tests for both Erasure multiple disks and single node setup.
+func TestObjectAPIMultipartPutObjectStaleFiles(t *testing.T) {
+ ExecObjectLayerStaleFilesTest(t, testObjectAPIMultipartPutObjectStaleFiles)
+}
+
+// Tests validate correctness of PutObject.
+func testObjectAPIMultipartPutObjectStaleFiles(obj ObjectLayer, instanceType string, disks []string, t *testing.T) {
+ // Generating cases for which the PutObject fails.
+ bucket := "minio-bucket"
+ object := "minio-object"
+
+ // Create bucket.
+ err := obj.MakeBucket(context.Background(), bucket, MakeBucketOptions{})
+ if err != nil {
+ // Failed to create newbucket, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ opts := ObjectOptions{}
+ // Initiate Multipart Upload on the above created bucket.
+ res, err := obj.NewMultipartUpload(context.Background(), bucket, object, opts)
+ if err != nil {
+ // Failed to create NewMultipartUpload, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+ uploadID := res.UploadID
+
+ // Upload part1.
+ fiveMBBytes := bytes.Repeat([]byte("a"), 5*humanize.MiByte)
+ md5Writer := md5.New()
+ md5Writer.Write(fiveMBBytes)
+ etag1 := hex.EncodeToString(md5Writer.Sum(nil))
+ sha256sum := ""
+ _, err = obj.PutObjectPart(context.Background(), bucket, object, uploadID, 1, mustGetPutObjReader(t, bytes.NewReader(fiveMBBytes), int64(len(fiveMBBytes)), etag1, sha256sum), opts)
+ if err != nil {
+ // Failed to upload object part, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ // Upload part2.
+ data := []byte("hello, world")
+ md5Writer = md5.New()
+ md5Writer.Write(data)
+ etag2 := hex.EncodeToString(md5Writer.Sum(nil))
+ _, err = obj.PutObjectPart(context.Background(), bucket, object, uploadID, 2, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), etag2, sha256sum), opts)
+ if err != nil {
+ // Failed to upload object part, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ // Complete multipart.
+ parts := []CompletePart{
+ {ETag: etag1, PartNumber: 1},
+ {ETag: etag2, PartNumber: 2},
+ }
+ _, err = obj.CompleteMultipartUpload(context.Background(), bucket, object, uploadID, parts, ObjectOptions{})
+ if err != nil {
+ // Failed to complete multipart upload, abort.
+ t.Fatalf("%s : %s", instanceType, err.Error())
+ }
+
+ for _, disk := range disks {
+ tmpMetaDir := path.Join(disk, minioMetaTmpBucket)
+ files, err := os.ReadDir(tmpMetaDir)
+ if err != nil {
+ // It's OK to have non-existing tmpMetaDir.
+ if osIsNotExist(err) {
+ continue
+ }
+
+ // Print the error
+ t.Errorf("%s", err)
+ }
+
+ var found bool
+ for _, fi := range files {
+ if fi.Name() == ".trash" {
+ continue
+ }
+ found = true
+ break
+ }
+
+ if found {
+ t.Fatalf("%s: expected: empty, got: non-empty. content: %#v", tmpMetaDir, files)
+ }
+ }
+}
+
+// Benchmarks for ObjectLayer.PutObject().
+// The intent is to benchmark PutObject for various sizes ranging from few bytes to 100MB.
+// Also each of these Benchmarks are run both Erasure and FS backends.
+
+// BenchmarkPutObjectVerySmallFS - Benchmark FS.PutObject() for object size of 10 bytes.
+func BenchmarkPutObjectVerySmallFS(b *testing.B) {
+ benchmarkPutObject(b, "FS", 10)
+}
+
+// BenchmarkPutObjectVerySmallErasure - Benchmark Erasure.PutObject() for object size of 10 bytes.
+func BenchmarkPutObjectVerySmallErasure(b *testing.B) {
+ benchmarkPutObject(b, "Erasure", 10)
+}
+
+// BenchmarkPutObject10KbFS - Benchmark FS.PutObject() for object size of 10KB.
+func BenchmarkPutObject10KbFS(b *testing.B) {
+ benchmarkPutObject(b, "FS", 10*humanize.KiByte)
+}
+
+// BenchmarkPutObject10KbErasure - Benchmark Erasure.PutObject() for object size of 10KB.
+func BenchmarkPutObject10KbErasure(b *testing.B) {
+ benchmarkPutObject(b, "Erasure", 10*humanize.KiByte)
+}
+
+// BenchmarkPutObject100KbFS - Benchmark FS.PutObject() for object size of 100KB.
+func BenchmarkPutObject100KbFS(b *testing.B) {
+ benchmarkPutObject(b, "FS", 100*humanize.KiByte)
+}
+
+// BenchmarkPutObject100KbErasure - Benchmark Erasure.PutObject() for object size of 100KB.
+func BenchmarkPutObject100KbErasure(b *testing.B) {
+ benchmarkPutObject(b, "Erasure", 100*humanize.KiByte)
+}
+
+// BenchmarkPutObject1MbFS - Benchmark FS.PutObject() for object size of 1MB.
+func BenchmarkPutObject1MbFS(b *testing.B) {
+ benchmarkPutObject(b, "FS", 1*humanize.MiByte)
+}
+
+// BenchmarkPutObject1MbErasure - Benchmark Erasure.PutObject() for object size of 1MB.
+func BenchmarkPutObject1MbErasure(b *testing.B) {
+ benchmarkPutObject(b, "Erasure", 1*humanize.MiByte)
+}
+
+// BenchmarkPutObject5MbFS - Benchmark FS.PutObject() for object size of 5MB.
+func BenchmarkPutObject5MbFS(b *testing.B) {
+ benchmarkPutObject(b, "FS", 5*humanize.MiByte)
+}
+
+// BenchmarkPutObject5MbErasure - Benchmark Erasure.PutObject() for object size of 5MB.
+func BenchmarkPutObject5MbErasure(b *testing.B) {
+ benchmarkPutObject(b, "Erasure", 5*humanize.MiByte)
+}
+
+// BenchmarkPutObject10MbFS - Benchmark FS.PutObject() for object size of 10MB.
+func BenchmarkPutObject10MbFS(b *testing.B) {
+ benchmarkPutObject(b, "FS", 10*humanize.MiByte)
+}
+
+// BenchmarkPutObject10MbErasure - Benchmark Erasure.PutObject() for object size of 10MB.
+func BenchmarkPutObject10MbErasure(b *testing.B) {
+ benchmarkPutObject(b, "Erasure", 10*humanize.MiByte)
+}
+
+// BenchmarkPutObject25MbFS - Benchmark FS.PutObject() for object size of 25MB.
+func BenchmarkPutObject25MbFS(b *testing.B) {
+ benchmarkPutObject(b, "FS", 25*humanize.MiByte)
+}
+
+// BenchmarkPutObject25MbErasure - Benchmark Erasure.PutObject() for object size of 25MB.
+func BenchmarkPutObject25MbErasure(b *testing.B) {
+ benchmarkPutObject(b, "Erasure", 25*humanize.MiByte)
+}
+
+// BenchmarkPutObject50MbFS - Benchmark FS.PutObject() for object size of 50MB.
+func BenchmarkPutObject50MbFS(b *testing.B) {
+ benchmarkPutObject(b, "FS", 50*humanize.MiByte)
+}
+
+// BenchmarkPutObject50MbErasure - Benchmark Erasure.PutObject() for object size of 50MB.
+func BenchmarkPutObject50MbErasure(b *testing.B) {
+ benchmarkPutObject(b, "Erasure", 50*humanize.MiByte)
+}
+
+// parallel benchmarks for ObjectLayer.PutObject() .
+
+// BenchmarkParallelPutObjectVerySmallFS - BenchmarkParallel FS.PutObject() for object size of 10 bytes.
+func BenchmarkParallelPutObjectVerySmallFS(b *testing.B) {
+ benchmarkPutObjectParallel(b, "FS", 10)
+}
+
+// BenchmarkParallelPutObjectVerySmallErasure - BenchmarkParallel Erasure.PutObject() for object size of 10 bytes.
+func BenchmarkParallelPutObjectVerySmallErasure(b *testing.B) {
+ benchmarkPutObjectParallel(b, "Erasure", 10)
+}
+
+// BenchmarkParallelPutObject10KbFS - BenchmarkParallel FS.PutObject() for object size of 10KB.
+func BenchmarkParallelPutObject10KbFS(b *testing.B) {
+ benchmarkPutObjectParallel(b, "FS", 10*humanize.KiByte)
+}
+
+// BenchmarkParallelPutObject10KbErasure - BenchmarkParallel Erasure.PutObject() for object size of 10KB.
+func BenchmarkParallelPutObject10KbErasure(b *testing.B) {
+ benchmarkPutObjectParallel(b, "Erasure", 10*humanize.KiByte)
+}
+
+// BenchmarkParallelPutObject100KbFS - BenchmarkParallel FS.PutObject() for object size of 100KB.
+func BenchmarkParallelPutObject100KbFS(b *testing.B) {
+ benchmarkPutObjectParallel(b, "FS", 100*humanize.KiByte)
+}
+
+// BenchmarkParallelPutObject100KbErasure - BenchmarkParallel Erasure.PutObject() for object size of 100KB.
+func BenchmarkParallelPutObject100KbErasure(b *testing.B) {
+ benchmarkPutObjectParallel(b, "Erasure", 100*humanize.KiByte)
+}
+
+// BenchmarkParallelPutObject1MbFS - BenchmarkParallel FS.PutObject() for object size of 1MB.
+func BenchmarkParallelPutObject1MbFS(b *testing.B) {
+ benchmarkPutObjectParallel(b, "FS", 1*humanize.MiByte)
+}
+
+// BenchmarkParallelPutObject1MbErasure - BenchmarkParallel Erasure.PutObject() for object size of 1MB.
+func BenchmarkParallelPutObject1MbErasure(b *testing.B) {
+ benchmarkPutObjectParallel(b, "Erasure", 1*humanize.MiByte)
+}
+
+// BenchmarkParallelPutObject5MbFS - BenchmarkParallel FS.PutObject() for object size of 5MB.
+func BenchmarkParallelPutObject5MbFS(b *testing.B) {
+ benchmarkPutObjectParallel(b, "FS", 5*humanize.MiByte)
+}
+
+// BenchmarkParallelPutObject5MbErasure - BenchmarkParallel Erasure.PutObject() for object size of 5MB.
+func BenchmarkParallelPutObject5MbErasure(b *testing.B) {
+ benchmarkPutObjectParallel(b, "Erasure", 5*humanize.MiByte)
+}
+
+// BenchmarkParallelPutObject10MbFS - BenchmarkParallel FS.PutObject() for object size of 10MB.
+func BenchmarkParallelPutObject10MbFS(b *testing.B) {
+ benchmarkPutObjectParallel(b, "FS", 10*humanize.MiByte)
+}
+
+// BenchmarkParallelPutObject10MbErasure - BenchmarkParallel Erasure.PutObject() for object size of 10MB.
+func BenchmarkParallelPutObject10MbErasure(b *testing.B) {
+ benchmarkPutObjectParallel(b, "Erasure", 10*humanize.MiByte)
+}
+
+// BenchmarkParallelPutObject25MbFS - BenchmarkParallel FS.PutObject() for object size of 25MB.
+func BenchmarkParallelPutObject25MbFS(b *testing.B) {
+ benchmarkPutObjectParallel(b, "FS", 25*humanize.MiByte)
+}
+
+// BenchmarkParallelPutObject25MbErasure - BenchmarkParallel Erasure.PutObject() for object size of 25MB.
+func BenchmarkParallelPutObject25MbErasure(b *testing.B) {
+ benchmarkPutObjectParallel(b, "Erasure", 25*humanize.MiByte)
+}
diff --git a/cmd/object-api-utils.go b/cmd/object-api-utils.go
new file mode 100644
index 00000000..5d791ce4
--- /dev/null
+++ b/cmd/object-api-utils.go
@@ -0,0 +1,1306 @@
+// Copyright (c) 2015-2024 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "io"
+ "math/rand"
+ "net"
+ "net/http"
+ "path"
+ "runtime"
+ "slices"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+ "unicode/utf8"
+ "unsafe"
+
+ "github.com/google/uuid"
+ "github.com/klauspost/compress/s2"
+ "github.com/klauspost/readahead"
+ "github.com/minio/minio-go/v7/pkg/s3utils"
+ "github.com/minio/minio/internal/config/compress"
+ "github.com/minio/minio/internal/config/dns"
+ "github.com/minio/minio/internal/config/storageclass"
+ "github.com/minio/minio/internal/crypto"
+ "github.com/minio/minio/internal/hash"
+ xhttp "github.com/minio/minio/internal/http"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/pkg/v3/trie"
+ "github.com/minio/pkg/v3/wildcard"
+ "github.com/valyala/bytebufferpool"
+)
+
+const (
+ // MinIO meta bucket.
+ minioMetaBucket = ".minio.sys"
+ // Multipart meta prefix.
+ mpartMetaPrefix = "multipart"
+ // MinIO Multipart meta prefix.
+ minioMetaMultipartBucket = minioMetaBucket + SlashSeparator + mpartMetaPrefix
+ // MinIO tmp meta prefix.
+ minioMetaTmpBucket = minioMetaBucket + "/tmp"
+ // MinIO tmp meta prefix for deleted objects.
+ minioMetaTmpDeletedBucket = minioMetaTmpBucket + "/.trash"
+
+ // DNS separator (period), used for bucket name validation.
+ dnsDelimiter = "."
+ // On compressed files bigger than this;
+ compReadAheadSize = 100 << 20
+ // Read this many buffers ahead.
+ compReadAheadBuffers = 5
+ // Size of each buffer.
+ compReadAheadBufSize = 1 << 20
+ // Pad Encrypted+Compressed files to a multiple of this.
+ compPadEncrypted = 256
+ // Disable compressed file indices below this size
+ compMinIndexSize = 8 << 20
+)
+
+// getkeyeparator - returns the separator to be used for
+// persisting on drive.
+//
+// - ":" is used on non-windows platforms
+// - "_" is used on windows platforms
+func getKeySeparator() string {
+ if runtime.GOOS == globalWindowsOSName {
+ return "_"
+ }
+ return ":"
+}
+
+// isMinioBucket returns true if given bucket is a MinIO internal
+// bucket and false otherwise.
+func isMinioMetaBucketName(bucket string) bool {
+ return strings.HasPrefix(bucket, minioMetaBucket)
+}
+
+// IsValidBucketName verifies that a bucket name is in accordance with
+// Amazon's requirements (i.e. DNS naming conventions). It must be 3-63
+// characters long, and it must be a sequence of one or more labels
+// separated by periods. Each label can contain lowercase ascii
+// letters, decimal digits and hyphens, but must not begin or end with
+// a hyphen. See:
+// http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html
+func IsValidBucketName(bucket string) bool {
+ // Special case when bucket is equal to one of the meta buckets.
+ if isMinioMetaBucketName(bucket) {
+ return true
+ }
+ if len(bucket) < 3 || len(bucket) > 63 {
+ return false
+ }
+
+ // Split on dot and check each piece conforms to rules.
+ allNumbers := true
+ pieces := strings.Split(bucket, dnsDelimiter)
+ for _, piece := range pieces {
+ if len(piece) == 0 || piece[0] == '-' ||
+ piece[len(piece)-1] == '-' {
+ // Current piece has 0-length or starts or
+ // ends with a hyphen.
+ return false
+ }
+ // Now only need to check if each piece is a valid
+ // 'label' in AWS terminology and if the bucket looks
+ // like an IP address.
+ isNotNumber := false
+ for i := range len(piece) {
+ switch {
+ case (piece[i] >= 'a' && piece[i] <= 'z' ||
+ piece[i] == '-'):
+ // Found a non-digit character, so
+ // this piece is not a number.
+ isNotNumber = true
+ case piece[i] >= '0' && piece[i] <= '9':
+ // Nothing to do.
+ default:
+ // Found invalid character.
+ return false
+ }
+ }
+ allNumbers = allNumbers && !isNotNumber
+ }
+ // Does the bucket name look like an IP address?
+ return len(pieces) != 4 || !allNumbers
+}
+
+// IsValidObjectName verifies an object name in accordance with Amazon's
+// requirements. It cannot exceed 1024 characters and must be a valid UTF8
+// string.
+//
+// See:
+// http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html
+//
+// You should avoid the following characters in a key name because of
+// significant special handling for consistency across all
+// applications.
+//
+// Rejects strings with following characters.
+//
+// - Backslash ("\")
+//
+// additionally minio does not support object names with trailing SlashSeparator.
+func IsValidObjectName(object string) bool {
+ if len(object) == 0 {
+ return false
+ }
+ if HasSuffix(object, SlashSeparator) {
+ return false
+ }
+ return IsValidObjectPrefix(object)
+}
+
+// IsValidObjectPrefix verifies whether the prefix is a valid object name.
+// Its valid to have a empty prefix.
+func IsValidObjectPrefix(object string) bool {
+ if hasBadPathComponent(object) {
+ return false
+ }
+ if !utf8.ValidString(object) {
+ return false
+ }
+ if strings.Contains(object, `//`) {
+ return false
+ }
+ // This is valid for AWS S3 but it will never
+ // work with file systems, we will reject here
+ // to return object name invalid rather than
+ // a cryptic error from the file system.
+ return !strings.ContainsRune(object, 0)
+}
+
+// checkObjectNameForLengthAndSlash -check for the validity of object name length and prefis as slash
+func checkObjectNameForLengthAndSlash(bucket, object string) error {
+ // Check for the length of object name
+ if len(object) > 1024 {
+ return ObjectNameTooLong{
+ Bucket: bucket,
+ Object: object,
+ }
+ }
+ // Check for slash as prefix in object name
+ if HasPrefix(object, SlashSeparator) {
+ return ObjectNamePrefixAsSlash{
+ Bucket: bucket,
+ Object: object,
+ }
+ }
+ if runtime.GOOS == globalWindowsOSName {
+ // Explicitly disallowed characters on windows.
+ // Avoids most problematic names.
+ if strings.ContainsAny(object, `\:*?"|<>`) {
+ return ObjectNameInvalid{
+ Bucket: bucket,
+ Object: object,
+ }
+ }
+ }
+ return nil
+}
+
+// SlashSeparator - slash separator.
+const SlashSeparator = "/"
+
+// SlashSeparatorChar - slash separator.
+const SlashSeparatorChar = '/'
+
+// retainSlash - retains slash from a path.
+func retainSlash(s string) string {
+ if s == "" {
+ return s
+ }
+ return strings.TrimSuffix(s, SlashSeparator) + SlashSeparator
+}
+
+// pathsJoinPrefix - like pathJoin retains trailing SlashSeparator
+// for all elements, prepends them with 'prefix' respectively.
+func pathsJoinPrefix(prefix string, elem ...string) (paths []string) {
+ paths = make([]string, len(elem))
+ for i, e := range elem {
+ paths[i] = pathJoin(prefix, e)
+ }
+ return paths
+}
+
+// string concat alternative to s1 + s2 with low overhead.
+func concat(ss ...string) string {
+ length := len(ss)
+ if length == 0 {
+ return ""
+ }
+ // create & allocate the memory in advance.
+ n := 0
+ for i := range length {
+ n += len(ss[i])
+ }
+ b := make([]byte, 0, n)
+ for i := range length {
+ b = append(b, ss[i]...)
+ }
+ return unsafe.String(unsafe.SliceData(b), n)
+}
+
+// pathJoin - like path.Join() but retains trailing SlashSeparator of the last element
+func pathJoin(elem ...string) string {
+ sb := bytebufferpool.Get()
+ defer func() {
+ sb.Reset()
+ bytebufferpool.Put(sb)
+ }()
+
+ return pathJoinBuf(sb, elem...)
+}
+
+// pathJoinBuf - like path.Join() but retains trailing SlashSeparator of the last element.
+// Provide a string builder to reduce allocation.
+func pathJoinBuf(dst *bytebufferpool.ByteBuffer, elem ...string) string {
+ trailingSlash := len(elem) > 0 && hasSuffixByte(elem[len(elem)-1], SlashSeparatorChar)
+ dst.Reset()
+ added := 0
+ for _, e := range elem {
+ if added > 0 || e != "" {
+ if added > 0 {
+ dst.WriteByte(SlashSeparatorChar)
+ }
+ dst.WriteString(e)
+ added += len(e)
+ }
+ }
+
+ if pathNeedsClean(dst.Bytes()) {
+ s := path.Clean(dst.String())
+ if trailingSlash {
+ return s + SlashSeparator
+ }
+ return s
+ }
+ if trailingSlash {
+ dst.WriteByte(SlashSeparatorChar)
+ }
+ return dst.String()
+}
+
+// hasSuffixByte returns true if the last byte of s is 'suffix'
+func hasSuffixByte(s string, suffix byte) bool {
+ return len(s) > 0 && s[len(s)-1] == suffix
+}
+
+// pathNeedsClean returns whether path.Clean may change the path.
+// Will detect all cases that will be cleaned,
+// but may produce false positives on non-trivial paths.
+func pathNeedsClean(path []byte) bool {
+ if len(path) == 0 {
+ return true
+ }
+
+ rooted := path[0] == '/'
+ n := len(path)
+
+ r, w := 0, 0
+ if rooted {
+ r, w = 1, 1
+ }
+
+ for r < n {
+ switch {
+ case path[r] > 127:
+ // Non ascii.
+ return true
+ case path[r] == '/':
+ // multiple / elements
+ return true
+ case path[r] == '.' && (r+1 == n || path[r+1] == '/'):
+ // . element - assume it has to be cleaned.
+ return true
+ case path[r] == '.' && path[r+1] == '.' && (r+2 == n || path[r+2] == '/'):
+ // .. element: remove to last / - assume it has to be cleaned.
+ return true
+ default:
+ // real path element.
+ // add slash if needed
+ if rooted && w != 1 || !rooted && w != 0 {
+ w++
+ }
+ // copy element
+ for ; r < n && path[r] != '/'; r++ {
+ w++
+ }
+ // allow one slash, not at end
+ if r < n-1 && path[r] == '/' {
+ r++
+ }
+ }
+ }
+
+ // Turn empty string into "."
+ if w == 0 {
+ return true
+ }
+
+ return false
+}
+
+// mustGetUUID - get a random UUID.
+func mustGetUUID() string {
+ u, err := uuid.NewRandom()
+ if err != nil {
+ logger.CriticalIf(GlobalContext, err)
+ }
+
+ return u.String()
+}
+
+// mustGetUUIDBytes - get a random UUID as 16 bytes unencoded.
+func mustGetUUIDBytes() []byte {
+ u, err := uuid.NewRandom()
+ if err != nil {
+ logger.CriticalIf(GlobalContext, err)
+ }
+ return u[:]
+}
+
+// Create an s3 compatible MD5sum for complete multipart transaction.
+func getCompleteMultipartMD5(parts []CompletePart) string {
+ var finalMD5Bytes []byte
+ for _, part := range parts {
+ md5Bytes, err := hex.DecodeString(canonicalizeETag(part.ETag))
+ if err != nil {
+ finalMD5Bytes = append(finalMD5Bytes, []byte(part.ETag)...)
+ } else {
+ finalMD5Bytes = append(finalMD5Bytes, md5Bytes...)
+ }
+ }
+ s3MD5 := fmt.Sprintf("%s-%d", getMD5Hash(finalMD5Bytes), len(parts))
+ return s3MD5
+}
+
+// Clean unwanted fields from metadata
+func cleanMetadata(metadata map[string]string) map[string]string {
+ // Remove STANDARD StorageClass
+ metadata = removeStandardStorageClass(metadata)
+ // Clean meta etag keys 'md5Sum', 'etag', "expires", "x-amz-tagging".
+ return cleanMetadataKeys(metadata, "md5Sum", "etag", "expires", xhttp.AmzObjectTagging, "last-modified", VersionPurgeStatusKey)
+}
+
+// Filter X-Amz-Storage-Class field only if it is set to STANDARD.
+// This is done since AWS S3 doesn't return STANDARD Storage class as response header.
+func removeStandardStorageClass(metadata map[string]string) map[string]string {
+ if metadata[xhttp.AmzStorageClass] == storageclass.STANDARD {
+ delete(metadata, xhttp.AmzStorageClass)
+ }
+ return metadata
+}
+
+// cleanMetadataKeys takes keyNames to be filtered
+// and returns a new map with all the entries with keyNames removed.
+func cleanMetadataKeys(metadata map[string]string, keyNames ...string) map[string]string {
+ newMeta := make(map[string]string, len(metadata))
+ for k, v := range metadata {
+ if slices.Contains(keyNames, k) {
+ continue
+ }
+ newMeta[k] = v
+ }
+ return newMeta
+}
+
+// Extracts etag value from the metadata.
+func extractETag(metadata map[string]string) string {
+ etag, ok := metadata["etag"]
+ if !ok {
+ // md5Sum tag is kept for backward compatibility.
+ etag = metadata["md5Sum"]
+ }
+ // Success.
+ return etag
+}
+
+// HasPrefix - Prefix matcher string matches prefix in a platform specific way.
+// For example on windows since its case insensitive we are supposed
+// to do case insensitive checks.
+func HasPrefix(s string, prefix string) bool {
+ if runtime.GOOS == globalWindowsOSName {
+ return stringsHasPrefixFold(s, prefix)
+ }
+ return strings.HasPrefix(s, prefix)
+}
+
+// HasSuffix - Suffix matcher string matches suffix in a platform specific way.
+// For example on windows since its case insensitive we are supposed
+// to do case insensitive checks.
+func HasSuffix(s string, suffix string) bool {
+ if runtime.GOOS == globalWindowsOSName {
+ return strings.HasSuffix(strings.ToLower(s), strings.ToLower(suffix))
+ }
+ return strings.HasSuffix(s, suffix)
+}
+
+// Validates if two strings are equal.
+func isStringEqual(s1 string, s2 string) bool {
+ if runtime.GOOS == globalWindowsOSName {
+ return strings.EqualFold(s1, s2)
+ }
+ return s1 == s2
+}
+
+// Ignores all reserved bucket names or invalid bucket names.
+func isReservedOrInvalidBucket(bucketEntry string, strict bool) bool {
+ if bucketEntry == "" {
+ return true
+ }
+
+ bucketEntry = strings.TrimSuffix(bucketEntry, SlashSeparator)
+ if strict {
+ if err := s3utils.CheckValidBucketNameStrict(bucketEntry); err != nil {
+ return true
+ }
+ } else {
+ if err := s3utils.CheckValidBucketName(bucketEntry); err != nil {
+ return true
+ }
+ }
+ return isMinioMetaBucket(bucketEntry) || isMinioReservedBucket(bucketEntry)
+}
+
+// Returns true if input bucket is a reserved minio meta bucket '.minio.sys'.
+func isMinioMetaBucket(bucketName string) bool {
+ return bucketName == minioMetaBucket
+}
+
+// Returns true if input bucket is a reserved minio bucket 'minio'.
+func isMinioReservedBucket(bucketName string) bool {
+ return bucketName == minioReservedBucket
+}
+
+// returns a slice of hosts by reading a slice of DNS records
+func getHostsSlice(records []dns.SrvRecord) []string {
+ hosts := make([]string, len(records))
+ for i, r := range records {
+ hosts[i] = net.JoinHostPort(r.Host, string(r.Port))
+ }
+ return hosts
+}
+
+// returns an online host (and corresponding port) from a slice of DNS records
+func getHostFromSrv(records []dns.SrvRecord) (host string) {
+ hosts := getHostsSlice(records)
+ rng := rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
+ var d net.Dialer
+ var retry int
+ for retry < len(hosts) {
+ ctx, cancel := context.WithTimeout(GlobalContext, 300*time.Millisecond)
+
+ host = hosts[rng.Intn(len(hosts))]
+ conn, err := d.DialContext(ctx, "tcp", host)
+ cancel()
+ if err != nil {
+ retry++
+ continue
+ }
+ conn.Close()
+ break
+ }
+
+ return host
+}
+
+// IsCompressed returns true if the object is marked as compressed.
+func (o *ObjectInfo) IsCompressed() bool {
+ _, ok := o.UserDefined[ReservedMetadataPrefix+"compression"]
+ return ok
+}
+
+// IsCompressedOK returns whether the object is compressed and can be decompressed.
+func (o *ObjectInfo) IsCompressedOK() (bool, error) {
+ scheme, ok := o.UserDefined[ReservedMetadataPrefix+"compression"]
+ if !ok {
+ return false, nil
+ }
+ switch scheme {
+ case compressionAlgorithmV1, compressionAlgorithmV2:
+ return true, nil
+ }
+ return true, fmt.Errorf("unknown compression scheme: %s", scheme)
+}
+
+// GetActualSize - returns the actual size of the stored object
+func (o ObjectInfo) GetActualSize() (int64, error) {
+ if o.ActualSize != nil {
+ return *o.ActualSize, nil
+ }
+ if o.IsCompressed() {
+ sizeStr := o.UserDefined[ReservedMetadataPrefix+"actual-size"]
+ if sizeStr != "" {
+ size, err := strconv.ParseInt(sizeStr, 10, 64)
+ if err != nil {
+ return -1, errInvalidDecompressedSize
+ }
+ return size, nil
+ }
+ var actualSize int64
+ for _, part := range o.Parts {
+ actualSize += part.ActualSize
+ }
+ if (actualSize == 0) && (actualSize != o.Size) {
+ return -1, errInvalidDecompressedSize
+ }
+ return actualSize, nil
+ }
+ if _, ok := crypto.IsEncrypted(o.UserDefined); ok {
+ sizeStr := o.UserDefined[ReservedMetadataPrefix+"actual-size"]
+ if sizeStr != "" {
+ size, err := strconv.ParseInt(sizeStr, 10, 64)
+ if err != nil {
+ return -1, errObjectTampered
+ }
+ return size, nil
+ }
+ actualSize, err := o.DecryptedSize()
+ if err != nil {
+ return -1, err
+ }
+ if (actualSize == 0) && (actualSize != o.Size) {
+ return -1, errObjectTampered
+ }
+ return actualSize, nil
+ }
+ return o.Size, nil
+}
+
+// Disabling compression for encrypted enabled requests.
+// Using compression and encryption together enables room for side channel attacks.
+// Eliminate non-compressible objects by extensions/content-types.
+func isCompressible(header http.Header, object string) bool {
+ globalCompressConfigMu.Lock()
+ cfg := globalCompressConfig
+ globalCompressConfigMu.Unlock()
+
+ return !excludeForCompression(header, object, cfg)
+}
+
+// Eliminate the non-compressible objects.
+func excludeForCompression(header http.Header, object string, cfg compress.Config) bool {
+ objStr := object
+ contentType := header.Get(xhttp.ContentType)
+ if !cfg.Enabled {
+ return true
+ }
+
+ if crypto.Requested(header) && !cfg.AllowEncrypted {
+ return true
+ }
+
+ // We strictly disable compression for standard extensions/content-types (`compressed`).
+ if hasStringSuffixInSlice(objStr, standardExcludeCompressExtensions) || hasPattern(standardExcludeCompressContentTypes, contentType) {
+ return true
+ }
+
+ // Filter compression includes.
+ if len(cfg.Extensions) == 0 && len(cfg.MimeTypes) == 0 {
+ // Nothing to filter, include everything.
+ return false
+ }
+
+ if len(cfg.Extensions) > 0 && hasStringSuffixInSlice(objStr, cfg.Extensions) {
+ // Matched an extension to compress, do not exclude.
+ return false
+ }
+
+ if len(cfg.MimeTypes) > 0 && hasPattern(cfg.MimeTypes, contentType) {
+ // Matched an MIME type to compress, do not exclude.
+ return false
+ }
+
+ // Did not match any inclusion filters, exclude from compression.
+ return true
+}
+
+// Utility which returns if a string is present in the list.
+// Comparison is case insensitive. Explicit short-circuit if
+// the list contains the wildcard "*".
+func hasStringSuffixInSlice(str string, list []string) bool {
+ str = strings.ToLower(str)
+ for _, v := range list {
+ if v == "*" {
+ return true
+ }
+
+ if strings.HasSuffix(str, strings.ToLower(v)) {
+ return true
+ }
+ }
+ return false
+}
+
+// Returns true if any of the given wildcard patterns match the matchStr.
+func hasPattern(patterns []string, matchStr string) bool {
+ for _, pattern := range patterns {
+ if ok := wildcard.MatchSimple(pattern, matchStr); ok {
+ return true
+ }
+ }
+ return false
+}
+
+// Returns the part file name which matches the partNumber and etag.
+func getPartFile(entriesTrie *trie.Trie, partNumber int, etag string) (partFile string) {
+ for _, match := range entriesTrie.PrefixMatch(fmt.Sprintf("%.5d.%s.", partNumber, etag)) {
+ partFile = match
+ break
+ }
+ return partFile
+}
+
+func partNumberToRangeSpec(oi ObjectInfo, partNumber int) *HTTPRangeSpec {
+ if oi.Size == 0 || len(oi.Parts) == 0 {
+ return nil
+ }
+
+ var start int64
+ end := int64(-1)
+ for i := 0; i < len(oi.Parts) && i < partNumber; i++ {
+ start = end + 1
+ end = start + oi.Parts[i].ActualSize - 1
+ }
+
+ return &HTTPRangeSpec{Start: start, End: end}
+}
+
+// Returns the compressed offset which should be skipped.
+// If encrypted offsets are adjusted for encrypted block headers/trailers.
+// Since de-compression is after decryption encryption overhead is only added to compressedOffset.
+func getCompressedOffsets(oi ObjectInfo, offset int64, decrypt func([]byte) ([]byte, error)) (compressedOffset int64, partSkip int64, firstPart int, decryptSkip int64, seqNum uint32) {
+ var skipLength int64
+ var cumulativeActualSize int64
+ var firstPartIdx int
+ for i, part := range oi.Parts {
+ cumulativeActualSize += part.ActualSize
+ if cumulativeActualSize <= offset {
+ compressedOffset += part.Size
+ } else {
+ firstPartIdx = i
+ skipLength = cumulativeActualSize - part.ActualSize
+ break
+ }
+ }
+ partSkip = offset - skipLength
+
+ // Load index and skip more if feasible.
+ if partSkip > 0 && len(oi.Parts) > firstPartIdx && len(oi.Parts[firstPartIdx].Index) > 0 {
+ _, isEncrypted := crypto.IsEncrypted(oi.UserDefined)
+ if isEncrypted {
+ dec, err := decrypt(oi.Parts[firstPartIdx].Index)
+ if err == nil {
+ // Load Index
+ var idx s2.Index
+ _, err := idx.Load(s2.RestoreIndexHeaders(dec))
+
+ // Find compressed/uncompressed offsets of our partskip
+ compOff, uCompOff, err2 := idx.Find(partSkip)
+
+ if err == nil && err2 == nil && compOff > 0 {
+ // Encrypted.
+ const sseDAREEncPackageBlockSize = SSEDAREPackageBlockSize + SSEDAREPackageMetaSize
+ // Number of full blocks in skipped area
+ seqNum = uint32(compOff / SSEDAREPackageBlockSize)
+ // Skip this many inside a decrypted block to get to compression block start
+ decryptSkip = compOff % SSEDAREPackageBlockSize
+ // Skip this number of full blocks.
+ skipEnc := compOff / SSEDAREPackageBlockSize
+ skipEnc *= sseDAREEncPackageBlockSize
+ compressedOffset += skipEnc
+ // Skip this number of uncompressed bytes.
+ partSkip -= uCompOff
+ }
+ }
+ } else {
+ // Not encrypted
+ var idx s2.Index
+ _, err := idx.Load(s2.RestoreIndexHeaders(oi.Parts[firstPartIdx].Index))
+
+ // Find compressed/uncompressed offsets of our partskip
+ compOff, uCompOff, err2 := idx.Find(partSkip)
+
+ if err == nil && err2 == nil && compOff > 0 {
+ compressedOffset += compOff
+ partSkip -= uCompOff
+ }
+ }
+ }
+
+ return compressedOffset, partSkip, firstPartIdx, decryptSkip, seqNum
+}
+
+// GetObjectReader is a type that wraps a reader with a lock to
+// provide a ReadCloser interface that unlocks on Close()
+type GetObjectReader struct {
+ io.Reader
+ ObjInfo ObjectInfo
+ cleanUpFns []func()
+ once sync.Once
+}
+
+// WithCleanupFuncs sets additional cleanup functions to be called when closing
+// the GetObjectReader.
+func (g *GetObjectReader) WithCleanupFuncs(fns ...func()) *GetObjectReader {
+ g.cleanUpFns = append(g.cleanUpFns, fns...)
+ return g
+}
+
+// NewGetObjectReaderFromReader sets up a GetObjectReader with a given
+// reader. This ignores any object properties.
+func NewGetObjectReaderFromReader(r io.Reader, oi ObjectInfo, opts ObjectOptions, cleanupFns ...func()) (*GetObjectReader, error) {
+ if opts.CheckPrecondFn != nil && opts.CheckPrecondFn(oi) {
+ // Call the cleanup funcs
+ for i := len(cleanupFns) - 1; i >= 0; i-- {
+ cleanupFns[i]()
+ }
+ return nil, PreConditionFailed{}
+ }
+ return &GetObjectReader{
+ ObjInfo: oi,
+ Reader: r,
+ cleanUpFns: cleanupFns,
+ }, nil
+}
+
+// ObjReaderFn is a function type that takes a reader and returns
+// GetObjectReader and an error. Request headers are passed to provide
+// encryption parameters. cleanupFns allow cleanup funcs to be
+// registered for calling after usage of the reader.
+type ObjReaderFn func(inputReader io.Reader, h http.Header, cleanupFns ...func()) (r *GetObjectReader, err error)
+
+// NewGetObjectReader creates a new GetObjectReader. The cleanUpFns
+// are called on Close() in FIFO order as passed in ObjReadFn(). NOTE: It is
+// assumed that clean up functions do not panic (otherwise, they may
+// not all run!).
+func NewGetObjectReader(rs *HTTPRangeSpec, oi ObjectInfo, opts ObjectOptions, h http.Header) (
+ fn ObjReaderFn, off, length int64, err error,
+) {
+ if opts.CheckPrecondFn != nil && opts.CheckPrecondFn(oi) {
+ return nil, 0, 0, PreConditionFailed{}
+ }
+
+ if rs == nil && opts.PartNumber > 0 {
+ rs = partNumberToRangeSpec(oi, opts.PartNumber)
+ }
+
+ _, isEncrypted := crypto.IsEncrypted(oi.UserDefined)
+ isCompressed, err := oi.IsCompressedOK()
+ if err != nil {
+ return nil, 0, 0, err
+ }
+
+ // if object is encrypted and it is a restore request or if NoDecryption
+ // was requested, fetch content without decrypting.
+ if opts.Transition.RestoreRequest != nil || opts.NoDecryption {
+ isEncrypted = false
+ isCompressed = false
+ }
+
+ // Calculate range to read (different for encrypted/compressed objects)
+ switch {
+ case isCompressed:
+ var firstPart int
+ if opts.PartNumber > 0 {
+ // firstPart is an index to Parts slice,
+ // make sure that PartNumber uses the
+ // index value properly.
+ firstPart = opts.PartNumber - 1
+ }
+
+ // If compressed, we start from the beginning of the part.
+ // Read the decompressed size from the meta.json.
+ actualSize, err := oi.GetActualSize()
+ if err != nil {
+ return nil, 0, 0, err
+ }
+ var decryptSkip int64
+ var seqNum uint32
+
+ off, length = int64(0), oi.Size
+ decOff, decLength := int64(0), actualSize
+ if rs != nil {
+ off, length, err = rs.GetOffsetLength(actualSize)
+ if err != nil {
+ return nil, 0, 0, err
+ }
+ decrypt := func(b []byte) ([]byte, error) {
+ return b, nil
+ }
+ if isEncrypted {
+ decrypt = func(b []byte) ([]byte, error) {
+ return oi.compressionIndexDecrypt(b, h)
+ }
+ }
+ // In case of range based queries on multiparts, the offset and length are reduced.
+ off, decOff, firstPart, decryptSkip, seqNum = getCompressedOffsets(oi, off, decrypt)
+ decLength = length
+ length = oi.Size - off
+ // For negative length we read everything.
+ if decLength < 0 {
+ decLength = actualSize - decOff
+ }
+
+ // Reply back invalid range if the input offset and length fall out of range.
+ if decOff > actualSize || decOff+decLength > actualSize {
+ return nil, 0, 0, errInvalidRange
+ }
+ }
+ fn = func(inputReader io.Reader, h http.Header, cFns ...func()) (r *GetObjectReader, err error) {
+ if isEncrypted {
+ copySource := h.Get(xhttp.AmzServerSideEncryptionCopyCustomerAlgorithm) != ""
+ // Attach decrypter on inputReader
+ inputReader, err = DecryptBlocksRequestR(inputReader, h, seqNum, firstPart, oi, copySource)
+ if err != nil {
+ // Call the cleanup funcs
+ for i := len(cFns) - 1; i >= 0; i-- {
+ cFns[i]()
+ }
+ return nil, err
+ }
+ if decryptSkip > 0 {
+ inputReader = xioutil.NewSkipReader(inputReader, decryptSkip)
+ }
+ oi.Size = decLength
+ }
+ // Decompression reader.
+ var dopts []s2.ReaderOption
+ if off > 0 || decOff > 0 {
+ // We are not starting at the beginning, so ignore stream identifiers.
+ dopts = append(dopts, s2.ReaderIgnoreStreamIdentifier())
+ }
+ s2Reader := s2.NewReader(inputReader, dopts...)
+ // Apply the skipLen and limit on the decompressed stream.
+ if decOff > 0 {
+ if err = s2Reader.Skip(decOff); err != nil {
+ // Call the cleanup funcs
+ for i := len(cFns) - 1; i >= 0; i-- {
+ cFns[i]()
+ }
+ return nil, err
+ }
+ }
+
+ decReader := io.LimitReader(s2Reader, decLength)
+ if decLength > compReadAheadSize {
+ rah, err := readahead.NewReaderSize(decReader, compReadAheadBuffers, compReadAheadBufSize)
+ if err == nil {
+ decReader = rah
+ cFns = append([]func(){func() {
+ rah.Close()
+ }}, cFns...)
+ }
+ }
+ oi.Size = decLength
+
+ // Assemble the GetObjectReader
+ r = &GetObjectReader{
+ ObjInfo: oi,
+ Reader: decReader,
+ cleanUpFns: cFns,
+ }
+ return r, nil
+ }
+
+ case isEncrypted:
+ var seqNumber uint32
+ var partStart int
+ var skipLen int64
+
+ off, length, skipLen, seqNumber, partStart, err = oi.GetDecryptedRange(rs)
+ if err != nil {
+ return nil, 0, 0, err
+ }
+ var decSize int64
+ decSize, err = oi.DecryptedSize()
+ if err != nil {
+ return nil, 0, 0, err
+ }
+ var decRangeLength int64
+ decRangeLength, err = rs.GetLength(decSize)
+ if err != nil {
+ return nil, 0, 0, err
+ }
+
+ // We define a closure that performs decryption given
+ // a reader that returns the desired range of
+ // encrypted bytes. The header parameter is used to
+ // provide encryption parameters.
+ fn = func(inputReader io.Reader, h http.Header, cFns ...func()) (r *GetObjectReader, err error) {
+ copySource := h.Get(xhttp.AmzServerSideEncryptionCopyCustomerAlgorithm) != ""
+
+ // Attach decrypter on inputReader
+ var decReader io.Reader
+ decReader, err = DecryptBlocksRequestR(inputReader, h, seqNumber, partStart, oi, copySource)
+ if err != nil {
+ // Call the cleanup funcs
+ for i := len(cFns) - 1; i >= 0; i-- {
+ cFns[i]()
+ }
+ return nil, err
+ }
+
+ oi.ETag = getDecryptedETag(h, oi, false)
+
+ // Apply the skipLen and limit on the
+ // decrypted stream
+ decReader = io.LimitReader(xioutil.NewSkipReader(decReader, skipLen), decRangeLength)
+
+ // Assemble the GetObjectReader
+ r = &GetObjectReader{
+ ObjInfo: oi,
+ Reader: decReader,
+ cleanUpFns: cFns,
+ }
+ return r, nil
+ }
+
+ default:
+ off, length, err = rs.GetOffsetLength(oi.Size)
+ if err != nil {
+ return nil, 0, 0, err
+ }
+ fn = func(inputReader io.Reader, _ http.Header, cFns ...func()) (r *GetObjectReader, err error) {
+ r = &GetObjectReader{
+ ObjInfo: oi,
+ Reader: inputReader,
+ cleanUpFns: cFns,
+ }
+ return r, nil
+ }
+ }
+ return fn, off, length, nil
+}
+
+// Close - calls the cleanup actions in reverse order
+func (g *GetObjectReader) Close() error {
+ if g == nil {
+ return nil
+ }
+ // sync.Once is used here to ensure that Close() is
+ // idempotent.
+ g.once.Do(func() {
+ for i := len(g.cleanUpFns) - 1; i >= 0; i-- {
+ g.cleanUpFns[i]()
+ }
+ })
+ return nil
+}
+
+// compressionIndexEncrypter returns a function that will read data from input,
+// encrypt it using the provided key and return the result.
+func compressionIndexEncrypter(key crypto.ObjectKey, input func() []byte) func() []byte {
+ var data []byte
+ var fetched bool
+ return func() []byte {
+ if !fetched {
+ data = input()
+ fetched = true
+ }
+ return metadataEncrypter(key)("compression-index", data)
+ }
+}
+
+// compressionIndexDecrypt reverses compressionIndexEncrypter.
+func (o *ObjectInfo) compressionIndexDecrypt(input []byte, h http.Header) ([]byte, error) {
+ return o.metadataDecrypter(h)("compression-index", input)
+}
+
+// SealMD5CurrFn seals md5sum with object encryption key and returns sealed
+// md5sum
+type SealMD5CurrFn func([]byte) []byte
+
+// PutObjReader is a type that wraps sio.EncryptReader and
+// underlying hash.Reader in a struct
+type PutObjReader struct {
+ *hash.Reader // actual data stream
+ rawReader *hash.Reader // original data stream
+ sealMD5Fn SealMD5CurrFn
+}
+
+// Size returns the absolute number of bytes the Reader
+// will return during reading. It returns -1 for unlimited
+// data.
+func (p *PutObjReader) Size() int64 {
+ return p.Reader.Size()
+}
+
+// MD5CurrentHexString returns the current MD5Sum or encrypted MD5Sum
+// as a hex encoded string
+func (p *PutObjReader) MD5CurrentHexString() string {
+ md5sumCurr := p.rawReader.MD5Current()
+ var appendHyphen bool
+ // md5sumcurr is not empty in two scenarios
+ // - server is running in strict compatibility mode
+ // - client set Content-Md5 during PUT operation
+ if len(md5sumCurr) == 0 {
+ // md5sumCurr is only empty when we are running
+ // in non-compatibility mode.
+ md5sumCurr = make([]byte, 16)
+ rand.Read(md5sumCurr)
+ appendHyphen = true
+ }
+ if p.sealMD5Fn != nil {
+ md5sumCurr = p.sealMD5Fn(md5sumCurr)
+ }
+ if appendHyphen {
+ // Make sure to return etag string upto 32 length, for SSE
+ // requests ETag might be longer and the code decrypting the
+ // ETag ignores ETag in multipart ETag form i.e -N
+ return hex.EncodeToString(md5sumCurr)[:32] + "-1"
+ }
+ return hex.EncodeToString(md5sumCurr)
+}
+
+// WithEncryption sets up encrypted reader and the sealing for content md5sum
+// using objEncKey. Unsealed md5sum is computed from the rawReader setup when
+// NewPutObjReader was called. It returns an error if called on an uninitialized
+// PutObjReader.
+func (p *PutObjReader) WithEncryption(encReader *hash.Reader, objEncKey *crypto.ObjectKey) (*PutObjReader, error) {
+ if p.Reader == nil {
+ return nil, errors.New("put-object reader uninitialized")
+ }
+ p.Reader = encReader
+ p.sealMD5Fn = sealETagFn(*objEncKey)
+ return p, nil
+}
+
+// NewPutObjReader returns a new PutObjReader. It uses given hash.Reader's
+// MD5Current method to construct md5sum when requested downstream.
+func NewPutObjReader(rawReader *hash.Reader) *PutObjReader {
+ return &PutObjReader{Reader: rawReader, rawReader: rawReader}
+}
+
+// RawServerSideChecksumResult returns the ServerSideChecksumResult from the
+// underlying rawReader, since the PutObjReader might be encrypted data and
+// thus any checksum from that would be incorrect.
+func (p *PutObjReader) RawServerSideChecksumResult() *hash.Checksum {
+ if p.rawReader != nil {
+ return p.rawReader.ServerSideChecksumResult
+ }
+ return nil
+}
+
+func sealETag(encKey crypto.ObjectKey, md5CurrSum []byte) []byte {
+ var emptyKey [32]byte
+ if bytes.Equal(encKey[:], emptyKey[:]) {
+ return md5CurrSum
+ }
+ return encKey.SealETag(md5CurrSum)
+}
+
+func sealETagFn(key crypto.ObjectKey) SealMD5CurrFn {
+ fn := func(md5sumcurr []byte) []byte {
+ return sealETag(key, md5sumcurr)
+ }
+ return fn
+}
+
+// compressOpts are the options for writing compressed data.
+var compressOpts []s2.WriterOption
+
+func init() {
+ if runtime.GOARCH == "amd64" {
+ // On amd64 we have assembly and can use stronger compression.
+ compressOpts = append(compressOpts, s2.WriterBetterCompression())
+ }
+}
+
+// newS2CompressReader will read data from r, compress it and return the compressed data as a Reader.
+// Use Close to ensure resources are released on incomplete streams.
+//
+// input 'on' is always recommended such that this function works
+// properly, because we do not wish to create an object even if
+// client closed the stream prematurely.
+func newS2CompressReader(r io.Reader, on int64, encrypted bool) (rc io.ReadCloser, idx func() []byte) {
+ pr, pw := io.Pipe()
+ // Copy input to compressor
+ opts := compressOpts
+ if encrypted {
+ // The values used for padding are not a security concern,
+ // but we choose pseudo-random numbers instead of just zeros.
+ rng := rand.New(rand.NewSource(time.Now().UnixNano()))
+ opts = append([]s2.WriterOption{s2.WriterPadding(compPadEncrypted), s2.WriterPaddingSrc(rng)}, compressOpts...)
+ }
+ comp := s2.NewWriter(pw, opts...)
+ indexCh := make(chan []byte, 1)
+ go func() {
+ defer xioutil.SafeClose(indexCh)
+ cn, err := io.Copy(comp, r)
+ if err != nil {
+ comp.Close()
+ pw.CloseWithError(err)
+ return
+ }
+ if on > 0 && on != cn {
+ // if client didn't sent all data
+ // from the client verify here.
+ comp.Close()
+ pw.CloseWithError(IncompleteBody{})
+ return
+ }
+ // Close the stream.
+ // If more than compMinIndexSize was written, generate index.
+ if cn > compMinIndexSize {
+ idx, err := comp.CloseIndex()
+ idx = s2.RemoveIndexHeaders(idx)
+ indexCh <- idx
+ pw.CloseWithError(err)
+ return
+ }
+ pw.CloseWithError(comp.Close())
+ }()
+ var gotIdx []byte
+ return pr, func() []byte {
+ if gotIdx != nil {
+ return gotIdx
+ }
+ // Will get index or nil if closed.
+ gotIdx = <-indexCh
+ return gotIdx
+ }
+}
+
+// compressSelfTest performs a self-test to ensure that compression
+// algorithms completes a roundtrip. If any algorithm
+// produces an incorrect checksum it fails with a hard error.
+//
+// compressSelfTest tries to catch any issue in the compression implementation
+// early instead of silently corrupting data.
+func compressSelfTest() {
+ // 4 MB block.
+ // Approx runtime ~30ms
+ data := make([]byte, 4<<20)
+ rng := rand.New(rand.NewSource(0))
+ for i := range data {
+ // Generate compressible stream...
+ data[i] = byte(rng.Int63() & 3)
+ }
+ failOnErr := func(err error) {
+ if err != nil {
+ logger.Fatal(errSelfTestFailure, "compress: error on self-test: %v", err)
+ }
+ }
+ const skip = 2<<20 + 511
+ r, _ := newS2CompressReader(bytes.NewBuffer(data), int64(len(data)), true)
+ b, err := io.ReadAll(r)
+ failOnErr(err)
+ failOnErr(r.Close())
+ // Decompression reader.
+ s2Reader := s2.NewReader(bytes.NewBuffer(b))
+ // Apply the skipLen on the decompressed stream.
+ failOnErr(s2Reader.Skip(skip))
+ got, err := io.ReadAll(s2Reader)
+ failOnErr(err)
+ if !bytes.Equal(got, data[skip:]) {
+ logger.Fatal(errSelfTestFailure, "compress: self-test roundtrip mismatch.")
+ }
+}
+
+// getDiskInfos returns the disk information for the provided disks.
+// If a disk is nil or an error is returned the result will be nil as well.
+func getDiskInfos(ctx context.Context, disks ...StorageAPI) []*DiskInfo {
+ res := make([]*DiskInfo, len(disks))
+ opts := DiskInfoOptions{}
+ for i, disk := range disks {
+ if disk == nil {
+ continue
+ }
+ if di, err := disk.DiskInfo(ctx, opts); err == nil {
+ res[i] = &di
+ }
+ }
+ return res
+}
+
+// hasSpaceFor returns whether the disks in `di` have space for and object of a given size.
+func hasSpaceFor(di []*DiskInfo, size int64) (bool, error) {
+ // We multiply the size by 2 to account for erasure coding.
+ size *= 2
+ if size < 0 {
+ // If no size, assume diskAssumeUnknownSize.
+ size = diskAssumeUnknownSize
+ }
+
+ var available uint64
+ var total uint64
+ var nDisks int
+ for _, disk := range di {
+ if disk == nil || disk.Total == 0 {
+ // Disk offline, no inodes or something else is wrong.
+ continue
+ }
+ nDisks++
+ total += disk.Total
+ available += disk.Total - disk.Used
+ }
+
+ if nDisks < len(di)/2 || nDisks <= 0 {
+ var errs []error
+ for index, disk := range di {
+ switch {
+ case disk == nil:
+ errs = append(errs, fmt.Errorf("disk[%d]: offline", index))
+ case disk.Error != "":
+ errs = append(errs, fmt.Errorf("disk %s: %s", disk.Endpoint, disk.Error))
+ case disk.Total == 0:
+ errs = append(errs, fmt.Errorf("disk %s: total is zero", disk.Endpoint))
+ }
+ }
+ // Log disk errors.
+ peersLogIf(context.Background(), errors.Join(errs...))
+ return false, fmt.Errorf("not enough online disks to calculate the available space, need %d, found %d", (len(di)/2)+1, nDisks)
+ }
+
+ // Check we have enough on each disk, ignoring diskFillFraction.
+ perDisk := size / int64(nDisks)
+ for _, disk := range di {
+ if disk == nil || disk.Total == 0 {
+ continue
+ }
+ if !globalIsErasureSD && disk.FreeInodes < diskMinInodes && disk.UsedInodes > 0 {
+ // We have an inode count, but not enough inodes.
+ return false, nil
+ }
+ if int64(disk.Free) <= perDisk {
+ return false, nil
+ }
+ }
+
+ // Make sure we can fit "size" on to the disk without getting above the diskFillFraction
+ if available < uint64(size) {
+ return false, nil
+ }
+
+ // How much will be left after adding the file.
+ available -= uint64(size)
+
+ // wantLeft is how much space there at least must be left.
+ wantLeft := uint64(float64(total) * (1.0 - diskFillFraction))
+ return available > wantLeft, nil
+}
diff --git a/cmd/object-api-utils_test.go b/cmd/object-api-utils_test.go
new file mode 100644
index 00000000..53992bf8
--- /dev/null
+++ b/cmd/object-api-utils_test.go
@@ -0,0 +1,883 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "math/rand"
+ "net/http"
+ "net/http/httptest"
+ "path"
+ "reflect"
+ "runtime"
+ "strconv"
+ "testing"
+
+ "github.com/klauspost/compress/s2"
+ "github.com/minio/minio/internal/auth"
+ "github.com/minio/minio/internal/config/compress"
+ "github.com/minio/minio/internal/crypto"
+ "github.com/minio/pkg/v3/trie"
+)
+
+func pathJoinOld(elem ...string) string {
+ trailingSlash := ""
+ if len(elem) > 0 {
+ if hasSuffixByte(elem[len(elem)-1], SlashSeparatorChar) {
+ trailingSlash = SlashSeparator
+ }
+ }
+ return path.Join(elem...) + trailingSlash
+}
+
+func concatNaive(ss ...string) string {
+ rs := ss[0]
+ for i := 1; i < len(ss); i++ {
+ rs += ss[i]
+ }
+ return rs
+}
+
+func benchmark(b *testing.B, data []string) {
+ b.Run("concat naive", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+ for b.Loop() {
+ concatNaive(data...)
+ }
+ })
+ b.Run("concat fast", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+ for b.Loop() {
+ concat(data...)
+ }
+ })
+}
+
+func BenchmarkConcatImplementation(b *testing.B) {
+ data := make([]string, 2)
+ rng := rand.New(rand.NewSource(0))
+ for i := range 2 {
+ var tmp [16]byte
+ rng.Read(tmp[:])
+ data[i] = hex.EncodeToString(tmp[:])
+ }
+ b.ResetTimer()
+ benchmark(b, data)
+}
+
+func BenchmarkPathJoinOld(b *testing.B) {
+ b.Run("PathJoin", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ pathJoinOld("volume", "path/path/path")
+ }
+ })
+}
+
+func BenchmarkPathJoin(b *testing.B) {
+ b.Run("PathJoin", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ pathJoin("volume", "path/path/path")
+ }
+ })
+}
+
+// Wrapper
+func TestPathTraversalExploit(t *testing.T) {
+ if runtime.GOOS != globalWindowsOSName {
+ t.Skip()
+ }
+ defer DetectTestLeak(t)()
+ ExecExtendedObjectLayerAPITest(t, testPathTraversalExploit, []string{"PutObject"})
+}
+
+// testPathTraversal exploit test, exploits path traversal on windows
+// with following object names "\\../.minio.sys/config/iam/${username}/identity.json"
+// #16852
+func testPathTraversalExploit(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ credentials auth.Credentials, t *testing.T,
+) {
+ if err := newTestConfig(globalMinioDefaultRegion, obj); err != nil {
+ t.Fatalf("Initializing config.json failed")
+ }
+
+ objectName := `\../.minio.sys/config/hello.txt`
+
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ rec := httptest.NewRecorder()
+ // construct HTTP request for Get Object end point.
+ req, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, objectName),
+ int64(5), bytes.NewReader([]byte("hello")), credentials.AccessKey, credentials.SecretKey, map[string]string{})
+ if err != nil {
+ t.Fatalf("failed to create HTTP request for Put Object: %v", err)
+ }
+
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler.
+ apiRouter.ServeHTTP(rec, req)
+
+ ctx, cancel := context.WithCancel(GlobalContext)
+ defer cancel()
+
+ // Now check if we actually wrote to backend (regardless of the response
+ // returned by the server).
+ z := obj.(*erasureServerPools)
+ xl := z.serverPools[0].sets[0]
+ erasureDisks := xl.getDisks()
+ parts, errs := readAllFileInfo(ctx, erasureDisks, "", bucketName, objectName, "", false, false)
+ for i := range parts {
+ if errs[i] == nil {
+ if parts[i].Name == objectName {
+ t.Errorf("path traversal allowed to allow writing to minioMetaBucket: %s", instanceType)
+ }
+ }
+ }
+}
+
+// Tests validate bucket name.
+func TestIsValidBucketName(t *testing.T) {
+ testCases := []struct {
+ bucketName string
+ shouldPass bool
+ }{
+ // cases which should pass the test.
+ // passing in valid bucket names.
+ {"lol", true},
+ {"1-this-is-valid", true},
+ {"1-this-too-is-valid-1", true},
+ {"this.works.too.1", true},
+ {"1234567", true},
+ {"123", true},
+ {"s3-eu-west-1.amazonaws.com", true},
+ {"ideas-are-more-powerful-than-guns", true},
+ {"testbucket", true},
+ {"1bucket", true},
+ {"bucket1", true},
+ {"a.b", true},
+ {"ab.a.bc", true},
+ // cases for which test should fail.
+ // passing invalid bucket names.
+ {"------", false},
+ {"my..bucket", false},
+ {"192.168.1.1", false},
+ {"$this-is-not-valid-too", false},
+ {"contains-$-dollar", false},
+ {"contains-^-caret", false},
+ {"contains-$-dollar", false},
+ {"contains-$-dollar", false},
+ {"......", false},
+ {"", false},
+ {"a", false},
+ {"ab", false},
+ {".starts-with-a-dot", false},
+ {"ends-with-a-dot.", false},
+ {"ends-with-a-dash-", false},
+ {"-starts-with-a-dash", false},
+ {"THIS-BEGINS-WITH-UPPERCASe", false},
+ {"tHIS-ENDS-WITH-UPPERCASE", false},
+ {"ThisBeginsAndEndsWithUpperCasE", false},
+ {"una ñina", false},
+ {"dash-.may-not-appear-next-to-dot", false},
+ {"dash.-may-not-appear-next-to-dot", false},
+ {"dash-.-may-not-appear-next-to-dot", false},
+ {"lalalallalallalalalallalallalala-thestring-size-is-greater-than-63", false},
+ }
+
+ for i, testCase := range testCases {
+ isValidBucketName := IsValidBucketName(testCase.bucketName)
+ if testCase.shouldPass && !isValidBucketName {
+ t.Errorf("Test case %d: Expected \"%s\" to be a valid bucket name", i+1, testCase.bucketName)
+ }
+ if !testCase.shouldPass && isValidBucketName {
+ t.Errorf("Test case %d: Expected bucket name \"%s\" to be invalid", i+1, testCase.bucketName)
+ }
+ }
+}
+
+// Tests for validate object name.
+func TestIsValidObjectName(t *testing.T) {
+ testCases := []struct {
+ objectName string
+ shouldPass bool
+ }{
+ // cases which should pass the test.
+ // passing in valid object name.
+ {"object", true},
+ {"The Shining Script .pdf", true},
+ {"Cost Benefit Analysis (2009-2010).pptx", true},
+ {"117Gn8rfHL2ACARPAhaFd0AGzic9pUbIA/5OCn5A", true},
+ {"SHØRT", true},
+ {"f*le", true},
+ {"contains-^-caret", true},
+ {"contains-|-pipe", true},
+ {"contains-`-tick", true},
+ {"..test", true},
+ {".. test", true},
+ {". test", true},
+ {".test", true},
+ {"There are far too many object names, and far too few bucket names!", true},
+ {"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~/!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~)", true},
+ {"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", true},
+ {"␀␁␂␃␄␅␆␇␈␉␊␋␌␍␎␏␐␑␒␓␔␕␖␗␘␙␚␛␜␝␞␟␡", true},
+ {"trailing VT␋/trailing VT␋", true},
+ {"␋leading VT/␋leading VT", true},
+ {"~leading tilde", true},
+ {"\rleading CR", true},
+ {"\nleading LF", true},
+ {"\tleading HT", true},
+ {"trailing CR\r", true},
+ {"trailing LF\n", true},
+ {"trailing HT\t", true},
+ // cases for which test should fail.
+ // passing invalid object names.
+ {"", false},
+ {"a/b/c/", false},
+ {"../../etc", false},
+ {"../../", false},
+ {"/../../etc", false},
+ {" ../etc", false},
+ {"./././", false},
+ {"./etc", false},
+ {`contains//double/forwardslash`, false},
+ {`//contains/double-forwardslash-prefix`, false},
+ {string([]byte{0xff, 0xfe, 0xfd}), false},
+ }
+
+ for i, testCase := range testCases {
+ isValidObjectName := IsValidObjectName(testCase.objectName)
+ if testCase.shouldPass && !isValidObjectName {
+ t.Errorf("Test case %d: Expected \"%s\" to be a valid object name", i+1, testCase.objectName)
+ }
+ if !testCase.shouldPass && isValidObjectName {
+ t.Errorf("Test case %d: Expected object name \"%s\" to be invalid", i+1, testCase.objectName)
+ }
+ }
+}
+
+// Tests getCompleteMultipartMD5
+func TestGetCompleteMultipartMD5(t *testing.T) {
+ testCases := []struct {
+ parts []CompletePart
+ expectedResult string
+ expectedErr string
+ }{
+ // Wrong MD5 hash string, returns md5um of hash
+ {[]CompletePart{{ETag: "wrong-md5-hash-string"}}, "0deb8cb07527b4b2669c861cb9653607-1", ""},
+
+ // Single CompletePart with valid MD5 hash string.
+ {[]CompletePart{{ETag: "cf1f738a5924e645913c984e0fe3d708"}}, "10dc1617fbcf0bd0858048cb96e6bd77-1", ""},
+
+ // Multiple CompletePart with valid MD5 hash string.
+ {[]CompletePart{{ETag: "cf1f738a5924e645913c984e0fe3d708"}, {ETag: "9ccbc9a80eee7fb6fdd22441db2aedbd"}}, "0239a86b5266bb624f0ac60ba2aed6c8-2", ""},
+ }
+
+ for i, test := range testCases {
+ result := getCompleteMultipartMD5(test.parts)
+ if result != test.expectedResult {
+ t.Fatalf("test %d failed: expected: result=%v, got=%v", i+1, test.expectedResult, result)
+ }
+ }
+}
+
+// TestIsMinioBucketName - Tests isMinioBucketName helper function.
+func TestIsMinioMetaBucketName(t *testing.T) {
+ testCases := []struct {
+ bucket string
+ result bool
+ }{
+ // MinIO meta bucket.
+ {
+ bucket: minioMetaBucket,
+ result: true,
+ },
+ // MinIO meta bucket.
+ {
+ bucket: minioMetaMultipartBucket,
+ result: true,
+ },
+ // MinIO meta bucket.
+ {
+ bucket: minioMetaTmpBucket,
+ result: true,
+ },
+ // Normal bucket
+ {
+ bucket: "mybucket",
+ result: false,
+ },
+ }
+
+ for i, test := range testCases {
+ actual := isMinioMetaBucketName(test.bucket)
+ if actual != test.result {
+ t.Errorf("Test %d - expected %v but received %v",
+ i+1, test.result, actual)
+ }
+ }
+}
+
+// Tests RemoveStandardStorageClass method. Expectation is metadata map
+// should be cleared of x-amz-storage-class, if it is set to STANDARD
+func TestRemoveStandardStorageClass(t *testing.T) {
+ tests := []struct {
+ name string
+ metadata map[string]string
+ want map[string]string
+ }{
+ {
+ name: "1",
+ metadata: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86", "x-amz-storage-class": "STANDARD"},
+ want: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86"},
+ },
+ {
+ name: "2",
+ metadata: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86", "x-amz-storage-class": "REDUCED_REDUNDANCY"},
+ want: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86", "x-amz-storage-class": "REDUCED_REDUNDANCY"},
+ },
+ {
+ name: "3",
+ metadata: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86"},
+ want: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86"},
+ },
+ }
+ for _, tt := range tests {
+ if got := removeStandardStorageClass(tt.metadata); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("Test %s failed, expected %v, got %v", tt.name, tt.want, got)
+ }
+ }
+}
+
+// Tests CleanMetadata method. Expectation is metadata map
+// should be cleared of etag, md5Sum and x-amz-storage-class, if it is set to STANDARD
+func TestCleanMetadata(t *testing.T) {
+ tests := []struct {
+ name string
+ metadata map[string]string
+ want map[string]string
+ }{
+ {
+ name: "1",
+ metadata: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86", "x-amz-storage-class": "STANDARD"},
+ want: map[string]string{"content-type": "application/octet-stream"},
+ },
+ {
+ name: "2",
+ metadata: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86", "x-amz-storage-class": "REDUCED_REDUNDANCY"},
+ want: map[string]string{"content-type": "application/octet-stream", "x-amz-storage-class": "REDUCED_REDUNDANCY"},
+ },
+ {
+ name: "3",
+ metadata: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86", "md5Sum": "abcde"},
+ want: map[string]string{"content-type": "application/octet-stream"},
+ },
+ }
+ for _, tt := range tests {
+ if got := cleanMetadata(tt.metadata); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("Test %s failed, expected %v, got %v", tt.name, tt.want, got)
+ }
+ }
+}
+
+// Tests CleanMetadataKeys method. Expectation is metadata map
+// should be cleared of keys passed to CleanMetadataKeys method
+func TestCleanMetadataKeys(t *testing.T) {
+ tests := []struct {
+ name string
+ metadata map[string]string
+ keys []string
+ want map[string]string
+ }{
+ {
+ name: "1",
+ metadata: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86", "x-amz-storage-class": "STANDARD", "md5": "abcde"},
+ keys: []string{"etag", "md5"},
+ want: map[string]string{"content-type": "application/octet-stream", "x-amz-storage-class": "STANDARD"},
+ },
+ {
+ name: "2",
+ metadata: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86", "x-amz-storage-class": "REDUCED_REDUNDANCY", "md5sum": "abcde"},
+ keys: []string{"etag", "md5sum"},
+ want: map[string]string{"content-type": "application/octet-stream", "x-amz-storage-class": "REDUCED_REDUNDANCY"},
+ },
+ {
+ name: "3",
+ metadata: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86", "xyz": "abcde"},
+ keys: []string{"etag", "xyz"},
+ want: map[string]string{"content-type": "application/octet-stream"},
+ },
+ }
+ for _, tt := range tests {
+ if got := cleanMetadataKeys(tt.metadata, tt.keys...); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("Test %s failed, expected %v, got %v", tt.name, tt.want, got)
+ }
+ }
+}
+
+// Tests isCompressed method
+func TestIsCompressed(t *testing.T) {
+ testCases := []struct {
+ objInfo ObjectInfo
+ result bool
+ err bool
+ }{
+ 0: {
+ objInfo: ObjectInfo{
+ UserDefined: map[string]string{
+ "X-Minio-Internal-compression": compressionAlgorithmV1,
+ "content-type": "application/octet-stream",
+ "etag": "b3ff3ef3789147152fbfbc50efba4bfd-2",
+ },
+ },
+ result: true,
+ },
+ 1: {
+ objInfo: ObjectInfo{
+ UserDefined: map[string]string{
+ "X-Minio-Internal-compression": compressionAlgorithmV2,
+ "content-type": "application/octet-stream",
+ "etag": "b3ff3ef3789147152fbfbc50efba4bfd-2",
+ },
+ },
+ result: true,
+ },
+ 2: {
+ objInfo: ObjectInfo{
+ UserDefined: map[string]string{
+ "X-Minio-Internal-compression": "unknown/compression/type",
+ "content-type": "application/octet-stream",
+ "etag": "b3ff3ef3789147152fbfbc50efba4bfd-2",
+ },
+ },
+ result: true,
+ err: true,
+ },
+ 3: {
+ objInfo: ObjectInfo{
+ UserDefined: map[string]string{
+ "X-Minio-Internal-compression": compressionAlgorithmV2,
+ "content-type": "application/octet-stream",
+ "etag": "b3ff3ef3789147152fbfbc50efba4bfd-2",
+ crypto.MetaIV: "yes",
+ },
+ },
+ result: true,
+ err: false,
+ },
+ 4: {
+ objInfo: ObjectInfo{
+ UserDefined: map[string]string{
+ "X-Minio-Internal-XYZ": "klauspost/compress/s2",
+ "content-type": "application/octet-stream",
+ "etag": "b3ff3ef3789147152fbfbc50efba4bfd-2",
+ },
+ },
+ result: false,
+ },
+ 5: {
+ objInfo: ObjectInfo{
+ UserDefined: map[string]string{
+ "content-type": "application/octet-stream",
+ "etag": "b3ff3ef3789147152fbfbc50efba4bfd-2",
+ },
+ },
+ result: false,
+ },
+ }
+ for i, test := range testCases {
+ t.Run(strconv.Itoa(i), func(t *testing.T) {
+ got := test.objInfo.IsCompressed()
+ if got != test.result {
+ t.Errorf("IsCompressed: Expected %v but received %v",
+ test.result, got)
+ }
+ got, gErr := test.objInfo.IsCompressedOK()
+ if got != test.result {
+ t.Errorf("IsCompressedOK: Expected %v but received %v",
+ test.result, got)
+ }
+ if gErr != nil != test.err {
+ t.Errorf("IsCompressedOK: want error: %t, got error: %v", test.err, gErr)
+ }
+ })
+ }
+}
+
+// Tests excludeForCompression.
+func TestExcludeForCompression(t *testing.T) {
+ testCases := []struct {
+ object string
+ header http.Header
+ result bool
+ }{
+ {
+ object: "object.txt",
+ header: http.Header{
+ "Content-Type": []string{"application/zip"},
+ },
+ result: true,
+ },
+ {
+ object: "object.zip",
+ header: http.Header{
+ "Content-Type": []string{"application/XYZ"},
+ },
+ result: true,
+ },
+ {
+ object: "object.json",
+ header: http.Header{
+ "Content-Type": []string{"application/json"},
+ },
+ result: false,
+ },
+ {
+ object: "object.txt",
+ header: http.Header{
+ "Content-Type": []string{"text/plain"},
+ },
+ result: false,
+ },
+ {
+ object: "object",
+ header: http.Header{
+ "Content-Type": []string{"text/something"},
+ },
+ result: false,
+ },
+ }
+ for i, test := range testCases {
+ got := excludeForCompression(test.header, test.object, compress.Config{
+ Enabled: true,
+ })
+ if got != test.result {
+ t.Errorf("Test %d - expected %v but received %v",
+ i+1, test.result, got)
+ }
+ }
+}
+
+func BenchmarkGetPartFileWithTrie(b *testing.B) {
+ b.ResetTimer()
+
+ entriesTrie := trie.NewTrie()
+ for i := 1; i <= 10000; i++ {
+ entriesTrie.Insert(fmt.Sprintf("%.5d.8a034f82cb9cb31140d87d3ce2a9ede3.67108864", i))
+ }
+
+ for i := 1; i <= 10000; i++ {
+ partFile := getPartFile(entriesTrie, i, "8a034f82cb9cb31140d87d3ce2a9ede3")
+ if partFile == "" {
+ b.Fatal("partFile returned is empty")
+ }
+ }
+
+ b.ReportAllocs()
+}
+
+func TestGetActualSize(t *testing.T) {
+ testCases := []struct {
+ objInfo ObjectInfo
+ result int64
+ }{
+ {
+ objInfo: ObjectInfo{
+ UserDefined: map[string]string{
+ "X-Minio-Internal-compression": "klauspost/compress/s2",
+ "content-type": "application/octet-stream",
+ "etag": "b3ff3ef3789147152fbfbc50efba4bfd-2",
+ },
+ Parts: []ObjectPartInfo{
+ {
+ Size: 39235668,
+ ActualSize: 67108864,
+ },
+ {
+ Size: 19177372,
+ ActualSize: 32891137,
+ },
+ },
+ Size: 100000001,
+ },
+ result: 100000001,
+ },
+ {
+ objInfo: ObjectInfo{
+ UserDefined: map[string]string{
+ "X-Minio-Internal-compression": "klauspost/compress/s2",
+ "X-Minio-Internal-actual-size": "841",
+ "content-type": "application/octet-stream",
+ "etag": "b3ff3ef3789147152fbfbc50efba4bfd-2",
+ },
+ Parts: []ObjectPartInfo{},
+ Size: 841,
+ },
+ result: 841,
+ },
+ {
+ objInfo: ObjectInfo{
+ UserDefined: map[string]string{
+ "X-Minio-Internal-compression": "klauspost/compress/s2",
+ "content-type": "application/octet-stream",
+ "etag": "b3ff3ef3789147152fbfbc50efba4bfd-2",
+ },
+ Parts: []ObjectPartInfo{},
+ Size: 100,
+ },
+ result: -1,
+ },
+ }
+ for i, test := range testCases {
+ got, _ := test.objInfo.GetActualSize()
+ if got != test.result {
+ t.Errorf("Test %d - expected %d but received %d",
+ i+1, test.result, got)
+ }
+ }
+}
+
+func TestGetCompressedOffsets(t *testing.T) {
+ testCases := []struct {
+ objInfo ObjectInfo
+ offset int64
+ startOffset int64
+ snappyStartOffset int64
+ firstPart int
+ }{
+ 0: {
+ objInfo: ObjectInfo{
+ Parts: []ObjectPartInfo{
+ {
+ Size: 39235668,
+ ActualSize: 67108864,
+ },
+ {
+ Size: 19177372,
+ ActualSize: 32891137,
+ },
+ },
+ },
+ offset: 79109865,
+ startOffset: 39235668,
+ snappyStartOffset: 12001001,
+ firstPart: 1,
+ },
+ 1: {
+ objInfo: ObjectInfo{
+ Parts: []ObjectPartInfo{
+ {
+ Size: 39235668,
+ ActualSize: 67108864,
+ },
+ {
+ Size: 19177372,
+ ActualSize: 32891137,
+ },
+ },
+ },
+ offset: 19109865,
+ startOffset: 0,
+ snappyStartOffset: 19109865,
+ },
+ 2: {
+ objInfo: ObjectInfo{
+ Parts: []ObjectPartInfo{
+ {
+ Size: 39235668,
+ ActualSize: 67108864,
+ },
+ {
+ Size: 19177372,
+ ActualSize: 32891137,
+ },
+ },
+ },
+ offset: 0,
+ startOffset: 0,
+ snappyStartOffset: 0,
+ },
+ }
+ for i, test := range testCases {
+ startOffset, snappyStartOffset, firstPart, _, _ := getCompressedOffsets(test.objInfo, test.offset, nil)
+ if startOffset != test.startOffset {
+ t.Errorf("Test %d - expected startOffset %d but received %d",
+ i, test.startOffset, startOffset)
+ }
+ if snappyStartOffset != test.snappyStartOffset {
+ t.Errorf("Test %d - expected snappyOffset %d but received %d",
+ i, test.snappyStartOffset, snappyStartOffset)
+ }
+ if firstPart != test.firstPart {
+ t.Errorf("Test %d - expected firstPart %d but received %d",
+ i, test.firstPart, firstPart)
+ }
+ }
+}
+
+func TestS2CompressReader(t *testing.T) {
+ tests := []struct {
+ name string
+ data []byte
+ wantIdx bool
+ }{
+ {name: "empty", data: nil},
+ {name: "small", data: []byte("hello, world!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")},
+ {name: "large", data: bytes.Repeat([]byte("hello, world"), 1000000), wantIdx: true},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ buf := make([]byte, 100) // make small buffer to ensure multiple reads are required for large case
+
+ r, idxCB := newS2CompressReader(bytes.NewReader(tt.data), int64(len(tt.data)), false)
+ defer r.Close()
+
+ var rdrBuf bytes.Buffer
+ _, err := io.CopyBuffer(&rdrBuf, r, buf)
+ if err != nil {
+ t.Fatal(err)
+ }
+ r.Close()
+ idx := idxCB()
+ if !tt.wantIdx && len(idx) > 0 {
+ t.Errorf("index returned above threshold")
+ }
+ if tt.wantIdx {
+ if idx == nil {
+ t.Errorf("no index returned")
+ }
+ var index s2.Index
+ _, err = index.Load(s2.RestoreIndexHeaders(idx))
+ if err != nil {
+ t.Errorf("error loading index: %v", err)
+ }
+ t.Log("size:", len(idx))
+ t.Log(string(index.JSON()))
+ if index.TotalUncompressed != int64(len(tt.data)) {
+ t.Errorf("Expected size %d, got %d", len(tt.data), index.TotalUncompressed)
+ }
+ }
+ var stdBuf bytes.Buffer
+ w := s2.NewWriter(&stdBuf)
+ _, err = io.CopyBuffer(w, bytes.NewReader(tt.data), buf)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = w.Close()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var (
+ got = rdrBuf.Bytes()
+ want = stdBuf.Bytes()
+ )
+ if !bytes.Equal(got, want) {
+ t.Errorf("encoded data does not match\n\t%q\n\t%q", got, want)
+ }
+
+ var decBuf bytes.Buffer
+ decRdr := s2.NewReader(&rdrBuf)
+ _, err = io.Copy(&decBuf, decRdr)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !bytes.Equal(tt.data, decBuf.Bytes()) {
+ t.Errorf("roundtrip failed\n\t%q\n\t%q", tt.data, decBuf.Bytes())
+ }
+ })
+ }
+}
+
+func Test_pathNeedsClean(t *testing.T) {
+ type pathTest struct {
+ path, result string
+ }
+
+ cleantests := []pathTest{
+ // Already clean
+ {"", "."},
+ {"abc", "abc"},
+ {"abc/def", "abc/def"},
+ {"a/b/c", "a/b/c"},
+ {".", "."},
+ {"..", ".."},
+ {"../..", "../.."},
+ {"../../abc", "../../abc"},
+ {"/abc", "/abc"},
+ {"/abc/def", "/abc/def"},
+ {"/", "/"},
+
+ // Remove trailing slash
+ {"abc/", "abc"},
+ {"abc/def/", "abc/def"},
+ {"a/b/c/", "a/b/c"},
+ {"./", "."},
+ {"../", ".."},
+ {"../../", "../.."},
+ {"/abc/", "/abc"},
+
+ // Remove doubled slash
+ {"abc//def//ghi", "abc/def/ghi"},
+ {"//abc", "/abc"},
+ {"///abc", "/abc"},
+ {"//abc//", "/abc"},
+ {"abc//", "abc"},
+
+ // Remove . elements
+ {"abc/./def", "abc/def"},
+ {"/./abc/def", "/abc/def"},
+ {"abc/.", "abc"},
+
+ // Remove .. elements
+ {"abc/def/ghi/../jkl", "abc/def/jkl"},
+ {"abc/def/../ghi/../jkl", "abc/jkl"},
+ {"abc/def/..", "abc"},
+ {"abc/def/../..", "."},
+ {"/abc/def/../..", "/"},
+ {"abc/def/../../..", ".."},
+ {"/abc/def/../../..", "/"},
+ {"abc/def/../../../ghi/jkl/../../../mno", "../../mno"},
+
+ // Combinations
+ {"abc/./../def", "def"},
+ {"abc//./../def", "def"},
+ {"abc/../../././../def", "../../def"},
+ }
+ for _, test := range cleantests {
+ want := test.path != test.result
+ got := pathNeedsClean([]byte(test.path))
+ if !got {
+ t.Logf("no clean: %q", test.path)
+ }
+ if want && !got {
+ t.Errorf("input: %q, want %v, got %v", test.path, want, got)
+ }
+ }
+}
diff --git a/cmd/object-handlers-common.go b/cmd/object-handlers-common.go
new file mode 100644
index 00000000..a6febc12
--- /dev/null
+++ b/cmd/object-handlers-common.go
@@ -0,0 +1,428 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/minio/minio/internal/amztime"
+ "github.com/minio/minio/internal/bucket/lifecycle"
+ "github.com/minio/minio/internal/event"
+ "github.com/minio/minio/internal/hash"
+ xhttp "github.com/minio/minio/internal/http"
+)
+
+var etagRegex = regexp.MustCompile("\"*?([^\"]*?)\"*?$")
+
+// Validates the preconditions for CopyObjectPart, returns true if CopyObjectPart
+// operation should not proceed. Preconditions supported are:
+//
+// x-amz-copy-source-if-modified-since
+// x-amz-copy-source-if-unmodified-since
+// x-amz-copy-source-if-match
+// x-amz-copy-source-if-none-match
+func checkCopyObjectPartPreconditions(ctx context.Context, w http.ResponseWriter, r *http.Request, objInfo ObjectInfo) bool {
+ return checkCopyObjectPreconditions(ctx, w, r, objInfo)
+}
+
+// Validates the preconditions for CopyObject, returns true if CopyObject operation should not proceed.
+// Preconditions supported are:
+//
+// x-amz-copy-source-if-modified-since
+// x-amz-copy-source-if-unmodified-since
+// x-amz-copy-source-if-match
+// x-amz-copy-source-if-none-match
+func checkCopyObjectPreconditions(ctx context.Context, w http.ResponseWriter, r *http.Request, objInfo ObjectInfo) bool {
+ // Return false for methods other than GET and HEAD.
+ if r.Method != http.MethodPut {
+ return false
+ }
+ // If the object doesn't have a modtime (IsZero), or the modtime
+ // is obviously garbage (Unix time == 0), then ignore modtimes
+ // and don't process the If-Modified-Since header.
+ if objInfo.ModTime.IsZero() || objInfo.ModTime.Equal(time.Unix(0, 0)) {
+ return false
+ }
+
+ // Headers to be set of object content is not going to be written to the client.
+ writeHeaders := func() {
+ // set common headers
+ setCommonHeaders(w)
+
+ // set object-related metadata headers
+ w.Header().Set(xhttp.LastModified, objInfo.ModTime.UTC().Format(http.TimeFormat))
+
+ if objInfo.ETag != "" {
+ w.Header()[xhttp.ETag] = []string{"\"" + objInfo.ETag + "\""}
+ }
+ }
+ // x-amz-copy-source-if-modified-since: Return the object only if it has been modified
+ // since the specified time otherwise return 412 (precondition failed).
+ ifModifiedSinceHeader := r.Header.Get(xhttp.AmzCopySourceIfModifiedSince)
+ if ifModifiedSinceHeader != "" {
+ if givenTime, err := amztime.ParseHeader(ifModifiedSinceHeader); err == nil {
+ if !ifModifiedSince(objInfo.ModTime, givenTime) {
+ // If the object is not modified since the specified time.
+ writeHeaders()
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrPreconditionFailed), r.URL)
+ return true
+ }
+ }
+ }
+
+ // x-amz-copy-source-if-unmodified-since : Return the object only if it has not been
+ // modified since the specified time, otherwise return a 412 (precondition failed).
+ ifUnmodifiedSinceHeader := r.Header.Get(xhttp.AmzCopySourceIfUnmodifiedSince)
+ if ifUnmodifiedSinceHeader != "" {
+ if givenTime, err := amztime.ParseHeader(ifUnmodifiedSinceHeader); err == nil {
+ if ifModifiedSince(objInfo.ModTime, givenTime) {
+ // If the object is modified since the specified time.
+ writeHeaders()
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrPreconditionFailed), r.URL)
+ return true
+ }
+ }
+ }
+
+ // x-amz-copy-source-if-match : Return the object only if its entity tag (ETag) is the
+ // same as the one specified; otherwise return a 412 (precondition failed).
+ ifMatchETagHeader := r.Header.Get(xhttp.AmzCopySourceIfMatch)
+ if ifMatchETagHeader != "" {
+ if !isETagEqual(objInfo.ETag, ifMatchETagHeader) {
+ // If the object ETag does not match with the specified ETag.
+ writeHeaders()
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrPreconditionFailed), r.URL)
+ return true
+ }
+ }
+
+ // If-None-Match : Return the object only if its entity tag (ETag) is different from the
+ // one specified otherwise, return a 304 (not modified).
+ ifNoneMatchETagHeader := r.Header.Get(xhttp.AmzCopySourceIfNoneMatch)
+ if ifNoneMatchETagHeader != "" {
+ if isETagEqual(objInfo.ETag, ifNoneMatchETagHeader) {
+ // If the object ETag matches with the specified ETag.
+ writeHeaders()
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrPreconditionFailed), r.URL)
+ return true
+ }
+ }
+ // Object content should be written to http.ResponseWriter
+ return false
+}
+
+// Validates the preconditions. Returns true if PUT operation should not proceed.
+// Preconditions supported are:
+//
+// x-minio-source-mtime
+// x-minio-source-etag
+func checkPreconditionsPUT(ctx context.Context, w http.ResponseWriter, r *http.Request, objInfo ObjectInfo, opts ObjectOptions) bool {
+ // Return false for methods other than PUT.
+ if r.Method != http.MethodPut && r.Method != http.MethodPost {
+ return false
+ }
+ // If the object doesn't have a modtime (IsZero), or the modtime
+ // is obviously garbage (Unix time == 0), then ignore modtimes
+ // and don't process the If-Modified-Since header.
+ if objInfo.ModTime.IsZero() || objInfo.ModTime.Equal(time.Unix(0, 0)) {
+ return false
+ }
+
+ // If top level is a delete marker proceed to upload.
+ if objInfo.DeleteMarker {
+ return false
+ }
+
+ // Headers to be set of object content is not going to be written to the client.
+ writeHeaders := func() {
+ // set common headers
+ setCommonHeaders(w)
+
+ // set object-related metadata headers
+ w.Header().Set(xhttp.LastModified, objInfo.ModTime.UTC().Format(http.TimeFormat))
+
+ if objInfo.ETag != "" {
+ w.Header()[xhttp.ETag] = []string{"\"" + objInfo.ETag + "\""}
+ }
+ }
+
+ // If-Match : Return the object only if its entity tag (ETag) is the same as the one specified;
+ // otherwise return a 412 (precondition failed).
+ ifMatchETagHeader := r.Header.Get(xhttp.IfMatch)
+ if ifMatchETagHeader != "" {
+ if !isETagEqual(objInfo.ETag, ifMatchETagHeader) {
+ // If the object ETag does not match with the specified ETag.
+ writeHeaders()
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrPreconditionFailed), r.URL)
+ return true
+ }
+ }
+
+ // If-None-Match : Return the object only if its entity tag (ETag) is different from the
+ // one specified otherwise, return a 304 (not modified).
+ ifNoneMatchETagHeader := r.Header.Get(xhttp.IfNoneMatch)
+ if ifNoneMatchETagHeader != "" {
+ if isETagEqual(objInfo.ETag, ifNoneMatchETagHeader) {
+ // If the object ETag matches with the specified ETag.
+ writeHeaders()
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrPreconditionFailed), r.URL)
+ return true
+ }
+ }
+
+ etagMatch := opts.PreserveETag != "" && isETagEqual(objInfo.ETag, opts.PreserveETag)
+ vidMatch := opts.VersionID != "" && opts.VersionID == objInfo.VersionID
+ if etagMatch && vidMatch {
+ writeHeaders()
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrPreconditionFailed), r.URL)
+ return true
+ }
+
+ // Object content should be persisted.
+ return false
+}
+
+// Headers to be set of object content is not going to be written to the client.
+func writeHeadersPrecondition(w http.ResponseWriter, objInfo ObjectInfo) {
+ // set common headers
+ setCommonHeaders(w)
+
+ // set object-related metadata headers
+ w.Header().Set(xhttp.LastModified, objInfo.ModTime.UTC().Format(http.TimeFormat))
+
+ if objInfo.ETag != "" {
+ w.Header()[xhttp.ETag] = []string{"\"" + objInfo.ETag + "\""}
+ }
+
+ if objInfo.VersionID != "" {
+ w.Header()[xhttp.AmzVersionID] = []string{objInfo.VersionID}
+ }
+
+ if !objInfo.Expires.IsZero() {
+ w.Header().Set(xhttp.Expires, objInfo.Expires.UTC().Format(http.TimeFormat))
+ }
+
+ if objInfo.CacheControl != "" {
+ w.Header().Set(xhttp.CacheControl, objInfo.CacheControl)
+ }
+}
+
+// Validates the preconditions. Returns true if GET/HEAD operation should not proceed.
+// Preconditions supported are:
+//
+// If-Modified-Since
+// If-Unmodified-Since
+// If-Match
+// If-None-Match
+func checkPreconditions(ctx context.Context, w http.ResponseWriter, r *http.Request, objInfo ObjectInfo, opts ObjectOptions) bool {
+ // Return false for methods other than GET and HEAD.
+ if r.Method != http.MethodGet && r.Method != http.MethodHead {
+ return false
+ }
+ // If the object doesn't have a modtime (IsZero), or the modtime
+ // is obviously garbage (Unix time == 0), then ignore modtimes
+ // and don't process the If-Modified-Since header.
+ if objInfo.ModTime.IsZero() || objInfo.ModTime.Equal(time.Unix(0, 0)) {
+ return false
+ }
+
+ // Check if the part number is correct.
+ if opts.PartNumber > 1 {
+ partFound := false
+ for _, pi := range objInfo.Parts {
+ if pi.Number == opts.PartNumber {
+ partFound = true
+ break
+ }
+ }
+ if !partFound {
+ // According to S3 we don't need to set any object information here.
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidPartNumber), r.URL)
+ return true
+ }
+ }
+
+ // If-None-Match : Return the object only if its entity tag (ETag) is different from the
+ // one specified otherwise, return a 304 (not modified).
+ ifNoneMatchETagHeader := r.Header.Get(xhttp.IfNoneMatch)
+ if ifNoneMatchETagHeader != "" {
+ if isETagEqual(objInfo.ETag, ifNoneMatchETagHeader) {
+ // Do not care If-Modified-Since, Because:
+ // 1. If If-Modified-Since condition evaluates to true.
+ // If both of the If-None-Match and If-Modified-Since headers are present in the request as follows:
+ // If-None-Match condition evaluates to false , and;
+ // If-Modified-Since condition evaluates to true ;
+ // Then Amazon S3 returns the 304 Not Modified response code.
+ // 2. If If-Modified-Since condition evaluates to false, The following `ifModifiedSinceHeader` judgment will also return 304
+
+ // If the object ETag matches with the specified ETag.
+ writeHeadersPrecondition(w, objInfo)
+ w.WriteHeader(http.StatusNotModified)
+ return true
+ }
+ }
+
+ // If-Modified-Since : Return the object only if it has been modified since the specified time,
+ // otherwise return a 304 (not modified).
+ ifModifiedSinceHeader := r.Header.Get(xhttp.IfModifiedSince)
+ if ifModifiedSinceHeader != "" {
+ if givenTime, err := amztime.ParseHeader(ifModifiedSinceHeader); err == nil {
+ if !ifModifiedSince(objInfo.ModTime, givenTime) {
+ // If the object is not modified since the specified time.
+ writeHeadersPrecondition(w, objInfo)
+ w.WriteHeader(http.StatusNotModified)
+ return true
+ }
+ }
+ }
+
+ // If-Match : Return the object only if its entity tag (ETag) is the same as the one specified;
+ // otherwise return a 412 (precondition failed).
+ ifMatchETagHeader := r.Header.Get(xhttp.IfMatch)
+ if ifMatchETagHeader != "" {
+ if !isETagEqual(objInfo.ETag, ifMatchETagHeader) {
+ // If the object ETag does not match with the specified ETag.
+ writeHeadersPrecondition(w, objInfo)
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrPreconditionFailed), r.URL)
+ return true
+ }
+ }
+
+ // If-Unmodified-Since : Return the object only if it has not been modified since the specified
+ // time, otherwise return a 412 (precondition failed).
+ ifUnmodifiedSinceHeader := r.Header.Get(xhttp.IfUnmodifiedSince)
+ if ifUnmodifiedSinceHeader != "" && ifMatchETagHeader == "" {
+ if givenTime, err := amztime.ParseHeader(ifUnmodifiedSinceHeader); err == nil {
+ if ifModifiedSince(objInfo.ModTime, givenTime) {
+ // If the object is modified since the specified time.
+ writeHeadersPrecondition(w, objInfo)
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrPreconditionFailed), r.URL)
+ return true
+ }
+ }
+ }
+
+ // Object content should be written to http.ResponseWriter
+ return false
+}
+
+// returns true if object was modified after givenTime.
+func ifModifiedSince(objTime time.Time, givenTime time.Time) bool {
+ // The Date-Modified header truncates sub-second precision, so
+ // use mtime < t+1s instead of mtime <= t to check for unmodified.
+ return !objTime.Before(givenTime.Add(1 * time.Second))
+}
+
+// canonicalizeETag returns ETag with leading and trailing double-quotes removed,
+// if any present
+func canonicalizeETag(etag string) string {
+ return etagRegex.ReplaceAllString(etag, "$1")
+}
+
+// isETagEqual return true if the canonical representations of two ETag strings
+// are equal, false otherwise
+func isETagEqual(left, right string) bool {
+ if strings.TrimSpace(right) == "*" {
+ return true
+ }
+ return canonicalizeETag(left) == canonicalizeETag(right)
+}
+
+// setPutObjHeaders sets all the necessary headers returned back
+// upon a success Put/Copy/CompleteMultipart/Delete requests
+// to activate delete only headers set delete as true
+func setPutObjHeaders(w http.ResponseWriter, objInfo ObjectInfo, del bool, h http.Header) {
+ // We must not use the http.Header().Set method here because some (broken)
+ // clients expect the ETag header key to be literally "ETag" - not "Etag" (case-sensitive).
+ // Therefore, we have to set the ETag directly as map entry.
+ if objInfo.ETag != "" && !del {
+ w.Header()[xhttp.ETag] = []string{`"` + objInfo.ETag + `"`}
+ }
+
+ // Set the relevant version ID as part of the response header.
+ if objInfo.VersionID != "" && objInfo.VersionID != nullVersionID {
+ w.Header()[xhttp.AmzVersionID] = []string{objInfo.VersionID}
+ // If version is a deleted marker, set this header as well
+ if objInfo.DeleteMarker && del { // only returned during delete object
+ w.Header()[xhttp.AmzDeleteMarker] = []string{strconv.FormatBool(objInfo.DeleteMarker)}
+ }
+ }
+
+ if objInfo.Bucket != "" && objInfo.Name != "" {
+ if lc, err := globalLifecycleSys.Get(objInfo.Bucket); err == nil && !del {
+ lc.SetPredictionHeaders(w, objInfo.ToLifecycleOpts())
+ }
+ }
+ cs, _ := objInfo.decryptChecksums(0, h)
+ hash.AddChecksumHeader(w, cs)
+}
+
+func deleteObjectVersions(ctx context.Context, o ObjectLayer, bucket string, toDel []ObjectToDelete, lcEvent []lifecycle.Event) {
+ for remaining := toDel; len(remaining) > 0; toDel = remaining {
+ if len(toDel) > maxDeleteList {
+ remaining = toDel[maxDeleteList:]
+ toDel = toDel[:maxDeleteList]
+ } else {
+ remaining = nil
+ }
+ vc, _ := globalBucketVersioningSys.Get(bucket)
+ deletedObjs, errs := o.DeleteObjects(ctx, bucket, toDel, ObjectOptions{
+ PrefixEnabledFn: vc.PrefixEnabled,
+ VersionSuspended: vc.Suspended(),
+ })
+
+ for i, dobj := range deletedObjs {
+ oi := ObjectInfo{
+ Bucket: bucket,
+ Name: dobj.ObjectName,
+ VersionID: dobj.VersionID,
+ }
+ traceFn := globalLifecycleSys.trace(oi)
+ tags := newLifecycleAuditEvent(lcEventSrc_Scanner, lcEvent[i]).Tags()
+
+ // Send audit for the lifecycle delete operation
+ auditLogLifecycle(
+ ctx,
+ oi,
+ ILMExpiry, tags, traceFn)
+
+ evArgs := eventArgs{
+ EventName: event.ObjectRemovedDelete,
+ BucketName: bucket,
+ Object: ObjectInfo{
+ Name: dobj.ObjectName,
+ VersionID: dobj.VersionID,
+ },
+ UserAgent: "Internal: [ILM-Expiry]",
+ Host: globalLocalNodeName,
+ }
+ if errs[i] != nil {
+ evArgs.RespElements = map[string]string{
+ "error": fmt.Sprintf("failed to delete %s(%s), with error %v", dobj.ObjectName, dobj.VersionID, errs[i]),
+ }
+ }
+ sendEvent(evArgs)
+ }
+ }
+}
diff --git a/cmd/object-handlers-common_test.go b/cmd/object-handlers-common_test.go
new file mode 100644
index 00000000..80555a1a
--- /dev/null
+++ b/cmd/object-handlers-common_test.go
@@ -0,0 +1,181 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ xhttp "github.com/minio/minio/internal/http"
+)
+
+// Tests - canonicalizeETag()
+func TestCanonicalizeETag(t *testing.T) {
+ testCases := []struct {
+ etag string
+ canonicalizedETag string
+ }{
+ {
+ etag: "\"\"\"",
+ canonicalizedETag: "",
+ },
+ {
+ etag: "\"\"\"abc\"",
+ canonicalizedETag: "abc",
+ },
+ {
+ etag: "abcd",
+ canonicalizedETag: "abcd",
+ },
+ {
+ etag: "abcd\"\"",
+ canonicalizedETag: "abcd",
+ },
+ }
+ for _, test := range testCases {
+ etag := canonicalizeETag(test.etag)
+ if test.canonicalizedETag != etag {
+ t.Fatalf("Expected %s , got %s", test.canonicalizedETag, etag)
+ }
+ }
+}
+
+// Tests - CheckPreconditions()
+func TestCheckPreconditions(t *testing.T) {
+ objModTime := time.Date(2024, time.August, 26, 0o2, 0o1, 0o1, 0, time.UTC)
+ objInfo := ObjectInfo{ETag: "aa", ModTime: objModTime}
+ testCases := []struct {
+ name string
+ ifMatch string
+ ifNoneMatch string
+ ifModifiedSince string
+ ifUnmodifiedSince string
+ objInfo ObjectInfo
+ expectedFlag bool
+ expectedCode int
+ }{
+ // If-None-Match(false) and If-Modified-Since(true)
+ {
+ name: "If-None-Match1",
+ ifNoneMatch: "aa",
+ ifModifiedSince: "Sun, 26 Aug 2024 02:01:00 GMT",
+ objInfo: objInfo,
+ expectedFlag: true,
+ expectedCode: 304,
+ },
+ // If-Modified-Since(false)
+ {
+ name: "If-None-Match2",
+ ifNoneMatch: "aaa",
+ ifModifiedSince: "Sun, 26 Aug 2024 02:01:01 GMT",
+ objInfo: objInfo,
+ expectedFlag: true,
+ expectedCode: 304,
+ },
+ {
+ name: "If-None-Match3",
+ ifNoneMatch: "aaa",
+ ifModifiedSince: "Sun, 26 Aug 2024 02:01:02 GMT",
+ objInfo: objInfo,
+ expectedFlag: true,
+ expectedCode: 304,
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ recorder := httptest.NewRecorder()
+ request := httptest.NewRequest(http.MethodHead, "/bucket/a", bytes.NewReader([]byte{}))
+ request.Header.Set(xhttp.IfNoneMatch, tc.ifNoneMatch)
+ request.Header.Set(xhttp.IfModifiedSince, tc.ifModifiedSince)
+ request.Header.Set(xhttp.IfMatch, tc.ifMatch)
+ request.Header.Set(xhttp.IfUnmodifiedSince, tc.ifUnmodifiedSince)
+ actualFlag := checkPreconditions(t.Context(), recorder, request, tc.objInfo, ObjectOptions{})
+ if tc.expectedFlag != actualFlag {
+ t.Errorf("test: %s, got flag: %v, want: %v", tc.name, actualFlag, tc.expectedFlag)
+ }
+ if tc.expectedCode != recorder.Code {
+ t.Errorf("test: %s, got code: %d, want: %d", tc.name, recorder.Code, tc.expectedCode)
+ }
+ })
+ }
+ testCases = []struct {
+ name string
+ ifMatch string
+ ifNoneMatch string
+ ifModifiedSince string
+ ifUnmodifiedSince string
+ objInfo ObjectInfo
+ expectedFlag bool
+ expectedCode int
+ }{
+ // If-Match(true) and If-Unmodified-Since(false)
+ {
+ name: "If-Match1",
+ ifMatch: "aa",
+ ifUnmodifiedSince: "Sun, 26 Aug 2024 02:01:00 GMT",
+ objInfo: objInfo,
+ expectedFlag: false,
+ expectedCode: 200,
+ },
+ // If-Unmodified-Since(true)
+ {
+ name: "If-Match2",
+ ifMatch: "aa",
+ ifUnmodifiedSince: "Sun, 26 Aug 2024 02:01:01 GMT",
+ objInfo: objInfo,
+ expectedFlag: false,
+ expectedCode: 200,
+ },
+ {
+ name: "If-Match3",
+ ifMatch: "aa",
+ ifUnmodifiedSince: "Sun, 26 Aug 2024 02:01:02 GMT",
+ objInfo: objInfo,
+ expectedFlag: false,
+ expectedCode: 200,
+ },
+ // If-Match(true)
+ {
+ name: "If-Match4",
+ ifMatch: "aa",
+ objInfo: objInfo,
+ expectedFlag: false,
+ expectedCode: 200,
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ recorder := httptest.NewRecorder()
+ request := httptest.NewRequest(http.MethodHead, "/bucket/a", bytes.NewReader([]byte{}))
+ request.Header.Set(xhttp.IfNoneMatch, tc.ifNoneMatch)
+ request.Header.Set(xhttp.IfModifiedSince, tc.ifModifiedSince)
+ request.Header.Set(xhttp.IfMatch, tc.ifMatch)
+ request.Header.Set(xhttp.IfUnmodifiedSince, tc.ifUnmodifiedSince)
+ actualFlag := checkPreconditions(t.Context(), recorder, request, tc.objInfo, ObjectOptions{})
+ if tc.expectedFlag != actualFlag {
+ t.Errorf("test: %s, got flag: %v, want: %v", tc.name, actualFlag, tc.expectedFlag)
+ }
+ if tc.expectedCode != recorder.Code {
+ t.Errorf("test: %s, got code: %d, want: %d", tc.name, recorder.Code, tc.expectedCode)
+ }
+ })
+ }
+}
diff --git a/cmd/object-handlers.go b/cmd/object-handlers.go
new file mode 100644
index 00000000..6542c465
--- /dev/null
+++ b/cmd/object-handlers.go
@@ -0,0 +1,3585 @@
+// Copyright (c) 2015-2023 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "archive/tar"
+ "context"
+ "encoding/hex"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "io"
+ "maps"
+ "net/http"
+ "net/http/httptest"
+ "net/textproto"
+ "net/url"
+ "os"
+ "sort"
+ "strconv"
+ "strings"
+ "sync/atomic"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/klauspost/compress/gzhttp"
+ miniogo "github.com/minio/minio-go/v7"
+ "github.com/minio/minio-go/v7/pkg/credentials"
+ "github.com/minio/minio-go/v7/pkg/encrypt"
+ "github.com/minio/minio-go/v7/pkg/tags"
+ "github.com/minio/minio/internal/amztime"
+ "github.com/minio/minio/internal/auth"
+ sse "github.com/minio/minio/internal/bucket/encryption"
+ "github.com/minio/minio/internal/bucket/lifecycle"
+ objectlock "github.com/minio/minio/internal/bucket/object/lock"
+ "github.com/minio/minio/internal/bucket/replication"
+ "github.com/minio/minio/internal/config/dns"
+ "github.com/minio/minio/internal/config/storageclass"
+ "github.com/minio/minio/internal/crypto"
+ "github.com/minio/minio/internal/etag"
+ "github.com/minio/minio/internal/event"
+ "github.com/minio/minio/internal/handlers"
+ "github.com/minio/minio/internal/hash"
+ xhttp "github.com/minio/minio/internal/http"
+ xioutil "github.com/minio/minio/internal/ioutil"
+ "github.com/minio/minio/internal/kms"
+ "github.com/minio/minio/internal/logger"
+ "github.com/minio/minio/internal/s3select"
+ "github.com/minio/mux"
+ "github.com/minio/pkg/v3/policy"
+)
+
+// supportedHeadGetReqParams - supported request parameters for GET and HEAD presigned request.
+var supportedHeadGetReqParams = map[string]string{
+ "response-expires": xhttp.Expires,
+ "response-content-type": xhttp.ContentType,
+ "response-cache-control": xhttp.CacheControl,
+ "response-content-encoding": xhttp.ContentEncoding,
+ "response-content-language": xhttp.ContentLanguage,
+ "response-content-disposition": xhttp.ContentDisposition,
+}
+
+const (
+ compressionAlgorithmV1 = "golang/snappy/LZ77"
+ compressionAlgorithmV2 = "klauspost/compress/s2"
+
+ // When an upload exceeds encryptBufferThreshold ...
+ encryptBufferThreshold = 1 << 20
+ // add an input buffer of this size.
+ encryptBufferSize = 1 << 20
+
+ // minCompressibleSize is the minimum size at which we enable compression.
+ minCompressibleSize = 4096
+)
+
+// setHeadGetRespHeaders - set any requested parameters as response headers.
+func setHeadGetRespHeaders(w http.ResponseWriter, reqParams url.Values) {
+ for k, v := range reqParams {
+ if header, ok := supportedHeadGetReqParams[strings.ToLower(k)]; ok {
+ w.Header()[header] = []string{strings.Join(v, ",")}
+ }
+ }
+}
+
+// SelectObjectContentHandler - GET Object?select
+// ----------
+// This implementation of the GET operation retrieves object content based
+// on an SQL expression. In the request, along with the sql expression, you must
+// also specify a data serialization format (JSON, CSV) of the object.
+func (api objectAPIHandlers) SelectObjectContentHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "SelectObject")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ // Fetch object stat info.
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ if crypto.S3.IsRequested(r.Header) || crypto.S3KMS.IsRequested(r.Header) { // If SSE-S3 or SSE-KMS present -> AWS fails with undefined error
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+ object, err := unescapePath(vars["object"])
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ opts, err := getOpts(ctx, r, bucket, object)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ getObjectInfo := objectAPI.GetObjectInfo
+
+ // Check for auth type to return S3 compatible error.
+ // type to return the correct error (NoSuchKey vs AccessDenied)
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetObjectAction, bucket, object); s3Error != ErrNone {
+ if getRequestAuthType(r) == authTypeAnonymous {
+ // As per "Permission" section in
+ // https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectGET.html
+ // If the object you request does not exist,
+ // the error Amazon S3 returns depends on
+ // whether you also have the s3:ListBucket
+ // permission.
+ // * If you have the s3:ListBucket permission
+ // on the bucket, Amazon S3 will return an
+ // HTTP status code 404 ("no such key")
+ // error.
+ // * if you don’t have the s3:ListBucket
+ // permission, Amazon S3 will return an HTTP
+ // status code 403 ("access denied") error.`
+ if globalPolicySys.IsAllowed(policy.BucketPolicyArgs{
+ Action: policy.ListBucketAction,
+ BucketName: bucket,
+ ConditionValues: getConditionValues(r, "", auth.AnonymousCredentials),
+ IsOwner: false,
+ }) {
+ _, err = getObjectInfo(ctx, bucket, object, opts)
+ if toAPIError(ctx, err).Code == "NoSuchKey" {
+ s3Error = ErrNoSuchKey
+ }
+ }
+ }
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Get request range.
+ rangeHeader := r.Header.Get(xhttp.Range)
+ if rangeHeader != "" {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrUnsupportedRangeHeader), r.URL)
+ return
+ }
+
+ if r.ContentLength <= 0 {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrEmptyRequestBody), r.URL)
+ return
+ }
+
+ // Take read lock on object, here so subsequent lower-level
+ // calls do not need to.
+ lock := objectAPI.NewNSLock(bucket, object)
+ lkctx, err := lock.GetRLock(ctx, globalOperationTimeout)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ ctx = lkctx.Context()
+ defer lock.RUnlock(lkctx)
+
+ getObjectNInfo := objectAPI.GetObjectNInfo
+
+ gopts := opts
+ gopts.NoLock = true // We already have a lock, we can live with it.
+ objInfo, err := getObjectInfo(ctx, bucket, object, gopts)
+ if err != nil {
+ // Versioning enabled quite possibly object is deleted might be delete-marker
+ // if present set the headers, no idea why AWS S3 sets these headers.
+ if objInfo.VersionID != "" && objInfo.DeleteMarker {
+ w.Header()[xhttp.AmzVersionID] = []string{objInfo.VersionID}
+ w.Header()[xhttp.AmzDeleteMarker] = []string{strconv.FormatBool(objInfo.DeleteMarker)}
+ }
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // filter object lock metadata if permission does not permit
+ getRetPerms := checkRequestAuthType(ctx, r, policy.GetObjectRetentionAction, bucket, object)
+ legalHoldPerms := checkRequestAuthType(ctx, r, policy.GetObjectLegalHoldAction, bucket, object)
+
+ // filter object lock metadata if permission does not permit
+ objInfo.UserDefined = objectlock.FilterObjectLockMetadata(objInfo.UserDefined, getRetPerms != ErrNone, legalHoldPerms != ErrNone)
+
+ if _, err = DecryptObjectInfo(&objInfo, r); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ actualSize, err := objInfo.GetActualSize()
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ objectRSC := s3select.NewObjectReadSeekCloser(
+ func(offset int64) (io.ReadCloser, error) {
+ rs := &HTTPRangeSpec{
+ IsSuffixLength: false,
+ Start: offset,
+ End: -1,
+ }
+ opts.NoLock = true
+ return getObjectNInfo(ctx, bucket, object, rs, r.Header, opts)
+ },
+ actualSize,
+ )
+ defer objectRSC.Close()
+ s3Select, err := s3select.NewS3Select(r.Body)
+ if err != nil {
+ if serr, ok := err.(s3select.SelectError); ok {
+ encodedErrorResponse := encodeResponse(APIErrorResponse{
+ Code: serr.ErrorCode(),
+ Message: serr.ErrorMessage(),
+ BucketName: bucket,
+ Key: object,
+ Resource: r.URL.Path,
+ RequestID: w.Header().Get(xhttp.AmzRequestID),
+ HostID: globalDeploymentID(),
+ })
+ writeResponse(w, serr.HTTPStatusCode(), encodedErrorResponse, mimeXML)
+ } else {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ }
+ return
+ }
+ defer s3Select.Close()
+
+ if err = s3Select.Open(objectRSC); err != nil {
+ if serr, ok := err.(s3select.SelectError); ok {
+ encodedErrorResponse := encodeResponse(APIErrorResponse{
+ Code: serr.ErrorCode(),
+ Message: serr.ErrorMessage(),
+ BucketName: bucket,
+ Key: object,
+ Resource: r.URL.Path,
+ RequestID: w.Header().Get(xhttp.AmzRequestID),
+ HostID: globalDeploymentID(),
+ })
+ writeResponse(w, serr.HTTPStatusCode(), encodedErrorResponse, mimeXML)
+ } else {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ }
+ return
+ }
+
+ // Set encryption response headers
+ switch kind, _ := crypto.IsEncrypted(objInfo.UserDefined); kind {
+ case crypto.S3:
+ w.Header().Set(xhttp.AmzServerSideEncryption, xhttp.AmzEncryptionAES)
+ case crypto.S3KMS:
+ w.Header().Set(xhttp.AmzServerSideEncryption, xhttp.AmzEncryptionKMS)
+ w.Header().Set(xhttp.AmzServerSideEncryptionKmsID, objInfo.KMSKeyID())
+ if kmsCtx, ok := objInfo.UserDefined[crypto.MetaContext]; ok {
+ w.Header().Set(xhttp.AmzServerSideEncryptionKmsContext, kmsCtx)
+ }
+ case crypto.SSEC:
+ // Validate the SSE-C Key set in the header.
+ if _, err = crypto.SSEC.UnsealObjectKey(r.Header, objInfo.UserDefined, bucket, object); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ w.Header().Set(xhttp.AmzServerSideEncryptionCustomerAlgorithm, r.Header.Get(xhttp.AmzServerSideEncryptionCustomerAlgorithm))
+ w.Header().Set(xhttp.AmzServerSideEncryptionCustomerKeyMD5, r.Header.Get(xhttp.AmzServerSideEncryptionCustomerKeyMD5))
+ }
+
+ s3Select.Evaluate(w)
+
+ // Notify object accessed via a GET request.
+ sendEvent(eventArgs{
+ EventName: event.ObjectAccessedGet,
+ BucketName: bucket,
+ Object: objInfo,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+}
+
+func (api objectAPIHandlers) getObjectHandler(ctx context.Context, objectAPI ObjectLayer, bucket, object string, w http.ResponseWriter, r *http.Request) {
+ if crypto.S3.IsRequested(r.Header) || crypto.S3KMS.IsRequested(r.Header) { // If SSE-S3 or SSE-KMS present -> AWS fails with undefined error
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
+ return
+ }
+
+ opts, err := getOpts(ctx, r, bucket, object)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Check for auth type to return S3 compatible error.
+ // type to return the correct error (NoSuchKey vs AccessDenied)
+ if s3Error := authenticateRequest(ctx, r, policy.GetObjectAction); s3Error != ErrNone {
+ if getRequestAuthType(r) == authTypeAnonymous {
+ // As per "Permission" section in
+ // https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectGET.html
+ // If the object you request does not exist,
+ // the error Amazon S3 returns depends on
+ // whether you also have the s3:ListBucket
+ // permission.
+ // * If you have the s3:ListBucket permission
+ // on the bucket, Amazon S3 will return an
+ // HTTP status code 404 ("no such key")
+ // error.
+ // * if you don’t have the s3:ListBucket
+ // permission, Amazon S3 will return an HTTP
+ // status code 403 ("access denied") error.`
+ if globalPolicySys.IsAllowed(policy.BucketPolicyArgs{
+ Action: policy.ListBucketAction,
+ BucketName: bucket,
+ ConditionValues: getConditionValues(r, "", auth.AnonymousCredentials),
+ IsOwner: false,
+ }) {
+ getObjectInfo := objectAPI.GetObjectInfo
+
+ _, err = getObjectInfo(ctx, bucket, object, opts)
+ if toAPIError(ctx, err).Code == "NoSuchKey" {
+ s3Error = ErrNoSuchKey
+ }
+ }
+ }
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ getObjectNInfo := objectAPI.GetObjectNInfo
+
+ // Get request range.
+ var rs *HTTPRangeSpec
+ var rangeErr error
+ rangeHeader := r.Header.Get(xhttp.Range)
+ if rangeHeader != "" {
+ // Both 'Range' and 'partNumber' cannot be specified at the same time
+ if opts.PartNumber > 0 {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRangePartNumber), r.URL)
+ return
+ }
+
+ rs, rangeErr = parseRequestRangeSpec(rangeHeader)
+ // Handle only errInvalidRange. Ignore other
+ // parse error and treat it as regular Get
+ // request like Amazon S3.
+ if errors.Is(rangeErr, errInvalidRange) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRange), r.URL)
+ return
+ }
+ }
+
+ // Validate pre-conditions if any.
+ opts.CheckPrecondFn = func(oi ObjectInfo) bool {
+ if _, err := DecryptObjectInfo(&oi, r); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return true
+ }
+
+ if oi.UserTags != "" {
+ r.Header.Set(xhttp.AmzObjectTagging, oi.UserTags)
+ }
+
+ if s3Error := authorizeRequest(ctx, r, policy.GetObjectAction); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return true
+ }
+
+ return checkPreconditions(ctx, w, r, oi, opts)
+ }
+
+ opts.FastGetObjInfo = true
+
+ var proxy proxyResult
+ gr, err := getObjectNInfo(ctx, bucket, object, rs, r.Header, opts)
+ if err != nil {
+ var (
+ reader *GetObjectReader
+ perr error
+ )
+
+ if (isErrObjectNotFound(err) || isErrVersionNotFound(err) || isErrReadQuorum(err)) && (gr == nil || !gr.ObjInfo.DeleteMarker) {
+ proxytgts := getProxyTargets(ctx, bucket, object, opts)
+ if !proxytgts.Empty() {
+ globalReplicationStats.Load().incProxy(bucket, getObjectAPI, false)
+ // proxy to replication target if active-active replication is in place.
+ reader, proxy, perr = proxyGetToReplicationTarget(ctx, bucket, object, rs, r.Header, opts, proxytgts)
+ if perr != nil {
+ globalReplicationStats.Load().incProxy(bucket, getObjectAPI, true)
+ proxyGetErr := ErrorRespToObjectError(perr, bucket, object)
+ if !isErrBucketNotFound(proxyGetErr) && !isErrObjectNotFound(proxyGetErr) && !isErrVersionNotFound(proxyGetErr) &&
+ !isErrPreconditionFailed(proxyGetErr) && !isErrInvalidRange(proxyGetErr) {
+ replLogIf(ctx, fmt.Errorf("Proxying request (replication) failed for %s/%s(%s) - %w", bucket, object, opts.VersionID, perr))
+ }
+ }
+ if reader != nil && proxy.Proxy && perr == nil {
+ gr = reader
+ }
+ }
+ }
+ if reader == nil || !proxy.Proxy {
+ // validate if the request indeed was authorized, if it wasn't we need to return "ErrAccessDenied"
+ // instead of any namespace related error.
+ if s3Error := authorizeRequest(ctx, r, policy.GetObjectAction); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+ if isErrPreconditionFailed(err) {
+ return
+ }
+ if proxy.Err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, proxy.Err), r.URL)
+ return
+ }
+ if gr != nil {
+ if !gr.ObjInfo.VersionPurgeStatus.Empty() {
+ // Shows the replication status of a permanent delete of a version
+ w.Header()[xhttp.MinIODeleteReplicationStatus] = []string{string(gr.ObjInfo.VersionPurgeStatus)}
+ }
+ if !gr.ObjInfo.ReplicationStatus.Empty() && gr.ObjInfo.DeleteMarker {
+ w.Header()[xhttp.MinIODeleteMarkerReplicationStatus] = []string{string(gr.ObjInfo.ReplicationStatus)}
+ }
+
+ // Versioning enabled quite possibly object is deleted might be delete-marker
+ // if present set the headers, no idea why AWS S3 sets these headers.
+ if gr.ObjInfo.VersionID != "" && gr.ObjInfo.DeleteMarker {
+ w.Header()[xhttp.AmzVersionID] = []string{gr.ObjInfo.VersionID}
+ w.Header()[xhttp.AmzDeleteMarker] = []string{strconv.FormatBool(gr.ObjInfo.DeleteMarker)}
+ }
+ QueueReplicationHeal(ctx, bucket, gr.ObjInfo, 0)
+ }
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ }
+ defer gr.Close()
+
+ objInfo := gr.ObjInfo
+
+ if !proxy.Proxy { // apply lifecycle rules only for local requests
+ // Automatically remove the object/version if an expiry lifecycle rule can be applied
+ if lc, err := globalLifecycleSys.Get(bucket); err == nil {
+ rcfg, err := globalBucketObjectLockSys.Get(bucket)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ replcfg, err := getReplicationConfig(ctx, bucket)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ event := evalActionFromLifecycle(ctx, *lc, rcfg, replcfg, objInfo)
+ if event.Action.Delete() {
+ // apply whatever the expiry rule is.
+ applyExpiryRule(event, lcEventSrc_s3GetObject, objInfo)
+ if !event.Action.DeleteRestored() {
+ // If the ILM action is not on restored object return error.
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNoSuchKey), r.URL)
+ return
+ }
+ }
+ }
+
+ QueueReplicationHeal(ctx, bucket, gr.ObjInfo, 0)
+ }
+
+ // filter object lock metadata if permission does not permit
+ getRetPerms := checkRequestAuthType(ctx, r, policy.GetObjectRetentionAction, bucket, object)
+ legalHoldPerms := checkRequestAuthType(ctx, r, policy.GetObjectLegalHoldAction, bucket, object)
+
+ // filter object lock metadata if permission does not permit
+ objInfo.UserDefined = objectlock.FilterObjectLockMetadata(objInfo.UserDefined, getRetPerms != ErrNone, legalHoldPerms != ErrNone)
+
+ // Set encryption response headers
+ if kind, isEncrypted := crypto.IsEncrypted(objInfo.UserDefined); isEncrypted {
+ switch kind {
+ case crypto.S3:
+ w.Header().Set(xhttp.AmzServerSideEncryption, xhttp.AmzEncryptionAES)
+ case crypto.S3KMS:
+ w.Header().Set(xhttp.AmzServerSideEncryption, xhttp.AmzEncryptionKMS)
+ w.Header().Set(xhttp.AmzServerSideEncryptionKmsID, objInfo.KMSKeyID())
+ if kmsCtx, ok := objInfo.UserDefined[crypto.MetaContext]; ok {
+ w.Header().Set(xhttp.AmzServerSideEncryptionKmsContext, kmsCtx)
+ }
+ case crypto.SSEC:
+ w.Header().Set(xhttp.AmzServerSideEncryptionCustomerAlgorithm, r.Header.Get(xhttp.AmzServerSideEncryptionCustomerAlgorithm))
+ w.Header().Set(xhttp.AmzServerSideEncryptionCustomerKeyMD5, r.Header.Get(xhttp.AmzServerSideEncryptionCustomerKeyMD5))
+ }
+ objInfo.ETag = getDecryptedETag(r.Header, objInfo, false)
+ }
+
+ if r.Header.Get(xhttp.AmzChecksumMode) == "ENABLED" && rs == nil {
+ // AWS S3 silently drops checksums on range requests.
+ cs, _ := objInfo.decryptChecksums(opts.PartNumber, r.Header)
+ hash.AddChecksumHeader(w, cs)
+ }
+
+ if err = setObjectHeaders(ctx, w, objInfo, rs, opts); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Set Parts Count Header
+ if opts.PartNumber > 0 && len(objInfo.Parts) > 0 {
+ setPartsCountHeaders(w, objInfo)
+ }
+
+ setHeadGetRespHeaders(w, r.Form)
+
+ var iw io.Writer = w
+
+ statusCodeWritten := false
+ httpWriter := xioutil.WriteOnClose(iw)
+ if rs != nil || opts.PartNumber > 0 {
+ statusCodeWritten = true
+ w.WriteHeader(http.StatusPartialContent)
+ }
+
+ // Write object content to response body
+ if _, err = xioutil.Copy(httpWriter, gr); err != nil {
+ if !httpWriter.HasWritten() && !statusCodeWritten {
+ // write error response only if no data or headers has been written to client yet
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ return
+ }
+
+ if err = httpWriter.Close(); err != nil {
+ if !httpWriter.HasWritten() && !statusCodeWritten { // write error response only if no data or headers has been written to client yet
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ return
+ }
+
+ // Notify object accessed via a GET request.
+ sendEvent(eventArgs{
+ EventName: event.ObjectAccessedGet,
+ BucketName: bucket,
+ Object: objInfo,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+}
+
+// GetObjectAttributes ...
+func (api objectAPIHandlers) getObjectAttributesHandler(ctx context.Context, objectAPI ObjectLayer, bucket, object string, w http.ResponseWriter, r *http.Request) {
+ opts, valid := getAndValidateAttributesOpts(ctx, w, r, bucket, object)
+ if !valid {
+ return
+ }
+
+ var s3Error APIErrorCode
+ if opts.VersionID != "" {
+ s3Error = checkRequestAuthType(ctx, r, policy.GetObjectVersionAttributesAction, bucket, object)
+ if s3Error == ErrNone {
+ s3Error = checkRequestAuthType(ctx, r, policy.GetObjectVersionAction, bucket, object)
+ }
+ } else {
+ s3Error = checkRequestAuthType(ctx, r, policy.GetObjectAttributesAction, bucket, object)
+ if s3Error == ErrNone {
+ s3Error = checkRequestAuthType(ctx, r, policy.GetObjectAction, bucket, object)
+ }
+ }
+
+ if s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ objInfo, err := objectAPI.GetObjectInfo(ctx, bucket, object, opts)
+ if err != nil {
+ s3Error = checkRequestAuthType(ctx, r, policy.ListBucketAction, bucket, object)
+ if s3Error == ErrNone {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
+ return
+ }
+
+ if _, err = DecryptObjectInfo(&objInfo, r); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if checkPreconditions(ctx, w, r, objInfo, opts) {
+ return
+ }
+
+ OA := new(getObjectAttributesResponse)
+
+ if opts.Versioned {
+ w.Header().Set(xhttp.AmzVersionID, objInfo.VersionID)
+ }
+
+ lastModified := objInfo.ModTime.UTC().Format(http.TimeFormat)
+ w.Header().Set(xhttp.LastModified, lastModified)
+ w.Header().Del(xhttp.ContentType)
+
+ if _, ok := opts.ObjectAttributes[xhttp.Checksum]; ok {
+ chkSums, _ := objInfo.decryptChecksums(0, r.Header)
+ // AWS does not appear to append part number on this API call.
+ if len(chkSums) > 0 {
+ OA.Checksum = &objectAttributesChecksum{
+ ChecksumCRC32: strings.Split(chkSums["CRC32"], "-")[0],
+ ChecksumCRC32C: strings.Split(chkSums["CRC32C"], "-")[0],
+ ChecksumSHA1: strings.Split(chkSums["SHA1"], "-")[0],
+ ChecksumSHA256: strings.Split(chkSums["SHA256"], "-")[0],
+ ChecksumCRC64NVME: strings.Split(chkSums["CRC64NVME"], "-")[0],
+ ChecksumType: chkSums[xhttp.AmzChecksumType],
+ }
+ }
+ }
+
+ if _, ok := opts.ObjectAttributes[xhttp.ETag]; ok {
+ OA.ETag = objInfo.ETag
+ }
+
+ if _, ok := opts.ObjectAttributes[xhttp.ObjectSize]; ok {
+ OA.ObjectSize, _ = objInfo.GetActualSize()
+ }
+
+ if _, ok := opts.ObjectAttributes[xhttp.StorageClass]; ok {
+ OA.StorageClass = filterStorageClass(ctx, objInfo.StorageClass)
+ }
+
+ objInfo.decryptPartsChecksums(r.Header)
+
+ if _, ok := opts.ObjectAttributes[xhttp.ObjectParts]; ok {
+ OA.ObjectParts = new(objectAttributesParts)
+ OA.ObjectParts.PartNumberMarker = opts.PartNumberMarker
+
+ OA.ObjectParts.MaxParts = opts.MaxParts
+ partsLength := len(objInfo.Parts)
+ OA.ObjectParts.PartsCount = partsLength
+
+ if opts.MaxParts > -1 {
+ for i, v := range objInfo.Parts {
+ if v.Number <= opts.PartNumberMarker {
+ continue
+ }
+
+ if len(OA.ObjectParts.Parts) == opts.MaxParts {
+ break
+ }
+
+ OA.ObjectParts.NextPartNumberMarker = v.Number
+ OA.ObjectParts.Parts = append(OA.ObjectParts.Parts, &objectAttributesPart{
+ ChecksumSHA1: objInfo.Parts[i].Checksums["SHA1"],
+ ChecksumSHA256: objInfo.Parts[i].Checksums["SHA256"],
+ ChecksumCRC32: objInfo.Parts[i].Checksums["CRC32"],
+ ChecksumCRC32C: objInfo.Parts[i].Checksums["CRC32C"],
+ ChecksumCRC64NVME: objInfo.Parts[i].Checksums["CRC64NVME"],
+ PartNumber: objInfo.Parts[i].Number,
+ Size: objInfo.Parts[i].Size,
+ })
+ }
+ }
+
+ if OA.ObjectParts.NextPartNumberMarker != partsLength {
+ OA.ObjectParts.IsTruncated = true
+ }
+ }
+
+ writeSuccessResponseXML(w, encodeResponse(OA))
+
+ sendEvent(eventArgs{
+ EventName: event.ObjectAccessedAttributes,
+ BucketName: bucket,
+ Object: objInfo,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+}
+
+// GetObjectHandler - GET Object
+// ----------
+// This implementation of the GET operation retrieves object. To use GET,
+// you must have READ access to the object.
+func (api objectAPIHandlers) GetObjectHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetObject")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+ object, err := unescapePath(vars["object"])
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ if !globalAPIConfig.shouldGzipObjects() {
+ w.Header().Set(gzhttp.HeaderNoCompression, "true")
+ }
+
+ if r.Header.Get(xMinIOExtract) == "true" && strings.Contains(object, archivePattern) {
+ api.getObjectInArchiveFileHandler(ctx, objectAPI, bucket, object, w, r)
+ } else {
+ api.getObjectHandler(ctx, objectAPI, bucket, object, w, r)
+ }
+}
+
+func (api objectAPIHandlers) headObjectHandler(ctx context.Context, objectAPI ObjectLayer, bucket, object string, w http.ResponseWriter, r *http.Request) {
+ if crypto.S3.IsRequested(r.Header) || crypto.S3KMS.IsRequested(r.Header) { // If SSE-S3 or SSE-KMS present -> AWS fails with undefined error
+ writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(ErrBadRequest))
+ return
+ }
+
+ getObjectInfo := objectAPI.GetObjectInfo
+
+ opts, err := getOpts(ctx, r, bucket, object)
+ if err != nil {
+ writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
+ return
+ }
+
+ // Check for auth type to return S3 compatible error.
+ // type to return the correct error (NoSuchKey vs AccessDenied)
+ if s3Error := authenticateRequest(ctx, r, policy.GetObjectAction); s3Error != ErrNone {
+ if getRequestAuthType(r) == authTypeAnonymous {
+ // As per "Permission" section in
+ // https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html
+ // If the object you request does not exist,
+ // the error Amazon S3 returns depends on
+ // whether you also have the s3:ListBucket
+ // permission.
+ // * If you have the s3:ListBucket permission
+ // on the bucket, Amazon S3 will return an
+ // HTTP status code 404 ("no such key")
+ // error.
+ // * if you don’t have the s3:ListBucket
+ // permission, Amazon S3 will return an HTTP
+ // status code 403 ("access denied") error.`
+ if globalPolicySys.IsAllowed(policy.BucketPolicyArgs{
+ Action: policy.ListBucketAction,
+ BucketName: bucket,
+ ConditionValues: getConditionValues(r, "", auth.AnonymousCredentials),
+ IsOwner: false,
+ }) {
+ getObjectInfo := objectAPI.GetObjectInfo
+
+ _, err = getObjectInfo(ctx, bucket, object, opts)
+ if toAPIError(ctx, err).Code == "NoSuchKey" {
+ s3Error = ErrNoSuchKey
+ }
+ }
+ }
+ writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(s3Error))
+ return
+ }
+
+ // Get request range.
+ var rs *HTTPRangeSpec
+ rangeHeader := r.Header.Get(xhttp.Range)
+ if rangeHeader != "" {
+ rs, _ = parseRequestRangeSpec(rangeHeader)
+ }
+
+ if rangeHeader != "" {
+ // Both 'Range' and 'partNumber' cannot be specified at the same time
+ if opts.PartNumber > 0 {
+ writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(ErrInvalidRangePartNumber))
+ return
+ }
+
+ if rs, err = parseRequestRangeSpec(rangeHeader); err != nil {
+ // Handle only errInvalidRange. Ignore other
+ // parse error and treat it as regular Get
+ // request like Amazon S3.
+ if errors.Is(err, errInvalidRange) {
+ writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(ErrInvalidRange))
+ return
+ }
+ }
+ }
+
+ opts.FastGetObjInfo = true
+
+ objInfo, err := getObjectInfo(ctx, bucket, object, opts)
+ var proxy proxyResult
+ if err != nil && !objInfo.DeleteMarker && (isErrObjectNotFound(err) || isErrVersionNotFound(err) || isErrReadQuorum(err)) {
+ // proxy HEAD to replication target if active-active replication configured on bucket
+ proxytgts := getProxyTargets(ctx, bucket, object, opts)
+ if !proxytgts.Empty() {
+ globalReplicationStats.Load().incProxy(bucket, headObjectAPI, false)
+ var oi ObjectInfo
+ oi, proxy = proxyHeadToReplicationTarget(ctx, bucket, object, rs, opts, proxytgts)
+ if proxy.Proxy {
+ objInfo = oi
+ }
+ if proxy.Err != nil {
+ globalReplicationStats.Load().incProxy(bucket, headObjectAPI, true)
+ writeErrorResponseHeadersOnly(w, toAPIError(ctx, proxy.Err))
+ return
+ }
+ }
+ }
+
+ if objInfo.UserTags != "" {
+ // Set this such that authorization policies can be applied on the object tags.
+ r.Header.Set(xhttp.AmzObjectTagging, objInfo.UserTags)
+ }
+
+ if s3Error := authorizeRequest(ctx, r, policy.GetObjectAction); s3Error != ErrNone {
+ writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(s3Error))
+ return
+ }
+
+ if err != nil && !proxy.Proxy {
+ switch {
+ case !objInfo.VersionPurgeStatus.Empty():
+ w.Header()[xhttp.MinIODeleteReplicationStatus] = []string{string(objInfo.VersionPurgeStatus)}
+ case !objInfo.ReplicationStatus.Empty() && objInfo.DeleteMarker:
+ w.Header()[xhttp.MinIODeleteMarkerReplicationStatus] = []string{string(objInfo.ReplicationStatus)}
+ }
+ // Versioning enabled quite possibly object is deleted might be delete-marker
+ // if present set the headers, no idea why AWS S3 sets these headers.
+ if objInfo.VersionID != "" && objInfo.DeleteMarker {
+ w.Header()[xhttp.AmzVersionID] = []string{objInfo.VersionID}
+ w.Header()[xhttp.AmzDeleteMarker] = []string{strconv.FormatBool(objInfo.DeleteMarker)}
+ }
+
+ QueueReplicationHeal(ctx, bucket, objInfo, 0)
+ // do an additional verification whether object exists when object is deletemarker and request
+ // is from replication
+ if opts.CheckDMReplicationReady {
+ topts := opts
+ topts.VersionID = ""
+ goi, gerr := getObjectInfo(ctx, bucket, object, topts)
+ if gerr == nil || goi.VersionID != "" { // object layer returned more info because object is deleted
+ w.Header().Set(xhttp.MinIOTargetReplicationReady, "true")
+ }
+ }
+
+ writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
+ return
+ }
+
+ if !proxy.Proxy { // apply lifecycle rules only locally not for proxied requests
+ // Automatically remove the object/version if an expiry lifecycle rule can be applied
+ if lc, err := globalLifecycleSys.Get(bucket); err == nil {
+ rcfg, err := globalBucketObjectLockSys.Get(bucket)
+ if err != nil {
+ writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
+ return
+ }
+ replcfg, err := getReplicationConfig(ctx, bucket)
+ if err != nil {
+ writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
+ return
+ }
+ event := evalActionFromLifecycle(ctx, *lc, rcfg, replcfg, objInfo)
+ if event.Action.Delete() {
+ // apply whatever the expiry rule is.
+ applyExpiryRule(event, lcEventSrc_s3HeadObject, objInfo)
+ if !event.Action.DeleteRestored() {
+ // If the ILM action is not on restored object return error.
+ writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(ErrNoSuchKey))
+ return
+ }
+ }
+ }
+ QueueReplicationHeal(ctx, bucket, objInfo, 0)
+ }
+
+ // filter object lock metadata if permission does not permit
+ getRetPerms := checkRequestAuthType(ctx, r, policy.GetObjectRetentionAction, bucket, object)
+ legalHoldPerms := checkRequestAuthType(ctx, r, policy.GetObjectLegalHoldAction, bucket, object)
+
+ // filter object lock metadata if permission does not permit
+ objInfo.UserDefined = objectlock.FilterObjectLockMetadata(objInfo.UserDefined, getRetPerms != ErrNone, legalHoldPerms != ErrNone)
+
+ if _, err = DecryptObjectInfo(&objInfo, r); err != nil {
+ writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
+ return
+ }
+
+ // Validate pre-conditions if any.
+ if checkPreconditions(ctx, w, r, objInfo, opts) {
+ return
+ }
+
+ // Set encryption response headers
+ switch kind, _ := crypto.IsEncrypted(objInfo.UserDefined); kind {
+ case crypto.S3:
+ w.Header().Set(xhttp.AmzServerSideEncryption, xhttp.AmzEncryptionAES)
+ case crypto.S3KMS:
+ w.Header().Set(xhttp.AmzServerSideEncryption, xhttp.AmzEncryptionKMS)
+ w.Header().Set(xhttp.AmzServerSideEncryptionKmsID, objInfo.KMSKeyID())
+ if kmsCtx, ok := objInfo.UserDefined[crypto.MetaContext]; ok {
+ w.Header().Set(xhttp.AmzServerSideEncryptionKmsContext, kmsCtx)
+ }
+ case crypto.SSEC:
+ // Validate the SSE-C Key set in the header.
+ if _, err = crypto.SSEC.UnsealObjectKey(r.Header, objInfo.UserDefined, bucket, object); err != nil {
+ writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
+ return
+ }
+ w.Header().Set(xhttp.AmzServerSideEncryptionCustomerAlgorithm, r.Header.Get(xhttp.AmzServerSideEncryptionCustomerAlgorithm))
+ w.Header().Set(xhttp.AmzServerSideEncryptionCustomerKeyMD5, r.Header.Get(xhttp.AmzServerSideEncryptionCustomerKeyMD5))
+ }
+
+ if r.Header.Get(xhttp.AmzChecksumMode) == "ENABLED" && rs == nil {
+ // AWS S3 silently drops checksums on range requests.
+ cs, _ := objInfo.decryptChecksums(opts.PartNumber, r.Header)
+ hash.AddChecksumHeader(w, cs)
+ }
+
+ // Set standard object headers.
+ if err = setObjectHeaders(ctx, w, objInfo, rs, opts); err != nil {
+ writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
+ return
+ }
+
+ // Set Parts Count Header
+ if opts.PartNumber > 0 && len(objInfo.Parts) > 0 {
+ setPartsCountHeaders(w, objInfo)
+ }
+
+ // Set any additional requested response headers.
+ setHeadGetRespHeaders(w, r.Form)
+
+ // Successful response.
+ if rs != nil || opts.PartNumber > 0 {
+ w.WriteHeader(http.StatusPartialContent)
+ } else {
+ w.WriteHeader(http.StatusOK)
+ }
+
+ // Notify object accessed via a HEAD request.
+ sendEvent(eventArgs{
+ EventName: event.ObjectAccessedHead,
+ BucketName: bucket,
+ Object: objInfo,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+}
+
+// GetObjectAttributesHandles - GET Object
+// -----------
+// This operation retrieves metadata and part metadata from an object without returning the object itself.
+func (api objectAPIHandlers) GetObjectAttributesHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetObjectAttributes")
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(ErrServerNotInitialized))
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+ object, err := unescapePath(vars["object"])
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ api.getObjectAttributesHandler(ctx, objectAPI, bucket, object, w, r)
+}
+
+// HeadObjectHandler - HEAD Object
+// -----------
+// The HEAD operation retrieves metadata from an object without returning the object itself.
+func (api objectAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "HeadObject")
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(ErrServerNotInitialized))
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+ object, err := unescapePath(vars["object"])
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if r.Header.Get(xMinIOExtract) == "true" && strings.Contains(object, archivePattern) {
+ api.headObjectInArchiveFileHandler(ctx, objectAPI, bucket, object, w, r)
+ } else {
+ api.headObjectHandler(ctx, objectAPI, bucket, object, w, r)
+ }
+}
+
+// Extract metadata relevant for an CopyObject operation based on conditional
+// header values specified in X-Amz-Metadata-Directive.
+func getCpObjMetadataFromHeader(ctx context.Context, r *http.Request, userMeta map[string]string) (map[string]string, error) {
+ // Make a copy of the supplied metadata to avoid
+ // to change the original one.
+ defaultMeta := make(map[string]string, len(userMeta))
+ for k, v := range userMeta {
+ // skip tier metadata when copying metadata from source object
+ switch k {
+ case metaTierName, metaTierStatus, metaTierObjName, metaTierVersionID:
+ continue
+ }
+ defaultMeta[k] = v
+ }
+
+ // remove SSE Headers from source info
+ crypto.RemoveSSEHeaders(defaultMeta)
+
+ // Storage class is special, it can be replaced regardless of the
+ // metadata directive, if set should be preserved and replaced
+ // to the destination metadata.
+ sc := r.Header.Get(xhttp.AmzStorageClass)
+ if sc == "" {
+ sc = r.Form.Get(xhttp.AmzStorageClass)
+ }
+
+ // if x-amz-metadata-directive says REPLACE then
+ // we extract metadata from the input headers.
+ if isDirectiveReplace(r.Header.Get(xhttp.AmzMetadataDirective)) {
+ emetadata, err := extractMetadataFromReq(ctx, r)
+ if err != nil {
+ return nil, err
+ }
+ if sc != "" {
+ emetadata[xhttp.AmzStorageClass] = sc
+ }
+ return emetadata, nil
+ }
+
+ if sc != "" {
+ defaultMeta[xhttp.AmzStorageClass] = sc
+ }
+
+ // if x-amz-metadata-directive says COPY then we
+ // return the default metadata.
+ if isDirectiveCopy(r.Header.Get(xhttp.AmzMetadataDirective)) {
+ return defaultMeta, nil
+ }
+
+ // Copy is default behavior if not x-amz-metadata-directive is set.
+ return defaultMeta, nil
+}
+
+// getRemoteInstanceTransport contains a roundtripper for external (not peers) servers
+var remoteInstanceTransport atomic.Value
+
+func setRemoteInstanceTransport(tr http.RoundTripper) {
+ remoteInstanceTransport.Store(tr)
+}
+
+func getRemoteInstanceTransport() http.RoundTripper {
+ rt, ok := remoteInstanceTransport.Load().(http.RoundTripper)
+ if ok {
+ return rt
+ }
+ return nil
+}
+
+// Returns a minio-go Client configured to access remote host described by destDNSRecord
+// Applicable only in a federated deployment
+var getRemoteInstanceClient = func(r *http.Request, host string) (*miniogo.Core, error) {
+ cred := getReqAccessCred(r, globalSite.Region())
+ // In a federated deployment, all the instances share config files
+ // and hence expected to have same credentials.
+ core, err := miniogo.NewCore(host, &miniogo.Options{
+ Creds: credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, ""),
+ Secure: globalIsTLS,
+ Transport: getRemoteInstanceTransport(),
+ })
+ if err != nil {
+ return nil, err
+ }
+ core.SetAppInfo("minio-federated", ReleaseTag)
+ return core, nil
+}
+
+// Check if the destination bucket is on a remote site, this code only gets executed
+// when federation is enabled, ie when globalDNSConfig is non 'nil'.
+//
+// This function is similar to isRemoteCallRequired but specifically for COPY object API
+// if destination and source are same we do not need to check for destination bucket
+// to exist locally.
+func isRemoteCopyRequired(ctx context.Context, srcBucket, dstBucket string, objAPI ObjectLayer) bool {
+ if srcBucket == dstBucket {
+ return false
+ }
+ return isRemoteCallRequired(ctx, dstBucket, objAPI)
+}
+
+// Check if the bucket is on a remote site, this code only gets executed when federation is enabled.
+func isRemoteCallRequired(ctx context.Context, bucket string, objAPI ObjectLayer) bool {
+ if globalDNSConfig == nil {
+ return false
+ }
+ if globalBucketFederation {
+ _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
+ return err == toObjectErr(errVolumeNotFound, bucket)
+ }
+ return false
+}
+
+// CopyObjectHandler - Copy Object
+// ----------
+// This implementation of the PUT operation adds an object to a bucket
+// while reading the object from another source.
+// Notice: The S3 client can send secret keys in headers for encryption related jobs,
+// the handler should ensure to remove these keys before sending them to the object layer.
+// Currently these keys are:
+// - X-Amz-Server-Side-Encryption-Customer-Key
+// - X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key
+func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "CopyObject")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ dstBucket := vars["bucket"]
+ dstObject, err := unescapePath(vars["object"])
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.PutObjectAction, dstBucket, dstObject); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Read escaped copy source path to check for parameters.
+ cpSrcPath := r.Header.Get(xhttp.AmzCopySource)
+ var vid string
+ if u, err := url.Parse(cpSrcPath); err == nil {
+ vid = strings.TrimSpace(u.Query().Get(xhttp.VersionID))
+ // Note that url.Parse does the unescaping
+ cpSrcPath = u.Path
+ }
+
+ srcBucket, srcObject := path2BucketObject(cpSrcPath)
+ // If source object is empty or bucket is empty, reply back invalid copy source.
+ if srcObject == "" || srcBucket == "" {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidCopySource), r.URL)
+ return
+ }
+
+ // Sanitize the source object name similar to NewMultipart and PutObject API
+ srcObject = trimLeadingSlash(srcObject)
+
+ if vid != "" && vid != nullVersionID {
+ _, err := uuid.Parse(vid)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, VersionNotFound{
+ Bucket: srcBucket,
+ Object: srcObject,
+ VersionID: vid,
+ }), r.URL)
+ return
+ }
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetObjectAction, srcBucket, srcObject); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ // Check if metadata directive is valid.
+ if !isDirectiveValid(r.Header.Get(xhttp.AmzMetadataDirective)) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidMetadataDirective), r.URL)
+ return
+ }
+
+ // check if tag directive is valid
+ if !isDirectiveValid(r.Header.Get(xhttp.AmzTagDirective)) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidTagDirective), r.URL)
+ return
+ }
+
+ // Validate storage class metadata if present
+ dstSc := r.Header.Get(xhttp.AmzStorageClass)
+ if dstSc != "" && !storageclass.IsValid(dstSc) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidStorageClass), r.URL)
+ return
+ }
+
+ // Check if bucket encryption is enabled
+ sseConfig, _ := globalBucketSSEConfigSys.Get(dstBucket)
+ sseConfig.Apply(r.Header, sse.ApplyOptions{
+ AutoEncrypt: globalAutoEncryption,
+ })
+
+ var srcOpts, dstOpts ObjectOptions
+ srcOpts, err = copySrcOpts(ctx, r, srcBucket, srcObject)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ srcOpts.VersionID = vid
+
+ // convert copy src encryption options for GET calls
+ getOpts := ObjectOptions{
+ VersionID: srcOpts.VersionID,
+ Versioned: srcOpts.Versioned,
+ VersionSuspended: srcOpts.VersionSuspended,
+ ReplicationRequest: r.Header.Get(xhttp.MinIOSourceReplicationRequest) == "true",
+ }
+ getSSE := encrypt.SSE(srcOpts.ServerSideEncryption)
+ if getSSE != srcOpts.ServerSideEncryption {
+ getOpts.ServerSideEncryption = getSSE
+ }
+
+ dstOpts, err = copyDstOpts(ctx, r, dstBucket, dstObject, nil)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ cpSrcDstSame := isStringEqual(pathJoin(srcBucket, srcObject), pathJoin(dstBucket, dstObject))
+
+ getObjectNInfo := objectAPI.GetObjectNInfo
+
+ checkCopyPrecondFn := func(o ObjectInfo) bool {
+ if _, err := DecryptObjectInfo(&o, r); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return true
+ }
+ return checkCopyObjectPreconditions(ctx, w, r, o)
+ }
+ getOpts.CheckPrecondFn = checkCopyPrecondFn
+ if cpSrcDstSame {
+ getOpts.NoLock = true
+ }
+
+ var rs *HTTPRangeSpec
+ gr, err := getObjectNInfo(ctx, srcBucket, srcObject, rs, r.Header, getOpts)
+ if err != nil {
+ if isErrPreconditionFailed(err) {
+ return
+ }
+
+ // Versioning enabled quite possibly object is deleted might be delete-marker
+ // if present set the headers, no idea why AWS S3 sets these headers.
+ if gr != nil && gr.ObjInfo.VersionID != "" && gr.ObjInfo.DeleteMarker {
+ w.Header()[xhttp.AmzVersionID] = []string{gr.ObjInfo.VersionID}
+ w.Header()[xhttp.AmzDeleteMarker] = []string{strconv.FormatBool(gr.ObjInfo.DeleteMarker)}
+ }
+
+ // Update context bucket & object names for correct S3 XML error response
+ reqInfo := logger.GetReqInfo(ctx)
+ reqInfo.BucketName = srcBucket
+ reqInfo.ObjectName = srcObject
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ defer gr.Close()
+ srcInfo := gr.ObjInfo
+
+ // maximum Upload size for object in a single CopyObject operation.
+ if isMaxObjectSize(srcInfo.Size) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrEntityTooLarge), r.URL)
+ return
+ }
+
+ // We have to copy metadata only if source and destination are same.
+ // this changes for encryption which can be observed below.
+ if cpSrcDstSame {
+ srcInfo.metadataOnly = true
+ }
+
+ var chStorageClass bool
+ if dstSc != "" && dstSc != srcInfo.StorageClass {
+ chStorageClass = true
+ srcInfo.metadataOnly = false
+ } // no changes in storage-class expected so its a metadataonly operation.
+
+ var reader io.Reader = gr
+
+ // Set the actual size to the compressed/decrypted size if encrypted.
+ actualSize, err := srcInfo.GetActualSize()
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ length := actualSize
+
+ if !cpSrcDstSame {
+ if err := enforceBucketQuotaHard(ctx, dstBucket, actualSize); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ }
+
+ var compressMetadata map[string]string
+ // No need to compress for remote etcd calls
+ // Pass the decompressed stream to such calls.
+ isDstCompressed := isCompressible(r.Header, dstObject) &&
+ length > minCompressibleSize &&
+ !isRemoteCopyRequired(ctx, srcBucket, dstBucket, objectAPI)
+ if isDstCompressed {
+ compressMetadata = make(map[string]string, 2)
+ // Preserving the compression metadata.
+ compressMetadata[ReservedMetadataPrefix+"compression"] = compressionAlgorithmV2
+ compressMetadata[ReservedMetadataPrefix+"actual-size"] = strconv.FormatInt(actualSize, 10)
+
+ reader = etag.NewReader(ctx, reader, nil, nil)
+ wantEncryption := crypto.Requested(r.Header)
+ s2c, cb := newS2CompressReader(reader, actualSize, wantEncryption)
+ dstOpts.IndexCB = cb
+ defer s2c.Close()
+ reader = etag.Wrap(s2c, reader)
+ length = -1
+ } else {
+ delete(srcInfo.UserDefined, ReservedMetadataPrefix+"compression")
+ delete(srcInfo.UserDefined, ReservedMetadataPrefix+"actual-size")
+ reader = gr
+ }
+
+ srcInfo.Reader, err = hash.NewReader(ctx, reader, length, "", "", actualSize)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ pReader := NewPutObjReader(srcInfo.Reader)
+
+ // Handle encryption
+ encMetadata := make(map[string]string)
+ // Encryption parameters not applicable for this object.
+ if _, ok := crypto.IsEncrypted(srcInfo.UserDefined); !ok && crypto.SSECopy.IsRequested(r.Header) {
+ writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidEncryptionParameters), r.URL)
+ return
+ }
+ // Encryption parameters not present for this object.
+ if crypto.SSEC.IsEncrypted(srcInfo.UserDefined) && !crypto.SSECopy.IsRequested(r.Header) && r.Header.Get(xhttp.MinIOSourceReplicationRequest) != "true" {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidSSECustomerAlgorithm), r.URL)
+ return
+ }
+
+ var oldKey, newKey []byte
+ var newKeyID string
+ var kmsCtx kms.Context
+ var objEncKey crypto.ObjectKey
+ sseCopyKMS := crypto.S3KMS.IsEncrypted(srcInfo.UserDefined)
+ sseCopyS3 := crypto.S3.IsEncrypted(srcInfo.UserDefined)
+ sseCopyC := crypto.SSEC.IsEncrypted(srcInfo.UserDefined) && crypto.SSECopy.IsRequested(r.Header)
+ sseC := crypto.SSEC.IsRequested(r.Header)
+ sseS3 := crypto.S3.IsRequested(r.Header)
+ sseKMS := crypto.S3KMS.IsRequested(r.Header)
+
+ isSourceEncrypted := sseCopyC || sseCopyS3 || sseCopyKMS
+ isTargetEncrypted := sseC || sseS3 || sseKMS
+
+ if sseC {
+ newKey, err = ParseSSECustomerRequest(r)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ }
+ if crypto.S3KMS.IsRequested(r.Header) {
+ newKeyID, kmsCtx, err = crypto.S3KMS.ParseHTTP(r.Header)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ }
+
+ // If src == dst and either
+ // - the object is encrypted using SSE-C and two different SSE-C keys are present
+ // - the object is encrypted using SSE-S3 and the SSE-S3 header is present
+ // - the object storage class is not changing
+ // then execute a key rotation.
+ if cpSrcDstSame && (sseCopyC && sseC) && !chStorageClass {
+ oldKey, err = ParseSSECopyCustomerRequest(r.Header, srcInfo.UserDefined)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ for k, v := range srcInfo.UserDefined {
+ if stringsHasPrefixFold(k, ReservedMetadataPrefixLower) {
+ encMetadata[k] = v
+ }
+ }
+
+ if err = rotateKey(ctx, oldKey, newKeyID, newKey, srcBucket, srcObject, encMetadata, kmsCtx); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Since we are rotating the keys, make sure to update the metadata.
+ srcInfo.metadataOnly = true
+ srcInfo.keyRotation = true
+ } else {
+ if isSourceEncrypted || isTargetEncrypted {
+ // We are not only copying just metadata instead
+ // we are creating a new object at this point, even
+ // if source and destination are same objects.
+ if !srcInfo.keyRotation {
+ srcInfo.metadataOnly = false
+ }
+ }
+
+ // Calculate the size of the target object
+ var targetSize int64
+
+ switch {
+ case isDstCompressed:
+ targetSize = -1
+ case !isSourceEncrypted && !isTargetEncrypted:
+ targetSize, _ = srcInfo.GetActualSize()
+ case isSourceEncrypted && isTargetEncrypted:
+ objInfo := ObjectInfo{Size: actualSize}
+ targetSize = objInfo.EncryptedSize()
+ case !isSourceEncrypted && isTargetEncrypted:
+ targetSize = srcInfo.EncryptedSize()
+ case isSourceEncrypted && !isTargetEncrypted:
+ targetSize, _ = srcInfo.DecryptedSize()
+ }
+
+ // Client can request that a different type of checksum is computed server-side for the
+ // destination object using the x-amz-checksum-algorithm header.
+ headerChecksumType := hash.NewChecksumHeader(r.Header)
+ if headerChecksumType.IsSet() {
+ dstOpts.WantServerSideChecksumType = headerChecksumType.Base()
+ srcInfo.Reader.AddServerSideChecksumHasher(headerChecksumType)
+ dstOpts.WantChecksum = nil
+ } else {
+ // Check the source object for checksum.
+ // If Checksum is not encrypted, decryptChecksum will be a no-op and return
+ // the already unencrypted value.
+ srcChecksumDecrypted, err := srcInfo.decryptChecksum(r.Header)
+ if err != nil {
+ encLogOnceIf(GlobalContext,
+ fmt.Errorf("Unable to decryptChecksum for object: %s/%s, error: %w", srcBucket, srcObject, err),
+ "copy-object-decrypt-checksums-"+srcBucket+srcObject)
+ }
+
+ // The source object has a checksum set, we need the destination to have one too.
+ if srcChecksumDecrypted != nil {
+ dstOpts.WantChecksum = hash.ChecksumFromBytes(srcChecksumDecrypted)
+
+ // When an object is being copied from a source that is multipart, the destination will
+ // no longer be multipart, and thus the checksum becomes full-object instead. Since
+ // the CopyObject API does not require that the caller send us this final checksum, we need
+ // to compute it server-side, with the same type as the source object.
+ if dstOpts.WantChecksum != nil && dstOpts.WantChecksum.Type.IsMultipartComposite() {
+ dstOpts.WantServerSideChecksumType = dstOpts.WantChecksum.Type.Base()
+ srcInfo.Reader.AddServerSideChecksumHasher(dstOpts.WantServerSideChecksumType)
+ dstOpts.WantChecksum = nil
+ }
+ } else {
+ // S3: All copied objects without checksums and specified destination checksum algorithms
+ // automatically gain a CRC-64NVME checksum algorithm.
+ dstOpts.WantServerSideChecksumType = hash.ChecksumCRC64NVME
+ srcInfo.Reader.AddServerSideChecksumHasher(dstOpts.WantServerSideChecksumType)
+ dstOpts.WantChecksum = nil
+ }
+ }
+
+ if isTargetEncrypted {
+ var encReader io.Reader
+ kind, _ := crypto.IsRequested(r.Header)
+ encReader, objEncKey, err = newEncryptReader(ctx, srcInfo.Reader, kind, newKeyID, newKey, dstBucket, dstObject, encMetadata, kmsCtx)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ reader = etag.Wrap(encReader, srcInfo.Reader)
+ }
+
+ if isSourceEncrypted {
+ // Remove all source encrypted related metadata to
+ // avoid copying them in target object.
+ crypto.RemoveInternalEntries(srcInfo.UserDefined)
+ }
+
+ // do not try to verify encrypted content
+ srcInfo.Reader, err = hash.NewReader(ctx, reader, targetSize, "", "", actualSize)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if isTargetEncrypted {
+ pReader, err = pReader.WithEncryption(srcInfo.Reader, &objEncKey)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ if dstOpts.IndexCB != nil {
+ dstOpts.IndexCB = compressionIndexEncrypter(objEncKey, dstOpts.IndexCB)
+ }
+ dstOpts.EncryptFn = metadataEncrypter(objEncKey)
+ }
+ }
+
+ srcInfo.PutObjReader = pReader
+
+ srcInfo.UserDefined, err = getCpObjMetadataFromHeader(ctx, r, srcInfo.UserDefined)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ objTags := srcInfo.UserTags
+ // If x-amz-tagging-directive header is REPLACE, get passed tags.
+ if isDirectiveReplace(r.Header.Get(xhttp.AmzTagDirective)) {
+ objTags = r.Header.Get(xhttp.AmzObjectTagging)
+ if _, err := tags.ParseObjectTags(objTags); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ }
+
+ if objTags != "" {
+ lastTaggingTimestamp := srcInfo.UserDefined[ReservedMetadataPrefixLower+TaggingTimestamp]
+ if dstOpts.ReplicationRequest {
+ srcTimestamp := dstOpts.ReplicationSourceTaggingTimestamp
+ if !srcTimestamp.IsZero() {
+ ondiskTimestamp, err := time.Parse(time.RFC3339Nano, lastTaggingTimestamp)
+ // update tagging metadata only if replica timestamp is newer than what's on disk
+ if err != nil || (err == nil && !ondiskTimestamp.After(srcTimestamp)) {
+ srcInfo.UserDefined[ReservedMetadataPrefixLower+TaggingTimestamp] = srcTimestamp.UTC().Format(time.RFC3339Nano)
+ srcInfo.UserDefined[xhttp.AmzObjectTagging] = objTags
+ }
+ }
+ } else {
+ srcInfo.UserDefined[xhttp.AmzObjectTagging] = objTags
+ srcInfo.UserDefined[ReservedMetadataPrefixLower+TaggingTimestamp] = UTCNow().Format(time.RFC3339Nano)
+ }
+ }
+
+ srcInfo.UserDefined = filterReplicationStatusMetadata(srcInfo.UserDefined)
+ srcInfo.UserDefined = objectlock.FilterObjectLockMetadata(srcInfo.UserDefined, true, true)
+ retPerms := isPutActionAllowed(ctx, getRequestAuthType(r), dstBucket, dstObject, r, policy.PutObjectRetentionAction)
+ holdPerms := isPutActionAllowed(ctx, getRequestAuthType(r), dstBucket, dstObject, r, policy.PutObjectLegalHoldAction)
+ getObjectInfo := objectAPI.GetObjectInfo
+
+ // apply default bucket configuration/governance headers for dest side.
+ retentionMode, retentionDate, legalHold, s3Err := checkPutObjectLockAllowed(ctx, r, dstBucket, dstObject, getObjectInfo, retPerms, holdPerms)
+ if s3Err == ErrNone && retentionMode.Valid() {
+ lastretentionTimestamp := srcInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp]
+ if dstOpts.ReplicationRequest {
+ srcTimestamp := dstOpts.ReplicationSourceRetentionTimestamp
+ if !srcTimestamp.IsZero() {
+ ondiskTimestamp, err := time.Parse(time.RFC3339Nano, lastretentionTimestamp)
+ // update retention metadata only if replica timestamp is newer than what's on disk
+ if err != nil || (err == nil && ondiskTimestamp.Before(srcTimestamp)) {
+ srcInfo.UserDefined[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode)
+ srcInfo.UserDefined[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = amztime.ISO8601Format(retentionDate.UTC())
+ srcInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp] = srcTimestamp.UTC().Format(time.RFC3339Nano)
+ }
+ }
+ } else {
+ srcInfo.UserDefined[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode)
+ srcInfo.UserDefined[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = amztime.ISO8601Format(retentionDate.UTC())
+ srcInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp] = UTCNow().Format(time.RFC3339Nano)
+ }
+ }
+
+ if s3Err == ErrNone && legalHold.Status.Valid() {
+ lastLegalHoldTimestamp := srcInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp]
+ if dstOpts.ReplicationRequest {
+ srcTimestamp := dstOpts.ReplicationSourceLegalholdTimestamp
+ if !srcTimestamp.IsZero() {
+ ondiskTimestamp, err := time.Parse(time.RFC3339Nano, lastLegalHoldTimestamp)
+ // update legalhold metadata only if replica timestamp is newer than what's on disk
+ if err != nil || (err == nil && ondiskTimestamp.Before(srcTimestamp)) {
+ srcInfo.UserDefined[strings.ToLower(xhttp.AmzObjectLockLegalHold)] = string(legalHold.Status)
+ srcInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp] = srcTimestamp.Format(time.RFC3339Nano)
+ }
+ }
+ } else {
+ srcInfo.UserDefined[strings.ToLower(xhttp.AmzObjectLockLegalHold)] = string(legalHold.Status)
+ }
+ }
+ if s3Err != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
+ return
+ }
+ if rs := r.Header.Get(xhttp.AmzBucketReplicationStatus); rs != "" {
+ srcInfo.UserDefined[ReservedMetadataPrefixLower+ReplicaStatus] = replication.Replica.String()
+ srcInfo.UserDefined[ReservedMetadataPrefixLower+ReplicaTimestamp] = UTCNow().Format(time.RFC3339Nano)
+ srcInfo.UserDefined[xhttp.AmzBucketReplicationStatus] = rs
+ }
+
+ op := replication.ObjectReplicationType
+ if srcInfo.metadataOnly {
+ op = replication.MetadataReplicationType
+ }
+ if dsc := mustReplicate(ctx, dstBucket, dstObject, srcInfo.getMustReplicateOptions(op, dstOpts)); dsc.ReplicateAny() {
+ srcInfo.UserDefined[ReservedMetadataPrefixLower+ReplicationStatus] = dsc.PendingStatus()
+ srcInfo.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano)
+ }
+ // Store the preserved compression metadata.
+ maps.Copy(srcInfo.UserDefined, compressMetadata)
+
+ // We need to preserve the encryption headers set in EncryptRequest,
+ // so we do not want to override them, copy them instead.
+ maps.Copy(srcInfo.UserDefined, encMetadata)
+
+ // Ensure that metadata does not contain sensitive information
+ crypto.RemoveSensitiveEntries(srcInfo.UserDefined)
+
+ // If we see legacy source, metadataOnly we have to overwrite the content.
+ if srcInfo.Legacy {
+ srcInfo.metadataOnly = false
+ }
+
+ // Check if x-amz-metadata-directive or x-amz-tagging-directive was not set to REPLACE and source,
+ // destination are same objects. Apply this restriction also when
+ // metadataOnly is true indicating that we are not overwriting the object.
+ // if encryption is enabled we do not need explicit "REPLACE" metadata to
+ // be enabled as well - this is to allow for key-rotation.
+ if !isDirectiveReplace(r.Header.Get(xhttp.AmzMetadataDirective)) && !isDirectiveReplace(r.Header.Get(xhttp.AmzTagDirective)) &&
+ srcInfo.metadataOnly && srcOpts.VersionID == "" &&
+ !crypto.Requested(r.Header) &&
+ !crypto.IsSourceEncrypted(srcInfo.UserDefined) {
+ // If x-amz-metadata-directive is not set to REPLACE then we need
+ // to error out if source and destination are same.
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidCopyDest), r.URL)
+ return
+ }
+
+ // After we've checked for an invalid copy (above), if a server-side checksum type
+ // is requested, we need to read the source to recompute the checksum.
+ if dstOpts.WantServerSideChecksumType.IsSet() {
+ srcInfo.metadataOnly = false
+ }
+
+ // Federation only.
+ remoteCallRequired := isRemoteCopyRequired(ctx, srcBucket, dstBucket, objectAPI)
+
+ var objInfo ObjectInfo
+ var os *objSweeper
+ if remoteCallRequired {
+ var dstRecords []dns.SrvRecord
+ dstRecords, err = globalDNSConfig.Get(dstBucket)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Send PutObject request to appropriate instance (in federated deployment)
+ core, rerr := getRemoteInstanceClient(r, getHostFromSrv(dstRecords))
+ if rerr != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, rerr), r.URL)
+ return
+ }
+ tag, err := tags.ParseObjectTags(objTags)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ // Remove the metadata for remote calls.
+ delete(srcInfo.UserDefined, ReservedMetadataPrefix+"compression")
+ delete(srcInfo.UserDefined, ReservedMetadataPrefix+"actual-size")
+ opts := miniogo.PutObjectOptions{
+ UserMetadata: srcInfo.UserDefined,
+ ServerSideEncryption: dstOpts.ServerSideEncryption,
+ UserTags: tag.ToMap(),
+ }
+ remoteObjInfo, rerr := core.PutObject(ctx, dstBucket, dstObject, srcInfo.Reader,
+ srcInfo.Size, "", "", opts)
+ if rerr != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, rerr), r.URL)
+ return
+ }
+ objInfo.UserDefined = cloneMSS(opts.UserMetadata)
+ objInfo.ETag = remoteObjInfo.ETag
+ objInfo.ModTime = remoteObjInfo.LastModified
+ } else {
+ os = newObjSweeper(dstBucket, dstObject).WithVersioning(dstOpts.Versioned, dstOpts.VersionSuspended)
+ // Get appropriate object info to identify the remote object to delete
+ if !srcInfo.metadataOnly {
+ goiOpts := os.GetOpts()
+ if !globalTierConfigMgr.Empty() {
+ if goi, gerr := getObjectInfo(ctx, dstBucket, dstObject, goiOpts); gerr == nil {
+ os.SetTransitionState(goi.TransitionedObject)
+ }
+ }
+ }
+
+ copyObjectFn := objectAPI.CopyObject
+
+ // Copy source object to destination, if source and destination
+ // object is same then only metadata is updated.
+ objInfo, err = copyObjectFn(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ }
+
+ origETag := objInfo.ETag
+ objInfo.ETag = getDecryptedETag(r.Header, objInfo, false)
+ response := generateCopyObjectResponse(objInfo.ETag, objInfo.ModTime)
+ encodedSuccessResponse := encodeResponse(response)
+
+ if dsc := mustReplicate(ctx, dstBucket, dstObject, objInfo.getMustReplicateOptions(replication.ObjectReplicationType, dstOpts)); dsc.ReplicateAny() {
+ scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.ObjectReplicationType)
+ }
+
+ setPutObjHeaders(w, objInfo, false, r.Header)
+ // We must not use the http.Header().Set method here because some (broken)
+ // clients expect the x-amz-copy-source-version-id header key to be literally
+ // "x-amz-copy-source-version-id"- not in canonicalized form, preserve it.
+ if srcOpts.VersionID != "" {
+ w.Header()[strings.ToLower(xhttp.AmzCopySourceVersionID)] = []string{srcOpts.VersionID}
+ }
+
+ // Write success response.
+ writeSuccessResponseXML(w, encodedSuccessResponse)
+
+ // Notify object created event.
+ sendEvent(eventArgs{
+ EventName: event.ObjectCreatedCopy,
+ BucketName: dstBucket,
+ Object: objInfo,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+
+ if !remoteCallRequired && !globalTierConfigMgr.Empty() {
+ // Schedule object for immediate transition if eligible.
+ objInfo.ETag = origETag
+ enqueueTransitionImmediate(objInfo, lcEventSrc_s3CopyObject)
+ // Remove the transitioned object whose object version is being overwritten.
+ os.Sweep()
+ }
+}
+
+// PutObjectHandler - PUT Object
+// ----------
+// This implementation of the PUT operation adds an object to a bucket.
+// Notice: The S3 client can send secret keys in headers for encryption related jobs,
+// the handler should ensure to remove these keys before sending them to the object layer.
+// Currently these keys are:
+// - X-Amz-Server-Side-Encryption-Customer-Key
+// - X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key
+func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "PutObject")
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+ object, err := unescapePath(vars["object"])
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // X-Amz-Copy-Source shouldn't be set for this call.
+ if _, ok := r.Header[xhttp.AmzCopySource]; ok {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidCopySource), r.URL)
+ return
+ }
+
+ // Validate storage class metadata if present
+ if sc := r.Header.Get(xhttp.AmzStorageClass); sc != "" {
+ if !storageclass.IsValid(sc) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidStorageClass), r.URL)
+ return
+ }
+ }
+
+ clientETag, err := etag.FromContentMD5(r.Header)
+ if err != nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidDigest), r.URL)
+ return
+ }
+
+ // if Content-Length is unknown/missing, deny the request
+ size := r.ContentLength
+ rAuthType := getRequestAuthType(r)
+ switch rAuthType {
+ // Check signature types that must have content length
+ case authTypeStreamingSigned, authTypeStreamingSignedTrailer, authTypeStreamingUnsignedTrailer:
+ if sizeStr, ok := r.Header[xhttp.AmzDecodedContentLength]; ok {
+ if sizeStr[0] == "" {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL)
+ return
+ }
+ size, err = strconv.ParseInt(sizeStr[0], 10, 64)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ }
+ }
+ if size == -1 {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL)
+ return
+ }
+
+ // maximum Upload size for objects in a single operation
+ if isMaxObjectSize(size) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrEntityTooLarge), r.URL)
+ return
+ }
+
+ metadata, err := extractMetadataFromReq(ctx, r)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if objTags := r.Header.Get(xhttp.AmzObjectTagging); objTags != "" {
+ if _, err := tags.ParseObjectTags(objTags); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ metadata[xhttp.AmzObjectTagging] = objTags
+ }
+
+ var (
+ md5hex = clientETag.String()
+ sha256hex = ""
+ rd io.Reader = r.Body
+ s3Err APIErrorCode
+ putObject = objectAPI.PutObject
+ )
+
+ // Check if put is allowed
+ if s3Err = isPutActionAllowed(ctx, rAuthType, bucket, object, r, policy.PutObjectAction); s3Err != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
+ return
+ }
+
+ switch rAuthType {
+ case authTypeStreamingSigned, authTypeStreamingSignedTrailer:
+ // Initialize stream signature verifier.
+ rd, s3Err = newSignV4ChunkedReader(r, rAuthType == authTypeStreamingSignedTrailer)
+ if s3Err != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
+ return
+ }
+ case authTypeStreamingUnsignedTrailer:
+ // Initialize stream chunked reader with optional trailers.
+ rd, s3Err = newUnsignedV4ChunkedReader(r, true, r.Header.Get(xhttp.Authorization) != "")
+ if s3Err != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
+ return
+ }
+ case authTypeSignedV2, authTypePresignedV2:
+ s3Err = isReqAuthenticatedV2(r)
+ if s3Err != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
+ return
+ }
+
+ case authTypePresigned, authTypeSigned:
+ if s3Err = reqSignatureV4Verify(r, globalSite.Region(), serviceS3); s3Err != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
+ return
+ }
+ if !skipContentSha256Cksum(r) {
+ sha256hex = getContentSha256Cksum(r, serviceS3)
+ }
+ }
+
+ if _, ok := r.Header[xhttp.MinIOSourceReplicationCheck]; ok {
+ // requests to just validate replication settings and permissions are not allowed to write data
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationPermissionCheckError), r.URL)
+ return
+ }
+
+ if err := enforceBucketQuotaHard(ctx, bucket, size); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ if r.Header.Get(xhttp.AmzBucketReplicationStatus) == replication.Replica.String() {
+ if s3Err = isPutActionAllowed(ctx, getRequestAuthType(r), bucket, object, r, policy.ReplicateObjectAction); s3Err != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
+ return
+ }
+ metadata[ReservedMetadataPrefixLower+ReplicaStatus] = replication.Replica.String()
+ metadata[ReservedMetadataPrefixLower+ReplicaTimestamp] = UTCNow().Format(time.RFC3339Nano)
+ defer globalReplicationStats.Load().UpdateReplicaStat(bucket, size)
+ }
+
+ // Check if bucket encryption is enabled
+ sseConfig, _ := globalBucketSSEConfigSys.Get(bucket)
+ sseConfig.Apply(r.Header, sse.ApplyOptions{
+ AutoEncrypt: globalAutoEncryption,
+ })
+
+ var reader io.Reader
+ reader = rd
+
+ var opts ObjectOptions
+ opts, err = putOptsFromReq(ctx, r, bucket, object, metadata)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ actualSize := size
+ var idxCb func() []byte
+ if isCompressible(r.Header, object) && size > minCompressibleSize {
+ // Storing the compression metadata.
+ metadata[ReservedMetadataPrefix+"compression"] = compressionAlgorithmV2
+ metadata[ReservedMetadataPrefix+"actual-size"] = strconv.FormatInt(size, 10)
+
+ actualReader, err := hash.NewReader(ctx, reader, size, md5hex, sha256hex, actualSize)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ if err = actualReader.AddChecksum(r, false); err != nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidChecksum), r.URL)
+ return
+ }
+ opts.WantChecksum = actualReader.Checksum()
+
+ // Set compression metrics.
+ var s2c io.ReadCloser
+ wantEncryption := crypto.Requested(r.Header)
+ s2c, idxCb = newS2CompressReader(actualReader, actualSize, wantEncryption)
+ defer s2c.Close()
+
+ reader = etag.Wrap(s2c, actualReader)
+ size = -1 // Since compressed size is un-predictable.
+ md5hex = "" // Do not try to verify the content.
+ sha256hex = ""
+ }
+
+ var forceMD5 []byte
+ // Optimization: If SSE-KMS and SSE-C did not request Content-Md5. Use uuid as etag. Optionally enable this also
+ // for server that is started with `--no-compat`.
+ if !etag.ContentMD5Requested(r.Header) && (crypto.S3KMS.IsRequested(r.Header) || crypto.SSEC.IsRequested(r.Header) || !globalServerCtxt.StrictS3Compat) {
+ forceMD5 = mustGetUUIDBytes()
+ }
+ hashReader, err := hash.NewReaderWithOpts(ctx, reader, hash.Options{
+ Size: size,
+ MD5Hex: md5hex,
+ SHA256Hex: sha256hex,
+ ActualSize: actualSize,
+ DisableMD5: false,
+ ForceMD5: forceMD5,
+ })
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ if size >= 0 {
+ if err := hashReader.AddChecksum(r, false); err != nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidChecksum), r.URL)
+ return
+ }
+ opts.WantChecksum = hashReader.Checksum()
+ }
+
+ rawReader := hashReader
+ pReader := NewPutObjReader(rawReader)
+ opts.IndexCB = idxCb
+
+ if r.Header.Get(xhttp.IfMatch) != "" {
+ opts.HasIfMatch = true
+ }
+ if opts.PreserveETag != "" ||
+ r.Header.Get(xhttp.IfMatch) != "" ||
+ r.Header.Get(xhttp.IfNoneMatch) != "" {
+ opts.CheckPrecondFn = func(oi ObjectInfo) bool {
+ if _, err := DecryptObjectInfo(&oi, r); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return true
+ }
+ return checkPreconditionsPUT(ctx, w, r, oi, opts)
+ }
+ }
+
+ retPerms := isPutActionAllowed(ctx, getRequestAuthType(r), bucket, object, r, policy.PutObjectRetentionAction)
+ holdPerms := isPutActionAllowed(ctx, getRequestAuthType(r), bucket, object, r, policy.PutObjectLegalHoldAction)
+
+ getObjectInfo := objectAPI.GetObjectInfo
+
+ retentionMode, retentionDate, legalHold, s3Err := checkPutObjectLockAllowed(ctx, r, bucket, object, getObjectInfo, retPerms, holdPerms)
+ if s3Err == ErrNone && retentionMode.Valid() {
+ metadata[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode)
+ metadata[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = amztime.ISO8601Format(retentionDate.UTC())
+ }
+ if s3Err == ErrNone && legalHold.Status.Valid() {
+ metadata[strings.ToLower(xhttp.AmzObjectLockLegalHold)] = string(legalHold.Status)
+ }
+ if s3Err != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
+ return
+ }
+ if dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(metadata, "", "", replication.ObjectReplicationType, opts)); dsc.ReplicateAny() {
+ metadata[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano)
+ metadata[ReservedMetadataPrefixLower+ReplicationStatus] = dsc.PendingStatus()
+ }
+ var objectEncryptionKey crypto.ObjectKey
+ if crypto.Requested(r.Header) {
+ if crypto.SSECopy.IsRequested(r.Header) {
+ writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidEncryptionParameters), r.URL)
+ return
+ }
+
+ if crypto.SSEC.IsRequested(r.Header) && crypto.S3.IsRequested(r.Header) {
+ writeErrorResponse(ctx, w, toAPIError(ctx, crypto.ErrIncompatibleEncryptionMethod), r.URL)
+ return
+ }
+
+ if crypto.SSEC.IsRequested(r.Header) && crypto.S3KMS.IsRequested(r.Header) {
+ writeErrorResponse(ctx, w, toAPIError(ctx, crypto.ErrIncompatibleEncryptionMethod), r.URL)
+ return
+ }
+
+ reader, objectEncryptionKey, err = EncryptRequest(hashReader, r, bucket, object, metadata)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ wantSize := int64(-1)
+ if size >= 0 {
+ info := ObjectInfo{Size: size}
+ wantSize = info.EncryptedSize()
+ }
+
+ // do not try to verify encrypted content
+ hashReader, err = hash.NewReader(ctx, etag.Wrap(reader, hashReader), wantSize, "", "", actualSize)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ pReader, err = pReader.WithEncryption(hashReader, &objectEncryptionKey)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ if opts.IndexCB != nil {
+ opts.IndexCB = compressionIndexEncrypter(objectEncryptionKey, opts.IndexCB)
+ }
+ opts.EncryptFn = metadataEncrypter(objectEncryptionKey)
+ }
+
+ // Ensure that metadata does not contain sensitive information
+ crypto.RemoveSensitiveEntries(metadata)
+
+ os := newObjSweeper(bucket, object).WithVersioning(opts.Versioned, opts.VersionSuspended)
+ if !globalTierConfigMgr.Empty() {
+ // Get appropriate object info to identify the remote object to delete
+ goiOpts := os.GetOpts()
+ if goi, gerr := getObjectInfo(ctx, bucket, object, goiOpts); gerr == nil {
+ os.SetTransitionState(goi.TransitionedObject)
+ }
+ }
+
+ // Create the object..
+ objInfo, err := putObject(ctx, bucket, object, pReader, opts)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if r.Header.Get(xMinIOExtract) == "true" && HasSuffix(object, archiveExt) {
+ opts := ObjectOptions{VersionID: objInfo.VersionID, MTime: objInfo.ModTime}
+ if _, err := updateObjectMetadataWithZipInfo(ctx, objectAPI, bucket, object, opts); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ }
+
+ origETag := objInfo.ETag
+ if kind, encrypted := crypto.IsEncrypted(objInfo.UserDefined); encrypted {
+ switch kind {
+ case crypto.S3:
+ w.Header().Set(xhttp.AmzServerSideEncryption, xhttp.AmzEncryptionAES)
+ objInfo.ETag, _ = DecryptETag(objectEncryptionKey, ObjectInfo{ETag: objInfo.ETag})
+ case crypto.S3KMS:
+ w.Header().Set(xhttp.AmzServerSideEncryption, xhttp.AmzEncryptionKMS)
+ w.Header().Set(xhttp.AmzServerSideEncryptionKmsID, objInfo.KMSKeyID())
+ if kmsCtx, ok := objInfo.UserDefined[crypto.MetaContext]; ok {
+ w.Header().Set(xhttp.AmzServerSideEncryptionKmsContext, kmsCtx)
+ }
+ if len(objInfo.ETag) >= 32 && strings.Count(objInfo.ETag, "-") != 1 {
+ objInfo.ETag = objInfo.ETag[len(objInfo.ETag)-32:]
+ }
+ case crypto.SSEC:
+ w.Header().Set(xhttp.AmzServerSideEncryptionCustomerAlgorithm, r.Header.Get(xhttp.AmzServerSideEncryptionCustomerAlgorithm))
+ w.Header().Set(xhttp.AmzServerSideEncryptionCustomerKeyMD5, r.Header.Get(xhttp.AmzServerSideEncryptionCustomerKeyMD5))
+
+ if len(objInfo.ETag) >= 32 && strings.Count(objInfo.ETag, "-") != 1 {
+ objInfo.ETag = objInfo.ETag[len(objInfo.ETag)-32:]
+ }
+ }
+ }
+ if dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(metadata, "", "", replication.ObjectReplicationType, opts)); dsc.ReplicateAny() {
+ scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.ObjectReplicationType)
+ }
+
+ setPutObjHeaders(w, objInfo, false, r.Header)
+
+ // Notify object created event.
+ evt := eventArgs{
+ EventName: event.ObjectCreatedPut,
+ BucketName: bucket,
+ Object: objInfo,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ }
+ sendEvent(evt)
+ if objInfo.NumVersions > int(scannerExcessObjectVersions.Load()) {
+ evt.EventName = event.ObjectManyVersions
+ sendEvent(evt)
+
+ auditLogInternal(context.Background(), AuditLogOptions{
+ Event: "scanner:manyversions",
+ APIName: "PutObject",
+ Bucket: objInfo.Bucket,
+ Object: objInfo.Name,
+ VersionID: objInfo.VersionID,
+ Status: http.StatusText(http.StatusOK),
+ })
+ }
+
+ // Do not send checksums in events to avoid leaks.
+ hash.TransferChecksumHeader(w, r)
+ writeSuccessResponseHeadersOnly(w)
+
+ // Remove the transitioned object whose object version is being overwritten.
+ if !globalTierConfigMgr.Empty() {
+ // Schedule object for immediate transition if eligible.
+ objInfo.ETag = origETag
+ enqueueTransitionImmediate(objInfo, lcEventSrc_s3PutObject)
+ os.Sweep()
+ }
+}
+
+// PutObjectExtractHandler - PUT Object extract is an extended API
+// based off from AWS Snowball feature to auto extract compressed
+// stream will be extracted in the same directory it is stored in
+// and the folder structures will be built out accordingly.
+func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "PutObjectExtract")
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ if crypto.S3KMS.IsRequested(r.Header) { // SSE-KMS is not supported
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+ object, err := unescapePath(vars["object"])
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // X-Amz-Copy-Source shouldn't be set for this call.
+ if _, ok := r.Header[xhttp.AmzCopySource]; ok {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidCopySource), r.URL)
+ return
+ }
+
+ // Validate storage class metadata if present
+ sc := r.Header.Get(xhttp.AmzStorageClass)
+ if sc != "" {
+ if !storageclass.IsValid(sc) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidStorageClass), r.URL)
+ return
+ }
+ }
+
+ clientETag, err := etag.FromContentMD5(r.Header)
+ if err != nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidDigest), r.URL)
+ return
+ }
+
+ // if Content-Length is unknown/missing, deny the request
+ size := r.ContentLength
+ rAuthType := getRequestAuthType(r)
+ if rAuthType == authTypeStreamingSigned || rAuthType == authTypeStreamingSignedTrailer {
+ if sizeStr, ok := r.Header[xhttp.AmzDecodedContentLength]; ok {
+ if sizeStr[0] == "" {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL)
+ return
+ }
+ size, err = strconv.ParseInt(sizeStr[0], 10, 64)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ }
+ }
+
+ if size == -1 {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL)
+ return
+ }
+
+ // maximum Upload size for objects in a single operation
+ if isMaxObjectSize(size) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrEntityTooLarge), r.URL)
+ return
+ }
+
+ var (
+ md5hex = clientETag.String()
+ sha256hex = ""
+ reader io.Reader = r.Body
+ s3Err APIErrorCode
+ putObject = objectAPI.PutObject
+ )
+
+ var opts untarOptions
+ opts.ignoreDirs = strings.EqualFold(r.Header.Get(xhttp.MinIOSnowballIgnoreDirs), "true")
+ opts.ignoreErrs = strings.EqualFold(r.Header.Get(xhttp.MinIOSnowballIgnoreErrors), "true")
+ opts.prefixAll = r.Header.Get(xhttp.MinIOSnowballPrefix)
+ if opts.prefixAll != "" {
+ opts.prefixAll = trimLeadingSlash(pathJoin(opts.prefixAll, slashSeparator))
+ }
+ // Check if put is allow for specified prefix.
+ if s3Err = isPutActionAllowed(ctx, rAuthType, bucket, opts.prefixAll, r, policy.PutObjectAction); s3Err != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
+ return
+ }
+
+ switch rAuthType {
+ case authTypeStreamingSigned, authTypeStreamingSignedTrailer:
+ // Initialize stream signature verifier.
+ reader, s3Err = newSignV4ChunkedReader(r, rAuthType == authTypeStreamingSignedTrailer)
+ if s3Err != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
+ return
+ }
+ case authTypeSignedV2, authTypePresignedV2:
+ s3Err = isReqAuthenticatedV2(r)
+ if s3Err != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
+ return
+ }
+
+ case authTypePresigned, authTypeSigned:
+ if s3Err = reqSignatureV4Verify(r, globalSite.Region(), serviceS3); s3Err != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
+ return
+ }
+ if !skipContentSha256Cksum(r) {
+ sha256hex = getContentSha256Cksum(r, serviceS3)
+ }
+ }
+
+ hreader, err := hash.NewReader(ctx, reader, size, md5hex, sha256hex, size)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ if err = hreader.AddChecksum(r, false); err != nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidChecksum), r.URL)
+ return
+ }
+
+ if err := enforceBucketQuotaHard(ctx, bucket, size); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Check if bucket encryption is enabled
+ sseConfig, _ := globalBucketSSEConfigSys.Get(bucket)
+ sseConfig.Apply(r.Header, sse.ApplyOptions{
+ AutoEncrypt: globalAutoEncryption,
+ })
+
+ retPerms := isPutActionAllowed(ctx, getRequestAuthType(r), bucket, object, r, policy.PutObjectRetentionAction)
+ holdPerms := isPutActionAllowed(ctx, getRequestAuthType(r), bucket, object, r, policy.PutObjectLegalHoldAction)
+
+ getObjectInfo := objectAPI.GetObjectInfo
+
+ // These are static for all objects extracted.
+ reqParams := extractReqParams(r)
+ respElements := map[string]string{
+ "requestId": w.Header().Get(xhttp.AmzRequestID),
+ "nodeId": w.Header().Get(xhttp.AmzRequestHostID),
+ }
+ if sc == "" {
+ sc = storageclass.STANDARD
+ }
+
+ putObjectTar := func(reader io.Reader, info os.FileInfo, object string) error {
+ size := info.Size()
+ if s3Err = isPutActionAllowed(ctx, getRequestAuthType(r), bucket, object, r, policy.PutObjectAction); s3Err != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
+ return errors.New(errorCodes.ToAPIErr(s3Err).Code)
+ }
+ metadata := map[string]string{
+ xhttp.AmzStorageClass: sc, // save same storage-class as incoming stream.
+ }
+
+ actualSize := size
+ var idxCb func() []byte
+ if isCompressible(r.Header, object) && size > minCompressibleSize {
+ // Storing the compression metadata.
+ metadata[ReservedMetadataPrefix+"compression"] = compressionAlgorithmV2
+ metadata[ReservedMetadataPrefix+"actual-size"] = strconv.FormatInt(size, 10)
+
+ actualReader, err := hash.NewReader(ctx, reader, size, "", "", actualSize)
+ if err != nil {
+ return err
+ }
+
+ // Set compression metrics.
+ wantEncryption := crypto.Requested(r.Header)
+ s2c, cb := newS2CompressReader(actualReader, actualSize, wantEncryption)
+ defer s2c.Close()
+ idxCb = cb
+ reader = etag.Wrap(s2c, actualReader)
+ size = -1 // Since compressed size is un-predictable.
+ }
+
+ hashReader, err := hash.NewReader(ctx, reader, size, "", "", actualSize)
+ if err != nil {
+ return err
+ }
+
+ rawReader := hashReader
+ pReader := NewPutObjReader(rawReader)
+
+ if r.Header.Get(xhttp.AmzBucketReplicationStatus) == replication.Replica.String() {
+ if s3Err = isPutActionAllowed(ctx, getRequestAuthType(r), bucket, object, r, policy.ReplicateObjectAction); s3Err != ErrNone {
+ return errors.New(errorCodes.ToAPIErr(s3Err).Code)
+ }
+ metadata[ReservedMetadataPrefixLower+ReplicaStatus] = replication.Replica.String()
+ metadata[ReservedMetadataPrefixLower+ReplicaTimestamp] = UTCNow().Format(time.RFC3339Nano)
+ }
+
+ var (
+ versionID string
+ hdrs http.Header
+ )
+
+ if tarHdrs, ok := info.Sys().(*tar.Header); ok && len(tarHdrs.PAXRecords) > 0 {
+ versionID = tarHdrs.PAXRecords["minio.versionId"]
+ hdrs = make(http.Header)
+ for k, v := range tarHdrs.PAXRecords {
+ if k == "minio.versionId" {
+ continue
+ }
+ if after, ok0 := strings.CutPrefix(k, "minio.metadata."); ok0 {
+ k = after
+ hdrs.Set(k, v)
+ }
+ }
+ m, err := extractMetadata(ctx, textproto.MIMEHeader(hdrs))
+ if err != nil {
+ return err
+ }
+ maps.Copy(metadata, m)
+ } else {
+ versionID = r.Form.Get(xhttp.VersionID)
+ hdrs = r.Header
+ }
+
+ opts, err := putOpts(ctx, bucket, object, versionID, hdrs, metadata)
+ if err != nil {
+ return err
+ }
+
+ opts.MTime = info.ModTime()
+ if opts.MTime.Unix() <= 0 {
+ opts.MTime = UTCNow()
+ }
+ opts.IndexCB = idxCb
+
+ retentionMode, retentionDate, legalHold, s3err := checkPutObjectLockAllowed(ctx, r, bucket, object, getObjectInfo, retPerms, holdPerms)
+ if s3err == ErrNone && retentionMode.Valid() {
+ metadata[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode)
+ metadata[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = amztime.ISO8601Format(retentionDate.UTC())
+ }
+
+ if s3err == ErrNone && legalHold.Status.Valid() {
+ metadata[strings.ToLower(xhttp.AmzObjectLockLegalHold)] = string(legalHold.Status)
+ }
+
+ if s3err != ErrNone {
+ s3Err = s3err
+ return ObjectLocked{}
+ }
+
+ if dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(metadata, "", "", replication.ObjectReplicationType, opts)); dsc.ReplicateAny() {
+ metadata[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano)
+ metadata[ReservedMetadataPrefixLower+ReplicationStatus] = dsc.PendingStatus()
+ }
+
+ var objectEncryptionKey crypto.ObjectKey
+ if crypto.Requested(r.Header) {
+ if crypto.SSECopy.IsRequested(r.Header) {
+ return errInvalidEncryptionParameters
+ }
+
+ reader, objectEncryptionKey, err = EncryptRequest(hashReader, r, bucket, object, metadata)
+ if err != nil {
+ return err
+ }
+
+ wantSize := int64(-1)
+ if size >= 0 {
+ info := ObjectInfo{Size: size}
+ wantSize = info.EncryptedSize()
+ }
+
+ // do not try to verify encrypted content
+ hashReader, err = hash.NewReader(ctx, etag.Wrap(reader, hashReader), wantSize, "", "", actualSize)
+ if err != nil {
+ return err
+ }
+
+ pReader, err = pReader.WithEncryption(hashReader, &objectEncryptionKey)
+ if err != nil {
+ return err
+ }
+ }
+ if opts.IndexCB != nil {
+ opts.IndexCB = compressionIndexEncrypter(objectEncryptionKey, opts.IndexCB)
+ }
+
+ // Ensure that metadata does not contain sensitive information
+ crypto.RemoveSensitiveEntries(metadata)
+
+ os := newObjSweeper(bucket, object).WithVersioning(opts.Versioned, opts.VersionSuspended)
+ if !globalTierConfigMgr.Empty() {
+ // Get appropriate object info to identify the remote object to delete
+ goiOpts := os.GetOpts()
+ if goi, gerr := getObjectInfo(ctx, bucket, object, goiOpts); gerr == nil {
+ os.SetTransitionState(goi.TransitionedObject)
+ }
+ }
+
+ // Create the object..
+ objInfo, err := putObject(ctx, bucket, object, pReader, opts)
+ if err != nil {
+ return err
+ }
+
+ origETag := objInfo.ETag
+ objInfo.ETag = getDecryptedETag(r.Header, objInfo, false)
+
+ if dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(metadata, "", "", replication.ObjectReplicationType, opts)); dsc.ReplicateAny() {
+ scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.ObjectReplicationType)
+ }
+
+ // Notify object created event.
+ evt := eventArgs{
+ EventName: event.ObjectCreatedPut,
+ BucketName: bucket,
+ Object: objInfo,
+ ReqParams: reqParams,
+ RespElements: respElements,
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ }
+ sendEvent(evt)
+
+ // Remove the transitioned object whose object version is being overwritten.
+ if !globalTierConfigMgr.Empty() {
+ objInfo.ETag = origETag
+ // Schedule object for immediate transition if eligible.
+ enqueueTransitionImmediate(objInfo, lcEventSrc_s3PutObject)
+ os.Sweep()
+ }
+
+ return nil
+ }
+
+ if err = untar(ctx, hreader, putObjectTar, opts); err != nil {
+ apiErr := errorCodes.ToAPIErr(s3Err)
+ // If not set, convert or use BadRequest
+ if s3Err == ErrNone {
+ apiErr = toAPIError(ctx, err)
+ if apiErr.Code == "InternalError" {
+ // Convert generic internal errors to bad requests.
+ apiErr = APIError{
+ Code: "BadRequest",
+ Description: err.Error(),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ }
+ }
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+
+ w.Header()[xhttp.ETag] = []string{`"` + hex.EncodeToString(hreader.MD5Current()) + `"`}
+ hash.TransferChecksumHeader(w, r)
+ writeSuccessResponseHeadersOnly(w)
+}
+
+// Delete objectAPIHandlers
+
+// DeleteObjectHandler - delete an object
+func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "DeleteObject")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+ object, err := unescapePath(vars["object"])
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ if s3Error := checkRequestAuthType(ctx, r, policy.DeleteObjectAction, bucket, object); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+ if _, ok := r.Header[xhttp.MinIOSourceReplicationCheck]; ok {
+ // requests to just validate replication settings and permissions are not allowed to delete data
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationPermissionCheckError), r.URL)
+ return
+ }
+
+ replica := r.Header.Get(xhttp.AmzBucketReplicationStatus) == replication.Replica.String()
+ if replica {
+ if s3Error := checkRequestAuthType(ctx, r, policy.ReplicateDeleteAction, bucket, object); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+ }
+
+ if globalDNSConfig != nil {
+ _, err := globalDNSConfig.Get(bucket)
+ if err != nil && err != dns.ErrNotImplemented {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ }
+
+ opts, err := delOpts(ctx, r, bucket, object)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ rcfg, _ := globalBucketObjectLockSys.Get(bucket)
+ if rcfg.LockEnabled && opts.DeletePrefix {
+ apiErr := toAPIError(ctx, errInvalidArgument)
+ apiErr.Description = "force-delete is forbidden on Object Locking enabled buckets"
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+
+ os := newObjSweeper(bucket, object).WithVersion(opts.VersionID).WithVersioning(opts.Versioned, opts.VersionSuspended)
+
+ opts.SetEvalMetadataFn(func(oi *ObjectInfo, gerr error) (dsc ReplicateDecision, err error) {
+ if replica { // no need to check replication on receiver
+ return dsc, nil
+ }
+ dsc = checkReplicateDelete(ctx, bucket, ObjectToDelete{
+ ObjectV: ObjectV{
+ ObjectName: object,
+ VersionID: opts.VersionID,
+ },
+ }, *oi, opts, gerr)
+ // Mutations of objects on versioning suspended buckets
+ // affect its null version. Through opts below we select
+ // the null version's remote object to delete if
+ // transitioned.
+ if gerr == nil {
+ os.SetTransitionState(oi.TransitionedObject)
+ }
+ return dsc, nil
+ })
+
+ vID := opts.VersionID
+ if replica {
+ opts.SetReplicaStatus(replication.Replica)
+ if opts.VersionPurgeStatus().Empty() {
+ // opts.VersionID holds delete marker version ID to replicate and not yet present on disk
+ vID = ""
+ }
+ }
+ opts.SetEvalRetentionBypassFn(func(goi ObjectInfo, gerr error) (err error) {
+ err = nil
+ if vID != "" {
+ err := enforceRetentionBypassForDelete(ctx, r, bucket, ObjectToDelete{
+ ObjectV: ObjectV{
+ ObjectName: object,
+ VersionID: vID,
+ },
+ }, goi, gerr)
+ if err != nil && !isErrObjectNotFound(err) {
+ return err
+ }
+ }
+ return err
+ })
+
+ deleteObject := objectAPI.DeleteObject
+
+ // http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectDELETE.html
+ objInfo, err := deleteObject(ctx, bucket, object, opts)
+ if err != nil {
+ if _, ok := err.(BucketNotFound); ok {
+ // When bucket doesn't exist specially handle it.
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
+ // Send an event when the object is not found
+ objInfo.Name = object
+ objInfo.VersionID = opts.VersionID
+ sendEvent(eventArgs{
+ EventName: event.ObjectRemovedNoOP,
+ BucketName: bucket,
+ Object: objInfo,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+ writeSuccessNoContent(w)
+ return
+ }
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if objInfo.Name == "" {
+ writeSuccessNoContent(w)
+ return
+ }
+
+ setPutObjHeaders(w, objInfo, true, r.Header)
+ writeSuccessNoContent(w)
+
+ eventName := event.ObjectRemovedDelete
+ if objInfo.DeleteMarker {
+ eventName = event.ObjectRemovedDeleteMarkerCreated
+ }
+
+ // Notify object deleted event.
+ sendEvent(eventArgs{
+ EventName: eventName,
+ BucketName: bucket,
+ Object: objInfo,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+
+ if objInfo.ReplicationStatus == replication.Pending || objInfo.VersionPurgeStatus == replication.VersionPurgePending {
+ dmVersionID := ""
+ versionID := ""
+ if objInfo.DeleteMarker {
+ dmVersionID = objInfo.VersionID
+ } else {
+ versionID = objInfo.VersionID
+ }
+ dobj := DeletedObjectReplicationInfo{
+ DeletedObject: DeletedObject{
+ ObjectName: object,
+ VersionID: versionID,
+ DeleteMarkerVersionID: dmVersionID,
+ DeleteMarkerMTime: DeleteMarkerMTime{objInfo.ModTime},
+ DeleteMarker: objInfo.DeleteMarker,
+ ReplicationState: objInfo.ReplicationState(),
+ },
+ Bucket: bucket,
+ EventType: ReplicateIncomingDelete,
+ }
+ scheduleReplicationDelete(ctx, dobj, objectAPI)
+ }
+
+ // Remove the transitioned object whose object version is being overwritten.
+ if !globalTierConfigMgr.Empty() {
+ os.Sweep()
+ }
+}
+
+// PutObjectLegalHoldHandler - set legal hold configuration to object,
+func (api objectAPIHandlers) PutObjectLegalHoldHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "PutObjectLegalHold")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+ object, err := unescapePath(vars["object"])
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ // Check permissions to perform this legal hold operation
+ if s3Err := checkRequestAuthType(ctx, r, policy.PutObjectLegalHoldAction, bucket, object); s3Err != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
+ return
+ }
+
+ if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ if !validateLengthAndChecksum(r) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL)
+ return
+ }
+
+ if rcfg, _ := globalBucketObjectLockSys.Get(bucket); !rcfg.LockEnabled {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidBucketObjectLockConfiguration), r.URL)
+ return
+ }
+
+ legalHold, err := objectlock.ParseObjectLegalHold(r.Body)
+ if err != nil {
+ apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
+ apiErr.Description = err.Error()
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+
+ opts, err := getOpts(ctx, r, bucket, object)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ popts := ObjectOptions{
+ MTime: opts.MTime,
+ VersionID: opts.VersionID,
+ EvalMetadataFn: func(oi *ObjectInfo, gerr error) (ReplicateDecision, error) {
+ oi.UserDefined[strings.ToLower(xhttp.AmzObjectLockLegalHold)] = strings.ToUpper(string(legalHold.Status))
+ oi.UserDefined[ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp] = UTCNow().Format(time.RFC3339Nano)
+
+ dsc := mustReplicate(ctx, bucket, object, oi.getMustReplicateOptions(replication.MetadataReplicationType, opts))
+ if dsc.ReplicateAny() {
+ oi.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano)
+ oi.UserDefined[ReservedMetadataPrefixLower+ReplicationStatus] = dsc.PendingStatus()
+ }
+ return dsc, nil
+ },
+ }
+
+ objInfo, err := objectAPI.PutObjectMetadata(ctx, bucket, object, popts)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ dsc := mustReplicate(ctx, bucket, object, objInfo.getMustReplicateOptions(replication.MetadataReplicationType, opts))
+ if dsc.ReplicateAny() {
+ scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.MetadataReplicationType)
+ }
+
+ writeSuccessResponseHeadersOnly(w)
+
+ // Notify object event.
+ sendEvent(eventArgs{
+ EventName: event.ObjectCreatedPutLegalHold,
+ BucketName: bucket,
+ Object: objInfo,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+}
+
+// GetObjectLegalHoldHandler - get legal hold configuration to object,
+func (api objectAPIHandlers) GetObjectLegalHoldHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetObjectLegalHold")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+ object, err := unescapePath(vars["object"])
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetObjectLegalHoldAction, bucket, object); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ getObjectInfo := objectAPI.GetObjectInfo
+
+ if rcfg, _ := globalBucketObjectLockSys.Get(bucket); !rcfg.LockEnabled {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidBucketObjectLockConfiguration), r.URL)
+ return
+ }
+
+ opts, err := getOpts(ctx, r, bucket, object)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ objInfo, err := getObjectInfo(ctx, bucket, object, opts)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ legalHold := objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined)
+ if legalHold.IsEmpty() {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNoSuchObjectLockConfiguration), r.URL)
+ return
+ }
+
+ writeSuccessResponseXML(w, encodeResponse(legalHold))
+ // Notify object legal hold accessed via a GET request.
+ sendEvent(eventArgs{
+ EventName: event.ObjectAccessedGetLegalHold,
+ BucketName: bucket,
+ Object: objInfo,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+}
+
+// PutObjectRetentionHandler - set object hold configuration to object,
+func (api objectAPIHandlers) PutObjectRetentionHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "PutObjectRetention")
+
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+ object, err := unescapePath(vars["object"])
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ // Check permissions to perform this object retention operation
+ if s3Error := authenticateRequest(ctx, r, policy.PutObjectRetentionAction); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if !validateLengthAndChecksum(r) {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL)
+ return
+ }
+
+ if rcfg, _ := globalBucketObjectLockSys.Get(bucket); !rcfg.LockEnabled {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidBucketObjectLockConfiguration), r.URL)
+ return
+ }
+
+ objRetention, err := objectlock.ParseObjectRetention(r.Body)
+ if err != nil {
+ apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
+ apiErr.Description = err.Error()
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+
+ reqInfo := logger.GetReqInfo(ctx)
+ reqInfo.SetTags("retention", objRetention.String())
+
+ opts, err := getOpts(ctx, r, bucket, object)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ popts := ObjectOptions{
+ MTime: opts.MTime,
+ VersionID: opts.VersionID,
+ EvalMetadataFn: func(oi *ObjectInfo, gerr error) (dsc ReplicateDecision, err error) {
+ if err := enforceRetentionBypassForPut(ctx, r, *oi, objRetention, reqInfo.Cred, reqInfo.Owner); err != nil {
+ return dsc, err
+ }
+ if objRetention.Mode.Valid() {
+ oi.UserDefined[strings.ToLower(xhttp.AmzObjectLockMode)] = string(objRetention.Mode)
+ oi.UserDefined[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = amztime.ISO8601Format(objRetention.RetainUntilDate.UTC())
+ } else {
+ oi.UserDefined[strings.ToLower(xhttp.AmzObjectLockMode)] = ""
+ oi.UserDefined[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = ""
+ }
+ oi.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp] = UTCNow().Format(time.RFC3339Nano)
+ dsc = mustReplicate(ctx, bucket, object, oi.getMustReplicateOptions(replication.MetadataReplicationType, opts))
+ if dsc.ReplicateAny() {
+ oi.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano)
+ oi.UserDefined[ReservedMetadataPrefixLower+ReplicationStatus] = dsc.PendingStatus()
+ }
+ return dsc, nil
+ },
+ }
+
+ objInfo, err := objectAPI.PutObjectMetadata(ctx, bucket, object, popts)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ dsc := mustReplicate(ctx, bucket, object, objInfo.getMustReplicateOptions(replication.MetadataReplicationType, opts))
+ if dsc.ReplicateAny() {
+ scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.MetadataReplicationType)
+ }
+
+ writeSuccessResponseHeadersOnly(w)
+
+ // Notify object event.
+ sendEvent(eventArgs{
+ EventName: event.ObjectCreatedPutRetention,
+ BucketName: bucket,
+ Object: objInfo,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+}
+
+// GetObjectRetentionHandler - get object retention configuration of object,
+func (api objectAPIHandlers) GetObjectRetentionHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetObjectRetention")
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+ object, err := unescapePath(vars["object"])
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+ if s3Error := checkRequestAuthType(ctx, r, policy.GetObjectRetentionAction, bucket, object); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ getObjectInfo := objectAPI.GetObjectInfo
+
+ if rcfg, _ := globalBucketObjectLockSys.Get(bucket); !rcfg.LockEnabled {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidBucketObjectLockConfiguration), r.URL)
+ return
+ }
+
+ opts, err := getOpts(ctx, r, bucket, object)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ objInfo, err := getObjectInfo(ctx, bucket, object, opts)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ retention := objectlock.GetObjectRetentionMeta(objInfo.UserDefined)
+ if !retention.Mode.Valid() {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNoSuchObjectLockConfiguration), r.URL)
+ return
+ }
+
+ writeSuccessResponseXML(w, encodeResponse(retention))
+ // Notify object retention accessed via a GET request.
+ sendEvent(eventArgs{
+ EventName: event.ObjectAccessedGetRetention,
+ BucketName: bucket,
+ Object: objInfo,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+}
+
+// ObjectTagSet key value tags
+type ObjectTagSet struct {
+ Tags []tags.Tag `xml:"Tag"`
+}
+
+type objectTagging struct {
+ XMLName xml.Name `xml:"Tagging"`
+ TagSet *ObjectTagSet `xml:"TagSet"`
+}
+
+// GetObjectTaggingHandler - GET object tagging
+func (api objectAPIHandlers) GetObjectTaggingHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "GetObjectTagging")
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+ object, err := unescapePath(vars["object"])
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ if s3Error := authenticateRequest(ctx, r, policy.GetObjectTaggingAction); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ opts, err := getOpts(ctx, r, bucket, object)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ ot, err := objAPI.GetObjectTags(ctx, bucket, object, opts)
+ if err != nil {
+ // if object/version is not found locally, but exists on peer site - proxy
+ // the tagging request to peer site. The response to client will
+ // return tags from peer site.
+ if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
+ proxytgts := getProxyTargets(ctx, bucket, object, opts)
+ if !proxytgts.Empty() {
+ globalReplicationStats.Load().incProxy(bucket, getObjectTaggingAPI, false)
+ // proxy to replication target if site replication is in place.
+ tags, gerr := proxyGetTaggingToRepTarget(ctx, bucket, object, opts, proxytgts)
+ if gerr.Err != nil || tags == nil {
+ globalReplicationStats.Load().incProxy(bucket, getObjectTaggingAPI, true)
+ writeErrorResponse(ctx, w, toAPIError(ctx, gerr.Err), r.URL)
+ return
+ } // overlay tags from peer site.
+ ot = tags
+ w.Header()[xhttp.MinIOTaggingProxied] = []string{"true"} // indicate that the request was proxied.
+ } else {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ } else {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ }
+
+ // Set this such that authorization policies can be applied on the object tags.
+ if tags := ot.String(); tags != "" {
+ r.Header.Set(xhttp.AmzObjectTagging, tags)
+ }
+
+ if s3Error := authorizeRequest(ctx, r, policy.GetObjectTaggingAction); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ if opts.VersionID != "" && opts.VersionID != nullVersionID {
+ w.Header()[xhttp.AmzVersionID] = []string{opts.VersionID}
+ }
+
+ otags := &objectTagging{
+ TagSet: &ObjectTagSet{},
+ }
+
+ var list []tags.Tag
+ for k, v := range ot.ToMap() {
+ list = append(list, tags.Tag{
+ Key: k,
+ Value: v,
+ })
+ }
+ // Always return in sorted order for tags.
+ sort.Slice(list, func(i, j int) bool {
+ return list[i].Key < list[j].Key
+ })
+ otags.TagSet.Tags = list
+
+ writeSuccessResponseXML(w, encodeResponse(otags))
+}
+
+// PutObjectTaggingHandler - PUT object tagging
+func (api objectAPIHandlers) PutObjectTaggingHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "PutObjectTagging")
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+ object, err := unescapePath(vars["object"])
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ // Tags XML will not be bigger than 1MiB in size, fail if its bigger.
+ tags, err := tags.ParseObjectXML(io.LimitReader(r.Body, 1<<20))
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Set this such that authorization policies can be applied on the object tags.
+ r.Header.Set(xhttp.AmzObjectTagging, tags.String())
+
+ // Allow putObjectTagging if policy action is set
+ if s3Error := checkRequestAuthType(ctx, r, policy.PutObjectTaggingAction, bucket, object); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ opts, err := getOpts(ctx, r, bucket, object)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ objInfo, err := objAPI.GetObjectInfo(ctx, bucket, object, opts)
+ if err != nil {
+ // if object is not found locally, but exists on peer site - proxy
+ // the tagging request to peer site. The response to client will
+ // be 200 with extra header indicating that the request was proxied.
+ if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
+ proxytgts := getProxyTargets(ctx, bucket, object, opts)
+ if !proxytgts.Empty() {
+ globalReplicationStats.Load().incProxy(bucket, putObjectTaggingAPI, false)
+ // proxy to replication target if site replication is in place.
+ perr := proxyTaggingToRepTarget(ctx, bucket, object, tags, opts, proxytgts)
+ if perr.Err != nil {
+ globalReplicationStats.Load().incProxy(bucket, putObjectTaggingAPI, true)
+ writeErrorResponse(ctx, w, toAPIError(ctx, perr.Err), r.URL)
+ return
+ }
+ w.Header()[xhttp.MinIOTaggingProxied] = []string{"true"}
+ writeSuccessResponseHeadersOnly(w)
+ // when tagging is proxied, the object version is not available to return
+ // as header in the response, or ObjectInfo in the notification event.
+ sendEvent(eventArgs{
+ EventName: event.ObjectCreatedPutTagging,
+ BucketName: bucket,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+ return
+ }
+ }
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+ tagsStr := tags.String()
+
+ dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(objInfo.UserDefined, tagsStr, objInfo.ReplicationStatus, replication.MetadataReplicationType, opts))
+ if dsc.ReplicateAny() {
+ opts.UserDefined = make(map[string]string)
+ opts.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano)
+ opts.UserDefined[ReservedMetadataPrefixLower+ReplicationStatus] = dsc.PendingStatus()
+ opts.UserDefined[ReservedMetadataPrefixLower+TaggingTimestamp] = UTCNow().Format(time.RFC3339Nano)
+ }
+
+ // Put object tags
+ objInfo, err = objAPI.PutObjectTags(ctx, bucket, object, tagsStr, opts)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if dsc.ReplicateAny() {
+ scheduleReplication(ctx, objInfo, objAPI, dsc, replication.MetadataReplicationType)
+ }
+
+ if objInfo.VersionID != "" && objInfo.VersionID != nullVersionID {
+ w.Header()[xhttp.AmzVersionID] = []string{objInfo.VersionID}
+ }
+
+ writeSuccessResponseHeadersOnly(w)
+
+ sendEvent(eventArgs{
+ EventName: event.ObjectCreatedPutTagging,
+ BucketName: bucket,
+ Object: objInfo,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+}
+
+// DeleteObjectTaggingHandler - DELETE object tagging
+func (api objectAPIHandlers) DeleteObjectTaggingHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "DeleteObjectTagging")
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+
+ objAPI := api.ObjectAPI()
+ if objAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+ object, err := unescapePath(vars["object"])
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ opts, err := getOpts(ctx, r, bucket, object)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ oi, err := objAPI.GetObjectInfo(ctx, bucket, object, opts)
+ if err != nil {
+ // if object is not found locally, but exists on peer site - proxy
+ // the tagging request to peer site. The response to client will
+ // be 200 OK with extra header indicating that the request was proxied.
+ if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
+ proxytgts := getProxyTargets(ctx, bucket, object, opts)
+ if !proxytgts.Empty() {
+ globalReplicationStats.Load().incProxy(bucket, removeObjectTaggingAPI, false)
+ // proxy to replication target if active-active replication is in place.
+ perr := proxyTaggingToRepTarget(ctx, bucket, object, nil, opts, proxytgts)
+ if perr.Err != nil {
+ globalReplicationStats.Load().incProxy(bucket, removeObjectTaggingAPI, true)
+ writeErrorResponse(ctx, w, toAPIError(ctx, perr.Err), r.URL)
+ return
+ }
+ // when delete tagging is proxied, the object version/tags are not available to return
+ // as header in the response, nor ObjectInfo in the notification event.
+ w.Header()[xhttp.MinIOTaggingProxied] = []string{"true"}
+ writeSuccessNoContent(w)
+ sendEvent(eventArgs{
+ EventName: event.ObjectCreatedDeleteTagging,
+ BucketName: bucket,
+ Object: oi,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+ return
+ }
+ }
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if userTags := oi.UserTags; userTags != "" {
+ // Set this such that authorization policies can be applied on the object tags.
+ r.Header.Set(xhttp.AmzObjectTagging, oi.UserTags)
+ }
+
+ // Allow deleteObjectTagging if policy action is set
+ if s3Error := checkRequestAuthType(ctx, r, policy.DeleteObjectTaggingAction, bucket, object); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ dsc := mustReplicate(ctx, bucket, object, oi.getMustReplicateOptions(replication.MetadataReplicationType, opts))
+ if dsc.ReplicateAny() {
+ opts.UserDefined = make(map[string]string)
+ opts.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano)
+ opts.UserDefined[ReservedMetadataPrefixLower+ReplicationStatus] = dsc.PendingStatus()
+ }
+
+ oi, err = objAPI.DeleteObjectTags(ctx, bucket, object, opts)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if dsc.ReplicateAny() {
+ scheduleReplication(ctx, oi, objAPI, dsc, replication.MetadataReplicationType)
+ }
+
+ if oi.VersionID != "" && oi.VersionID != nullVersionID {
+ w.Header()[xhttp.AmzVersionID] = []string{oi.VersionID}
+ }
+ writeSuccessNoContent(w)
+
+ sendEvent(eventArgs{
+ EventName: event.ObjectCreatedDeleteTagging,
+ BucketName: bucket,
+ Object: oi,
+ ReqParams: extractReqParams(r),
+ RespElements: extractRespElements(w),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+}
+
+// RestoreObjectHandler - POST restore object handler.
+// ----------
+func (api objectAPIHandlers) PostRestoreObjectHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := newContext(r, w, "PostRestoreObject")
+ defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
+ vars := mux.Vars(r)
+ bucket := vars["bucket"]
+ object, err := unescapePath(vars["object"])
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ // Fetch object stat info.
+ objectAPI := api.ObjectAPI()
+ if objectAPI == nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
+ return
+ }
+
+ getObjectInfo := objectAPI.GetObjectInfo
+
+ // Check for auth type to return S3 compatible error.
+ if s3Error := checkRequestAuthType(ctx, r, policy.RestoreObjectAction, bucket, object); s3Error != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
+ return
+ }
+
+ if r.ContentLength <= 0 {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrEmptyRequestBody), r.URL)
+ return
+ }
+ opts, err := postRestoreOpts(ctx, r, bucket, object)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ objInfo, err := getObjectInfo(ctx, bucket, object, opts)
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ if objInfo.TransitionedObject.Status != lifecycle.TransitionComplete {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidObjectState), r.URL)
+ return
+ }
+
+ rreq, err := parseRestoreRequest(io.LimitReader(r.Body, r.ContentLength))
+ if err != nil {
+ apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
+ apiErr.Description = err.Error()
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+ // validate the request
+ if err := rreq.validate(ctx, objectAPI); err != nil {
+ apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
+ apiErr.Description = err.Error()
+ writeErrorResponse(ctx, w, apiErr, r.URL)
+ return
+ }
+ statusCode := http.StatusOK
+ alreadyRestored := false
+ if err == nil {
+ if objInfo.RestoreOngoing && rreq.Type != SelectRestoreRequest {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrObjectRestoreAlreadyInProgress), r.URL)
+ return
+ }
+ if !objInfo.RestoreOngoing && !objInfo.RestoreExpires.IsZero() {
+ statusCode = http.StatusAccepted
+ alreadyRestored = true
+ }
+ }
+ // set or upgrade restore expiry
+ restoreExpiry := lifecycle.ExpectedExpiryTime(time.Now().UTC(), rreq.Days)
+ metadata := cloneMSS(objInfo.UserDefined)
+
+ // update self with restore metadata
+ if rreq.Type != SelectRestoreRequest {
+ objInfo.metadataOnly = true // Perform only metadata updates.
+ metadata[xhttp.AmzRestoreExpiryDays] = strconv.Itoa(rreq.Days)
+ metadata[xhttp.AmzRestoreRequestDate] = time.Now().UTC().Format(http.TimeFormat)
+ if alreadyRestored {
+ metadata[xhttp.AmzRestore] = completedRestoreObj(restoreExpiry).String()
+ } else {
+ metadata[xhttp.AmzRestore] = ongoingRestoreObj().String()
+ }
+ objInfo.UserDefined = metadata
+ if _, err := objectAPI.CopyObject(GlobalContext, bucket, object, bucket, object, objInfo, ObjectOptions{
+ VersionID: objInfo.VersionID,
+ }, ObjectOptions{
+ VersionID: objInfo.VersionID,
+ MTime: objInfo.ModTime,
+ }); err != nil {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidObjectState), r.URL)
+ return
+ }
+ // for previously restored object, just update the restore expiry
+ if alreadyRestored {
+ return
+ }
+ }
+
+ restoreObject := mustGetUUID()
+ if rreq.OutputLocation.S3.BucketName != "" {
+ w.Header()[xhttp.AmzRestoreOutputPath] = []string{pathJoin(rreq.OutputLocation.S3.BucketName, rreq.OutputLocation.S3.Prefix, restoreObject)}
+ }
+ w.WriteHeader(statusCode)
+ // Notify object restore started via a POST request.
+ sendEvent(eventArgs{
+ EventName: event.ObjectRestorePost,
+ BucketName: bucket,
+ Object: objInfo,
+ ReqParams: extractReqParams(r),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+ // now process the restore in background
+ go func() {
+ rctx := GlobalContext
+ if !rreq.SelectParameters.IsEmpty() {
+ actualSize, err := objInfo.GetActualSize()
+ if err != nil {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ return
+ }
+
+ objectRSC := s3select.NewObjectReadSeekCloser(
+ func(offset int64) (io.ReadCloser, error) {
+ rs := &HTTPRangeSpec{
+ IsSuffixLength: false,
+ Start: offset,
+ End: -1,
+ }
+ return getTransitionedObjectReader(rctx, bucket, object, rs, r.Header,
+ objInfo, ObjectOptions{VersionID: objInfo.VersionID})
+ },
+ actualSize,
+ )
+ defer objectRSC.Close()
+ if err = rreq.SelectParameters.Open(objectRSC); err != nil {
+ if serr, ok := err.(s3select.SelectError); ok {
+ encodedErrorResponse := encodeResponse(APIErrorResponse{
+ Code: serr.ErrorCode(),
+ Message: serr.ErrorMessage(),
+ BucketName: bucket,
+ Key: object,
+ Resource: r.URL.Path,
+ RequestID: w.Header().Get(xhttp.AmzRequestID),
+ HostID: globalDeploymentID(),
+ })
+ writeResponse(w, serr.HTTPStatusCode(), encodedErrorResponse, mimeXML)
+ } else {
+ writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
+ }
+ return
+ }
+ nr := httptest.NewRecorder()
+ rw := xhttp.NewResponseRecorder(nr)
+ rw.LogErrBody = true
+ rw.LogAllBody = true
+ rreq.SelectParameters.Evaluate(rw)
+ rreq.SelectParameters.Close()
+ return
+ }
+ opts := ObjectOptions{
+ Transition: TransitionOptions{
+ RestoreRequest: rreq,
+ RestoreExpiry: restoreExpiry,
+ },
+ VersionID: objInfo.VersionID,
+ }
+ if err := objectAPI.RestoreTransitionedObject(rctx, bucket, object, opts); err != nil {
+ s3LogIf(ctx, fmt.Errorf("Unable to restore transitioned bucket/object %s/%s: %w", bucket, object, err))
+ return
+ }
+
+ // Notify object restore completed via a POST request.
+ sendEvent(eventArgs{
+ EventName: event.ObjectRestoreCompleted,
+ BucketName: bucket,
+ Object: objInfo,
+ ReqParams: extractReqParams(r),
+ UserAgent: r.UserAgent(),
+ Host: handlers.GetSourceIP(r),
+ })
+ }()
+}
diff --git a/cmd/object-handlers_test.go b/cmd/object-handlers_test.go
new file mode 100644
index 00000000..ec71c692
--- /dev/null
+++ b/cmd/object-handlers_test.go
@@ -0,0 +1,4249 @@
+// Copyright (c) 2015-2021 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "crypto/md5"
+ "crypto/sha1"
+ "encoding/base64"
+ "encoding/hex"
+ "encoding/xml"
+ "fmt"
+ "hash"
+ "hash/crc32"
+ "io"
+ "maps"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "path"
+ "runtime"
+ "strconv"
+ "strings"
+ "sync"
+ "testing"
+
+ "github.com/dustin/go-humanize"
+ "github.com/minio/minio/internal/auth"
+ "github.com/minio/minio/internal/hash/sha256"
+ xhttp "github.com/minio/minio/internal/http"
+ ioutilx "github.com/minio/minio/internal/ioutil"
+)
+
+// Type to capture different modifications to API request to simulate failure cases.
+type Fault int
+
+const (
+ None Fault = iota
+ MissingContentLength
+ TooBigObject
+ TooBigDecodedLength
+ BadSignature
+ BadMD5
+ MissingUploadID
+)
+
+// Wrapper for calling HeadObject API handler tests for both Erasure multiple disks and FS single drive setup.
+func TestAPIHeadObjectHandler(t *testing.T) {
+ ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testAPIHeadObjectHandler, endpoints: []string{"HeadObject"}})
+}
+
+func testAPIHeadObjectHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ credentials auth.Credentials, t *testing.T,
+) {
+ objectName := "test-object"
+ // set of byte data for PutObject.
+ // object has to be created before running tests for HeadObject.
+ // this is required even to assert the HeadObject data,
+ // since dataInserted === dataFetched back is a primary criteria for any object storage this assertion is critical.
+ bytesData := []struct {
+ byteData []byte
+ }{
+ {generateBytesData(6 * humanize.MiByte)},
+ }
+ // set of inputs for uploading the objects before tests for downloading is done.
+ putObjectInputs := []struct {
+ bucketName string
+ objectName string
+ contentLength int64
+ textData []byte
+ metaData map[string]string
+ }{
+ {bucketName, objectName, int64(len(bytesData[0].byteData)), bytesData[0].byteData, make(map[string]string)},
+ }
+ // iterate through the above set of inputs and upload the object.
+ for i, input := range putObjectInputs {
+ // uploading the object.
+ _, err := obj.PutObject(context.Background(), input.bucketName, input.objectName, mustGetPutObjReader(t, bytes.NewReader(input.textData), input.contentLength, input.metaData[""], ""), ObjectOptions{UserDefined: input.metaData})
+ // if object upload fails stop the test.
+ if err != nil {
+ t.Fatalf("Put Object case %d: Error uploading object: %v", i+1, err)
+ }
+ }
+
+ // test cases with inputs and expected result for HeadObject.
+ testCases := []struct {
+ bucketName string
+ objectName string
+ accessKey string
+ secretKey string
+ // expected output.
+ expectedRespStatus int // expected response status body.
+ }{
+ // Test case - 1.
+ // Fetching stat info of object and validating it.
+ {
+ bucketName: bucketName,
+ objectName: objectName,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusOK,
+ },
+ // Test case - 2.
+ // Case with non-existent object name.
+ {
+ bucketName: bucketName,
+ objectName: "abcd",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusNotFound,
+ },
+ // Test case - 3.
+ // Test case to induce a signature mismatch.
+ // Using invalid accessID.
+ {
+ bucketName: bucketName,
+ objectName: objectName,
+ accessKey: "Invalid-AccessID",
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusForbidden,
+ },
+ }
+
+ // Iterating over the cases, fetching the object validating the response.
+ for i, testCase := range testCases {
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ rec := httptest.NewRecorder()
+ // construct HTTP request for Get Object end point.
+ req, err := newTestSignedRequestV4(http.MethodHead, getHeadObjectURL("", testCase.bucketName, testCase.objectName),
+ 0, nil, testCase.accessKey, testCase.secretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed to create HTTP request for Head Object: %v", i+1, instanceType, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler,`func (api objectAPIHandlers) GetObjectHandler` handles the request.
+ apiRouter.ServeHTTP(rec, req)
+
+ // Assert the response code with the expected status.
+ if rec.Code != testCase.expectedRespStatus {
+ t.Fatalf("Case %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testCase.expectedRespStatus, rec.Code)
+ }
+
+ // Verify response of the V2 signed HTTP request.
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ recV2 := httptest.NewRecorder()
+ // construct HTTP request for Head Object endpoint.
+ reqV2, err := newTestSignedRequestV2(http.MethodHead, getHeadObjectURL("", testCase.bucketName, testCase.objectName),
+ 0, nil, testCase.accessKey, testCase.secretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed to create HTTP request for Head Object: %v", i+1, instanceType, err)
+ }
+
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler.
+ apiRouter.ServeHTTP(recV2, reqV2)
+ 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)
+ }
+ }
+
+ // Test for Anonymous/unsigned http request.
+ anonReq, err := newTestRequest(http.MethodHead, getHeadObjectURL("", bucketName, objectName), 0, nil)
+ if err != nil {
+ t.Fatalf("MinIO %s: Failed to create an anonymous request for %s/%s: %v",
+ instanceType, bucketName, objectName, err)
+ }
+
+ // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
+ // sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
+ // unsigned request goes through and its validated again.
+ ExecObjectLayerAPIAnonTest(t, obj, "TestAPIHeadObjectHandler", bucketName, objectName, instanceType, apiRouter, anonReq, getAnonReadOnlyObjectPolicy(bucketName, objectName))
+
+ // HTTP request for testing when `objectLayer` is set to `nil`.
+ // There is no need to use an existing bucket and valid input for creating the request
+ // since the `objectLayer==nil` check is performed before any other checks inside the handlers.
+ // The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
+
+ nilBucket := "dummy-bucket"
+ nilObject := "dummy-object"
+ nilReq, err := newTestSignedRequestV4(http.MethodHead, getGetObjectURL("", nilBucket, nilObject),
+ 0, nil, "", "", nil)
+ if err != nil {
+ t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
+ }
+ // execute the object layer set to `nil` test.
+ // `ExecObjectLayerAPINilTest` manages the operation.
+ ExecObjectLayerAPINilTest(t, nilBucket, nilObject, instanceType, apiRouter, nilReq)
+}
+
+func TestAPIHeadObjectHandlerWithEncryption(t *testing.T) {
+ globalPolicySys = NewPolicySys()
+ defer func() { globalPolicySys = nil }()
+
+ defer DetectTestLeak(t)()
+ ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testAPIHeadObjectHandlerWithEncryption, endpoints: []string{"NewMultipart", "PutObjectPart", "CompleteMultipart", "GetObject", "PutObject", "HeadObject"}})
+}
+
+func testAPIHeadObjectHandlerWithEncryption(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ credentials auth.Credentials, t *testing.T,
+) {
+ // Set SSL to on to do encryption tests
+ globalIsTLS = true
+ defer func() { globalIsTLS = false }()
+
+ var (
+ oneMiB int64 = 1024 * 1024
+ key32Bytes = generateBytesData(32 * humanize.Byte)
+ key32BytesMd5 = md5.Sum(key32Bytes)
+ metaWithSSEC = map[string]string{
+ xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES,
+ xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key32Bytes),
+ xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(key32BytesMd5[:]),
+ }
+ mapCopy = func(m map[string]string) map[string]string {
+ r := make(map[string]string, len(m))
+ maps.Copy(r, m)
+ return r
+ }
+ )
+
+ type ObjectInput struct {
+ objectName string
+ partLengths []int64
+
+ metaData map[string]string
+ }
+
+ objectLength := func(oi ObjectInput) (sum int64) {
+ for _, l := range oi.partLengths {
+ sum += l
+ }
+ return sum
+ }
+
+ // set of inputs for uploading the objects before tests for
+ // downloading is done. Data bytes are from DummyDataGen.
+ objectInputs := []ObjectInput{
+ // Unencrypted objects
+ {"nothing", []int64{0}, nil},
+ {"small-1", []int64{509}, nil},
+
+ {"mp-1", []int64{5 * oneMiB, 1}, nil},
+ {"mp-2", []int64{5487701, 5487799, 3}, nil},
+
+ // Encrypted object
+ {"enc-nothing", []int64{0}, mapCopy(metaWithSSEC)},
+ {"enc-small-1", []int64{509}, mapCopy(metaWithSSEC)},
+
+ {"enc-mp-1", []int64{5 * oneMiB, 1}, mapCopy(metaWithSSEC)},
+ {"enc-mp-2", []int64{5487701, 5487799, 3}, mapCopy(metaWithSSEC)},
+ }
+
+ // iterate through the above set of inputs and upload the object.
+ for _, input := range objectInputs {
+ uploadTestObject(t, apiRouter, credentials, bucketName, input.objectName, input.partLengths, input.metaData, false)
+ }
+
+ for i, input := range objectInputs {
+ // initialize HTTP NewRecorder, this records any
+ // mutations to response writer inside the handler.
+ rec := httptest.NewRecorder()
+ // construct HTTP request for HEAD object.
+ req, err := newTestSignedRequestV4(http.MethodHead, getHeadObjectURL("", bucketName, input.objectName),
+ 0, nil, credentials.AccessKey, credentials.SecretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed to create HTTP request for Head Object: %v", i+1, instanceType, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a
+ // ServeHTTP to execute the logic of the handler.
+ apiRouter.ServeHTTP(rec, req)
+
+ isEnc := false
+ expected := 200
+ if strings.HasPrefix(input.objectName, "enc-") {
+ isEnc = true
+ expected = 400
+ }
+ if rec.Code != expected {
+ t.Errorf("Test %d: expected code %d but got %d for object %s", i+1, expected, rec.Code, input.objectName)
+ }
+
+ contentLength := rec.Header().Get("Content-Length")
+ if isEnc {
+ // initialize HTTP NewRecorder, this records any
+ // mutations to response writer inside the handler.
+ rec := httptest.NewRecorder()
+ // construct HTTP request for HEAD object.
+ req, err := newTestSignedRequestV4(http.MethodHead, getHeadObjectURL("", bucketName, input.objectName),
+ 0, nil, credentials.AccessKey, credentials.SecretKey, input.metaData)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed to create HTTP request for Head Object: %v", i+1, instanceType, err)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a
+ // ServeHTTP to execute the logic of the handler.
+ apiRouter.ServeHTTP(rec, req)
+
+ if rec.Code != 200 {
+ t.Errorf("Test %d: Did not receive a 200 response: %d", i+1, rec.Code)
+ }
+ contentLength = rec.Header().Get("Content-Length")
+ }
+
+ if contentLength != fmt.Sprintf("%d", objectLength(input)) {
+ t.Errorf("Test %d: Content length is mismatching: got %s (expected: %d)", i+1, contentLength, objectLength(input))
+ }
+ }
+}
+
+// Wrapper for calling GetObject API handler tests for both Erasure multiple disks and FS single drive setup.
+func TestAPIGetObjectHandler(t *testing.T) {
+ globalPolicySys = NewPolicySys()
+ defer func() { globalPolicySys = nil }()
+
+ defer DetectTestLeak(t)()
+ ExecExtendedObjectLayerAPITest(t, testAPIGetObjectHandler, []string{"GetObject"})
+}
+
+func testAPIGetObjectHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ credentials auth.Credentials, t *testing.T,
+) {
+ objectName := "test-object"
+ // set of byte data for PutObject.
+ // object has to be created before running tests for GetObject.
+ // this is required even to assert the GetObject data,
+ // since dataInserted === dataFetched back is a primary criteria for any object storage this assertion is critical.
+ bytesData := []struct {
+ byteData []byte
+ }{
+ {generateBytesData(6 * humanize.MiByte)},
+ }
+ // set of inputs for uploading the objects before tests for downloading is done.
+ putObjectInputs := []struct {
+ bucketName string
+ objectName string
+ contentLength int64
+ textData []byte
+ metaData map[string]string
+ }{
+ // case - 1.
+ {bucketName, objectName, int64(len(bytesData[0].byteData)), bytesData[0].byteData, make(map[string]string)},
+ }
+ // iterate through the above set of inputs and upload the object.
+ for i, input := range putObjectInputs {
+ // uploading the object.
+ _, err := obj.PutObject(context.Background(), input.bucketName, input.objectName, mustGetPutObjReader(t, bytes.NewReader(input.textData), input.contentLength, input.metaData[""], ""), ObjectOptions{UserDefined: input.metaData})
+ // if object upload fails stop the test.
+ if err != nil {
+ t.Fatalf("Put Object case %d: Error uploading object: %v", i+1, err)
+ }
+ }
+
+ ctx := context.Background()
+
+ // test cases with inputs and expected result for GetObject.
+ testCases := []struct {
+ bucketName string
+ objectName string
+ byteRange string // range of bytes to be fetched from GetObject.
+ accessKey string
+ secretKey string
+ // expected output.
+ expectedContent []byte // expected response body.
+ expectedRespStatus int // expected response status body.
+ }{
+ // Test case - 1.
+ // Fetching the entire object and validating its contents.
+ {
+ bucketName: bucketName,
+ objectName: objectName,
+ byteRange: "",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedContent: bytesData[0].byteData,
+ expectedRespStatus: http.StatusOK,
+ },
+ // Test case - 2.
+ // Case with non-existent object name.
+ {
+ bucketName: bucketName,
+ objectName: "abcd",
+ byteRange: "",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedContent: encodeResponse(getAPIErrorResponse(ctx,
+ getAPIError(ErrNoSuchKey),
+ getGetObjectURL("", bucketName, "abcd"), "", "")),
+ expectedRespStatus: http.StatusNotFound,
+ },
+ // Test case - 3.
+ // Requesting from range 10-100.
+ {
+ bucketName: bucketName,
+ objectName: objectName,
+ byteRange: "bytes=10-100",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedContent: bytesData[0].byteData[10:101],
+ expectedRespStatus: http.StatusPartialContent,
+ },
+ // Test case - 4.
+ // Test case with invalid range.
+ {
+ bucketName: bucketName,
+ objectName: objectName,
+ byteRange: "bytes=-0",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedContent: encodeResponse(getAPIErrorResponse(ctx,
+ getAPIError(ErrInvalidRange),
+ getGetObjectURL("", bucketName, objectName), "", "")),
+ expectedRespStatus: http.StatusRequestedRangeNotSatisfiable,
+ },
+ // Test case - 5.
+ // Test case with byte range exceeding the object size.
+ // Expected to read till end of the object.
+ {
+ bucketName: bucketName,
+ objectName: objectName,
+ byteRange: "bytes=10-1000000000000000",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedContent: bytesData[0].byteData[10:],
+ expectedRespStatus: http.StatusPartialContent,
+ },
+ // Test case - 6.
+ // Test case to induce a signature mismatch.
+ // Using invalid accessID.
+ {
+ bucketName: bucketName,
+ objectName: objectName,
+ byteRange: "",
+ accessKey: "Invalid-AccessID",
+ secretKey: credentials.SecretKey,
+
+ expectedContent: encodeResponse(getAPIErrorResponse(ctx,
+ getAPIError(ErrInvalidAccessKeyID),
+ getGetObjectURL("", bucketName, objectName), "", "")),
+ expectedRespStatus: http.StatusForbidden,
+ },
+ // Test case - 7.
+ // Case with bad components in object name.
+ {
+ bucketName: bucketName,
+ objectName: "../../etc",
+ byteRange: "",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedContent: encodeResponse(getAPIErrorResponse(ctx,
+ getAPIError(ErrInvalidObjectName),
+ getGetObjectURL("", bucketName, "../../etc"), "", "")),
+ expectedRespStatus: http.StatusBadRequest,
+ },
+ // Test case - 8.
+ // Case with strange components but returning error as not found.
+ {
+ bucketName: bucketName,
+ objectName: ". ./. ./etc",
+ byteRange: "",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedContent: encodeResponse(getAPIErrorResponse(ctx,
+ getAPIError(ErrNoSuchKey),
+ SlashSeparator+bucketName+SlashSeparator+". ./. ./etc", "", "")),
+ expectedRespStatus: http.StatusNotFound,
+ },
+ // Test case - 9.
+ // Case with bad components in object name.
+ {
+ bucketName: bucketName,
+ objectName: ". ./../etc",
+ byteRange: "",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedContent: encodeResponse(getAPIErrorResponse(ctx,
+ getAPIError(ErrInvalidObjectName),
+ SlashSeparator+bucketName+SlashSeparator+". ./../etc", "", "")),
+ expectedRespStatus: http.StatusBadRequest,
+ },
+ // Test case - 10.
+ // Case with proper components
+ {
+ bucketName: bucketName,
+ objectName: "etc/path/proper/.../etc",
+ byteRange: "",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedContent: encodeResponse(getAPIErrorResponse(ctx,
+ getAPIError(ErrNoSuchKey),
+ getGetObjectURL("", bucketName, "etc/path/proper/.../etc"),
+ "", "")),
+ expectedRespStatus: http.StatusNotFound,
+ },
+ }
+
+ // Iterating over the cases, fetching the object validating the response.
+ for i, testCase := range testCases {
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ rec := httptest.NewRecorder()
+ // construct HTTP request for Get Object end point.
+ req, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", testCase.bucketName, testCase.objectName),
+ 0, nil, testCase.accessKey, testCase.secretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: Failed to create HTTP request for Get Object: %v", i+1, err)
+ }
+ if testCase.byteRange != "" {
+ req.Header.Set("Range", testCase.byteRange)
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler,`func (api objectAPIHandlers) GetObjectHandler` handles the request.
+ apiRouter.ServeHTTP(rec, req)
+ // Assert the response code with the expected status.
+ if rec.Code != testCase.expectedRespStatus {
+ t.Fatalf("Case %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testCase.expectedRespStatus, rec.Code)
+ }
+ // read the response body.
+ actualContent, err := io.ReadAll(rec.Body)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed reading response body: %v", i+1, instanceType, err)
+ }
+
+ if rec.Code == http.StatusOK || rec.Code == http.StatusPartialContent {
+ if !bytes.Equal(testCase.expectedContent, actualContent) {
+ t.Errorf("Test %d: %s: Object content differs from expected value %s, got %s", i+1, instanceType, testCase.expectedContent, string(actualContent))
+ }
+ continue
+ }
+
+ // Verify whether the bucket obtained object is same as the one created.
+ actualError := &APIErrorResponse{}
+ if err = xml.Unmarshal(actualContent, actualError); err != nil {
+ t.Fatalf("Test %d: %s: Failed parsing response body: %v", i+1, instanceType, err)
+ }
+
+ if path.Clean(actualError.Resource) != pathJoin(SlashSeparator, testCase.bucketName, testCase.objectName) {
+ t.Fatalf("Test %d: %s: Unexpected resource, expected %s, got %s", i+1, instanceType, pathJoin(SlashSeparator, testCase.bucketName, testCase.objectName), actualError.Resource)
+ }
+
+ // Verify response of the V2 signed HTTP request.
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ recV2 := httptest.NewRecorder()
+ // construct HTTP request for GET Object endpoint.
+ reqV2, err := newTestSignedRequestV2(http.MethodGet, getGetObjectURL("", testCase.bucketName, testCase.objectName),
+ 0, nil, testCase.accessKey, testCase.secretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed to create HTTP request for GetObject: %v", i+1, instanceType, err)
+ }
+
+ if testCase.byteRange != "" {
+ reqV2.Header.Set("Range", testCase.byteRange)
+ }
+
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler.
+ apiRouter.ServeHTTP(recV2, reqV2)
+ 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)
+ }
+
+ // read the response body.
+ actualContent, err = io.ReadAll(recV2.Body)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed to read response body: %v", i+1, instanceType, err)
+ }
+
+ if rec.Code == http.StatusOK || rec.Code == http.StatusPartialContent {
+ // Verify whether the bucket obtained object is same as the one created.
+ if !bytes.Equal(testCase.expectedContent, actualContent) {
+ t.Errorf("Test %d: %s: Object content differs from expected value.", i+1, instanceType)
+ }
+ continue
+ }
+
+ actualError = &APIErrorResponse{}
+ if err = xml.Unmarshal(actualContent, actualError); err != nil {
+ t.Fatalf("Test %d: %s: Failed parsing response body: %v", i+1, instanceType, err)
+ }
+
+ if path.Clean(actualError.Resource) != pathJoin(SlashSeparator, testCase.bucketName, testCase.objectName) {
+ t.Fatalf("Test %d: %s: Unexpected resource, expected %s, got %s", i+1, instanceType, pathJoin(SlashSeparator, testCase.bucketName, testCase.objectName), actualError.Resource)
+ }
+ }
+
+ // Test for Anonymous/unsigned http request.
+ anonReq, err := newTestRequest(http.MethodGet, getGetObjectURL("", bucketName, objectName), 0, nil)
+ if err != nil {
+ t.Fatalf("MinIO %s: Failed to create an anonymous request for %s/%s: %v",
+ instanceType, bucketName, objectName, err)
+ }
+
+ // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
+ // sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
+ // unsigned request goes through and its validated again.
+ ExecObjectLayerAPIAnonTest(t, obj, "TestAPIGetObjectHandler", bucketName, objectName, instanceType, apiRouter, anonReq, getAnonReadOnlyObjectPolicy(bucketName, objectName))
+
+ // HTTP request for testing when `objectLayer` is set to `nil`.
+ // There is no need to use an existing bucket and valid input for creating the request
+ // since the `objectLayer==nil` check is performed before any other checks inside the handlers.
+ // The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
+
+ nilBucket := "dummy-bucket"
+ nilObject := "dummy-object"
+ nilReq, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", nilBucket, nilObject),
+ 0, nil, "", "", nil)
+ if err != nil {
+ t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
+ }
+ // execute the object layer set to `nil` test.
+ // `ExecObjectLayerAPINilTest` manages the operation.
+ ExecObjectLayerAPINilTest(t, nilBucket, nilObject, instanceType, apiRouter, nilReq)
+}
+
+// Wrapper for calling GetObject API handler tests for both Erasure multiple disks and FS single drive setup.
+func TestAPIGetObjectWithMPHandler(t *testing.T) {
+ globalPolicySys = NewPolicySys()
+ defer func() { globalPolicySys = nil }()
+
+ defer DetectTestLeak(t)()
+ ExecExtendedObjectLayerAPITest(t, testAPIGetObjectWithMPHandler, []string{"NewMultipart", "PutObjectPart", "CompleteMultipart", "GetObject", "PutObject"})
+}
+
+func testAPIGetObjectWithMPHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ credentials auth.Credentials, t *testing.T,
+) {
+ // Set SSL to on to do encryption tests
+ globalIsTLS = true
+ defer func() { globalIsTLS = false }()
+
+ var (
+ oneMiB int64 = 1024 * 1024
+ key32Bytes = generateBytesData(32 * humanize.Byte)
+ key32BytesMd5 = md5.Sum(key32Bytes)
+ metaWithSSEC = map[string]string{
+ xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES,
+ xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key32Bytes),
+ xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(key32BytesMd5[:]),
+ }
+ mapCopy = func(m map[string]string) map[string]string {
+ r := make(map[string]string, len(m))
+ maps.Copy(r, m)
+ return r
+ }
+ )
+
+ type ObjectInput struct {
+ objectName string
+ partLengths []int64
+
+ metaData map[string]string
+ }
+
+ objectLength := func(oi ObjectInput) (sum int64) {
+ for _, l := range oi.partLengths {
+ sum += l
+ }
+ return sum
+ }
+
+ // set of inputs for uploading the objects before tests for
+ // downloading is done. Data bytes are from DummyDataGen.
+ objectInputs := []ObjectInput{
+ // // cases 0-3: small single part objects
+ {"nothing", []int64{0}, make(map[string]string)},
+ {"small-0", []int64{11}, make(map[string]string)},
+ {"small-1", []int64{509}, make(map[string]string)},
+ {"small-2", []int64{5 * oneMiB}, make(map[string]string)},
+ // // // cases 4-7: multipart part objects
+ {"mp-0", []int64{5 * oneMiB, 10}, make(map[string]string)},
+ {"mp-1", []int64{5*oneMiB + 1, 10}, make(map[string]string)},
+ {"mp-2", []int64{5487701, 5487799, 3}, make(map[string]string)},
+ {"mp-3", []int64{10499807, 10499963, 7}, make(map[string]string)},
+ // cases 8-11: small single part objects with encryption
+ {"enc-nothing", []int64{0}, mapCopy(metaWithSSEC)},
+ {"enc-small-0", []int64{11}, mapCopy(metaWithSSEC)},
+ {"enc-small-1", []int64{509}, mapCopy(metaWithSSEC)},
+ {"enc-small-2", []int64{5 * oneMiB}, mapCopy(metaWithSSEC)},
+ // cases 12-15: multipart part objects with encryption
+ {"enc-mp-0", []int64{5 * oneMiB, 10}, mapCopy(metaWithSSEC)},
+ {"enc-mp-1", []int64{5*oneMiB + 1, 10}, mapCopy(metaWithSSEC)},
+ {"enc-mp-2", []int64{5487701, 5487799, 3}, mapCopy(metaWithSSEC)},
+ {"enc-mp-3", []int64{10499807, 10499963, 7}, mapCopy(metaWithSSEC)},
+ }
+ if testing.Short() {
+ objectInputs = append(objectInputs[0:5], objectInputs[8:11]...)
+ }
+ // iterate through the above set of inputs and upload the object.
+ for _, input := range objectInputs {
+ uploadTestObject(t, apiRouter, credentials, bucketName, input.objectName, input.partLengths, input.metaData, false)
+ }
+
+ // function type for creating signed requests - used to repeat
+ // requests with V2 and V4 signing.
+ type testSignedReqFn func(method, urlStr string, contentLength int64,
+ body io.ReadSeeker, accessKey, secretKey string, metamap map[string]string) (*http.Request,
+ error)
+
+ mkGetReq := func(oi ObjectInput, byteRange string, i int, mkSignedReq testSignedReqFn) {
+ object := oi.objectName
+ rec := httptest.NewRecorder()
+ req, err := mkSignedReq(http.MethodGet, getGetObjectURL("", bucketName, object),
+ 0, nil, credentials.AccessKey, credentials.SecretKey, oi.metaData)
+ if err != nil {
+ t.Fatalf("Object: %s Case %d ByteRange: %s: Failed to create HTTP request for Get Object: %v",
+ object, i+1, byteRange, err)
+ }
+
+ if byteRange != "" {
+ req.Header.Set("Range", byteRange)
+ }
+
+ apiRouter.ServeHTTP(rec, req)
+
+ // Check response code (we make only valid requests in
+ // this test)
+ if rec.Code != http.StatusPartialContent && rec.Code != http.StatusOK {
+ bd, err1 := io.ReadAll(rec.Body)
+ t.Fatalf("%s Object: %s Case %d ByteRange: %s: Got response status `%d` and body: %s,%v",
+ instanceType, object, i+1, byteRange, rec.Code, string(bd), err1)
+ }
+
+ var off, length int64
+ var rs *HTTPRangeSpec
+ if byteRange != "" {
+ rs, err = parseRequestRangeSpec(byteRange)
+ if err != nil {
+ t.Fatalf("Object: %s Case %d ByteRange: %s: Unexpected err: %v", object, i+1, byteRange, err)
+ }
+ }
+ off, length, err = rs.GetOffsetLength(objectLength(oi))
+ if err != nil {
+ t.Fatalf("Object: %s Case %d ByteRange: %s: Unexpected err: %v", object, i+1, byteRange, err)
+ }
+
+ readers := []io.Reader{}
+ cumulativeSum := int64(0)
+ for _, p := range oi.partLengths {
+ readers = append(readers, NewDummyDataGen(p, cumulativeSum))
+ cumulativeSum += p
+ }
+
+ refReader := io.LimitReader(ioutilx.NewSkipReader(io.MultiReader(readers...), off), length)
+ if ok, msg := cmpReaders(refReader, rec.Body); !ok {
+ t.Fatalf("(%s) Object: %s Case %d ByteRange: %s --> data mismatch! (msg: %s)", instanceType, oi.objectName, i+1, byteRange, msg)
+ }
+ }
+
+ // Iterate over each uploaded object and do a bunch of get
+ // requests on them.
+ caseNumber := 0
+ signFns := []testSignedReqFn{newTestSignedRequestV2, newTestSignedRequestV4}
+ for _, oi := range objectInputs {
+ objLen := objectLength(oi)
+ for _, sf := range signFns {
+ // Read whole object
+ mkGetReq(oi, "", caseNumber, sf)
+ caseNumber++
+
+ // No range requests are possible if the
+ // object length is 0
+ if objLen == 0 {
+ continue
+ }
+
+ // Various ranges to query - all are valid!
+ rangeHdrs := []string{
+ // Read first byte of object
+ fmt.Sprintf("bytes=%d-%d", 0, 0),
+ // Read second byte of object
+ fmt.Sprintf("bytes=%d-%d", 1, 1),
+ // Read last byte of object
+ fmt.Sprintf("bytes=-%d", 1),
+ // Read all but first byte of object
+ "bytes=1-",
+ // Read first half of object
+ fmt.Sprintf("bytes=%d-%d", 0, objLen/2),
+ // Read last half of object
+ fmt.Sprintf("bytes=-%d", objLen/2),
+ // Read middle half of object
+ fmt.Sprintf("bytes=%d-%d", objLen/4, objLen*3/4),
+ // Read 100MiB of the object from the beginning
+ fmt.Sprintf("bytes=%d-%d", 0, 100*humanize.MiByte),
+ // Read 100MiB of the object from the end
+ fmt.Sprintf("bytes=-%d", 100*humanize.MiByte),
+ }
+ for _, rangeHdr := range rangeHdrs {
+ mkGetReq(oi, rangeHdr, caseNumber, sf)
+ caseNumber++
+ }
+ }
+ }
+
+ // HTTP request for testing when `objectLayer` is set to `nil`.
+ // There is no need to use an existing bucket and valid input for creating the request
+ // since the `objectLayer==nil` check is performed before any other checks inside the handlers.
+ // The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
+
+ nilBucket := "dummy-bucket"
+ nilObject := "dummy-object"
+ nilReq, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", nilBucket, nilObject),
+ 0, nil, "", "", nil)
+ if err != nil {
+ t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
+ }
+ // execute the object layer set to `nil` test.
+ // `ExecObjectLayerAPINilTest` manages the operation.
+ ExecObjectLayerAPINilTest(t, nilBucket, nilObject, instanceType, apiRouter, nilReq)
+}
+
+// Wrapper for calling GetObject API handler tests for both Erasure multiple disks and FS single drive setup.
+func TestAPIGetObjectWithPartNumberHandler(t *testing.T) {
+ globalPolicySys = NewPolicySys()
+ defer func() { globalPolicySys = nil }()
+
+ defer DetectTestLeak(t)()
+ ExecExtendedObjectLayerAPITest(t, testAPIGetObjectWithPartNumberHandler, []string{"NewMultipart", "PutObjectPart", "CompleteMultipart", "GetObject", "PutObject"})
+}
+
+func testAPIGetObjectWithPartNumberHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ credentials auth.Credentials, t *testing.T,
+) {
+ // Set SSL to on to do encryption tests
+ globalIsTLS = true
+ defer func() { globalIsTLS = false }()
+
+ var (
+ oneMiB int64 = 1024 * 1024
+ key32Bytes = generateBytesData(32 * humanize.Byte)
+ key32BytesMd5 = md5.Sum(key32Bytes)
+ metaWithSSEC = map[string]string{
+ xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES,
+ xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key32Bytes),
+ xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(key32BytesMd5[:]),
+ }
+ mapCopy = func(m map[string]string) map[string]string {
+ r := make(map[string]string, len(m))
+ maps.Copy(r, m)
+ return r
+ }
+ )
+
+ type ObjectInput struct {
+ objectName string
+ partLengths []int64
+
+ metaData map[string]string
+ }
+
+ // set of inputs for uploading the objects before tests for
+ // downloading is done. Data bytes are from DummyDataGen.
+ objectInputs := []ObjectInput{
+ // // cases 0-4: small single part objects
+ {"nothing", []int64{0}, make(map[string]string)},
+ {"1byte", []int64{1}, make(map[string]string)},
+ {"small-0", []int64{11}, make(map[string]string)},
+ {"small-1", []int64{509}, make(map[string]string)},
+ {"small-2", []int64{5 * oneMiB}, make(map[string]string)},
+ // // // cases 5-8: multipart part objects
+ {"mp-0", []int64{5 * oneMiB, 1}, make(map[string]string)},
+ {"mp-1", []int64{5*oneMiB + 1, 1}, make(map[string]string)},
+ {"mp-2", []int64{5487701, 5487799, 3}, make(map[string]string)},
+ {"mp-3", []int64{10499807, 10499963, 7}, make(map[string]string)},
+ // cases 9-12: small single part objects with encryption
+ {"enc-nothing", []int64{0}, mapCopy(metaWithSSEC)},
+ {"enc-small-0", []int64{11}, mapCopy(metaWithSSEC)},
+ {"enc-small-1", []int64{509}, mapCopy(metaWithSSEC)},
+ {"enc-small-2", []int64{5 * oneMiB}, mapCopy(metaWithSSEC)},
+ // cases 13-16: multipart part objects with encryption
+ {"enc-mp-0", []int64{5 * oneMiB, 1}, mapCopy(metaWithSSEC)},
+ {"enc-mp-1", []int64{5*oneMiB + 1, 1}, mapCopy(metaWithSSEC)},
+ {"enc-mp-2", []int64{5487701, 5487799, 3}, mapCopy(metaWithSSEC)},
+ {"enc-mp-3", []int64{10499807, 10499963, 7}, mapCopy(metaWithSSEC)},
+ }
+
+ // SSEC can't be used with compression
+ globalCompressConfigMu.Lock()
+ compressEnabled := globalCompressConfig.Enabled
+ globalCompressConfigMu.Unlock()
+ if compressEnabled {
+ objectInputs = objectInputs[0:9]
+ }
+
+ // iterate through the above set of inputs and upload the object.
+ for _, input := range objectInputs {
+ uploadTestObject(t, apiRouter, credentials, bucketName, input.objectName, input.partLengths, input.metaData, false)
+ }
+
+ mkGetReqWithPartNumber := func(oindex int, oi ObjectInput, partNumber int) {
+ object := oi.objectName
+
+ queries := url.Values{}
+ queries.Add("partNumber", strconv.Itoa(partNumber))
+ targetURL := makeTestTargetURL("", bucketName, object, queries)
+ req, err := newTestSignedRequestV4(http.MethodGet, targetURL,
+ 0, nil, credentials.AccessKey, credentials.SecretKey, oi.metaData)
+ if err != nil {
+ t.Fatalf("Object: %s Object Index %d PartNumber: %d: Failed to create HTTP request for Get Object: %v",
+ object, oindex, partNumber, err)
+ }
+
+ rec := httptest.NewRecorder()
+ apiRouter.ServeHTTP(rec, req)
+
+ // Check response code (we make only valid requests in this test)
+ if rec.Code != http.StatusPartialContent && rec.Code != http.StatusOK {
+ bd, err1 := io.ReadAll(rec.Body)
+ t.Fatalf("%s Object: %s ObjectIndex %d PartNumber: %d: Got response status `%d` and body: %s,%v",
+ instanceType, object, oindex, partNumber, rec.Code, string(bd), err1)
+ }
+
+ oinfo, err := obj.GetObjectInfo(context.Background(), bucketName, object, ObjectOptions{})
+ if err != nil {
+ t.Fatalf("Object: %s Object Index %d: Unexpected err: %v", object, oindex, err)
+ }
+
+ rs := partNumberToRangeSpec(oinfo, partNumber)
+ size, err := oinfo.GetActualSize()
+ if err != nil {
+ t.Fatalf("Object: %s Object Index %d: Unexpected err: %v", object, oindex, err)
+ }
+
+ off, length, err := rs.GetOffsetLength(size)
+ if err != nil {
+ t.Fatalf("Object: %s Object Index %d: Unexpected err: %v", object, oindex, err)
+ }
+
+ readers := []io.Reader{}
+ cumulativeSum := int64(0)
+ for _, p := range oi.partLengths {
+ readers = append(readers, NewDummyDataGen(p, cumulativeSum))
+ cumulativeSum += p
+ }
+
+ refReader := io.LimitReader(ioutilx.NewSkipReader(io.MultiReader(readers...), off), length)
+ if ok, msg := cmpReaders(refReader, rec.Body); !ok {
+ t.Fatalf("(%s) Object: %s ObjectIndex %d PartNumber: %d --> data mismatch! (msg: %s)", instanceType, oi.objectName, oindex, partNumber, msg)
+ }
+ }
+
+ for idx, oi := range objectInputs {
+ for partNum := 1; partNum <= len(oi.partLengths); partNum++ {
+ mkGetReqWithPartNumber(idx, oi, partNum)
+ }
+ }
+}
+
+// Wrapper for calling PutObject API handler tests using streaming signature v4 for both Erasure multiple disks and FS single drive setup.
+func TestAPIPutObjectStreamSigV4Handler(t *testing.T) {
+ defer DetectTestLeak(t)()
+ ExecExtendedObjectLayerAPITest(t, testAPIPutObjectStreamSigV4Handler, []string{"PutObject"})
+}
+
+func testAPIPutObjectStreamSigV4Handler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ credentials auth.Credentials, t *testing.T,
+) {
+ objectName := "test-object"
+ bytesDataLen := 65 * humanize.KiByte
+ bytesData := bytes.Repeat([]byte{'a'}, bytesDataLen)
+ oneKData := bytes.Repeat([]byte("a"), 1*humanize.KiByte)
+
+ var err error
+
+ type streamFault int
+ const (
+ None streamFault = iota
+ malformedEncoding
+ unexpectedEOF
+ signatureMismatch
+ chunkDateMismatch
+ tooBigDecodedLength
+ )
+
+ // byte data for PutObject.
+ // test cases with inputs and expected result for GetObject.
+ testCases := []struct {
+ bucketName string
+ objectName string
+ data []byte
+ dataLen int
+ chunkSize int64
+ // expected output.
+ expectedContent []byte // expected response body.
+ expectedRespStatus int // expected response status body.
+ // Access keys
+ accessKey string
+ secretKey string
+ shouldPass bool
+ removeAuthHeader bool
+ fault streamFault
+ // Custom content encoding.
+ contentEncoding string
+ }{
+ // Test case - 1.
+ // Fetching the entire object and validating its contents.
+ {
+ bucketName: bucketName,
+ objectName: objectName,
+ data: bytesData,
+ dataLen: len(bytesData),
+ chunkSize: 64 * humanize.KiByte,
+ expectedContent: []byte{},
+ expectedRespStatus: http.StatusOK,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ shouldPass: true,
+ fault: None,
+ },
+ // Test case - 2
+ // Small chunk size.
+ {
+ bucketName: bucketName,
+ objectName: objectName,
+ data: bytesData,
+ dataLen: len(bytesData),
+ chunkSize: 1 * humanize.KiByte,
+ expectedContent: []byte{},
+ expectedRespStatus: http.StatusOK,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ shouldPass: true,
+ fault: None,
+ },
+ // Test case - 3
+ // Empty data
+ {
+ bucketName: bucketName,
+ objectName: objectName,
+ data: []byte{},
+ dataLen: 0,
+ chunkSize: 64 * humanize.KiByte,
+ expectedContent: []byte{},
+ expectedRespStatus: http.StatusOK,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ shouldPass: true,
+ },
+ // Test case - 4
+ // Invalid access key id.
+ {
+ bucketName: bucketName,
+ objectName: objectName,
+ data: bytesData,
+ dataLen: len(bytesData),
+ chunkSize: 64 * humanize.KiByte,
+ expectedContent: []byte{},
+ expectedRespStatus: http.StatusForbidden,
+ accessKey: "",
+ secretKey: "",
+ shouldPass: false,
+ fault: None,
+ },
+ // Test case - 5
+ // Wrong auth header returns as bad request.
+ {
+ bucketName: bucketName,
+ objectName: objectName,
+ data: bytesData,
+ dataLen: len(bytesData),
+ chunkSize: 64 * humanize.KiByte,
+ expectedContent: []byte{},
+ expectedRespStatus: http.StatusBadRequest,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ shouldPass: false,
+ removeAuthHeader: true,
+ fault: None,
+ },
+ // Test case - 6
+ // Large chunk size.. also passes.
+ {
+ bucketName: bucketName,
+ objectName: objectName,
+ data: bytesData,
+ dataLen: len(bytesData),
+ chunkSize: 100 * humanize.KiByte,
+ expectedContent: []byte{},
+ expectedRespStatus: http.StatusOK,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ shouldPass: true,
+ fault: None,
+ },
+ // Test case - 7
+ // Chunk with malformed encoding.
+ // Causes signature mismatch.
+ {
+ bucketName: bucketName,
+ objectName: objectName,
+ data: oneKData,
+ dataLen: 1024,
+ chunkSize: 1024,
+ expectedContent: []byte{},
+ expectedRespStatus: http.StatusForbidden,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ shouldPass: false,
+ fault: malformedEncoding,
+ },
+ // Test case - 8
+ // Chunk with shorter than advertised chunk data.
+ {
+ bucketName: bucketName,
+ objectName: objectName,
+ data: oneKData,
+ dataLen: 1024,
+ chunkSize: 1024,
+ expectedContent: []byte{},
+ expectedRespStatus: http.StatusBadRequest,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ shouldPass: false,
+ fault: unexpectedEOF,
+ },
+ // Test case - 9
+ // Chunk with first chunk data byte tampered.
+ {
+ bucketName: bucketName,
+ objectName: objectName,
+ data: oneKData,
+ dataLen: 1024,
+ chunkSize: 1024,
+ expectedContent: []byte{},
+ expectedRespStatus: http.StatusForbidden,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ shouldPass: false,
+ fault: signatureMismatch,
+ },
+ // Test case - 10
+ // Different date (timestamps) used in seed signature calculation
+ // and chunks signature calculation.
+ {
+ bucketName: bucketName,
+ objectName: objectName,
+ data: oneKData,
+ dataLen: 1024,
+ chunkSize: 1024,
+ expectedContent: []byte{},
+ expectedRespStatus: http.StatusForbidden,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ shouldPass: false,
+ fault: chunkDateMismatch,
+ },
+ // Test case - 11
+ // Set x-amz-decoded-content-length to a value too big to hold in int64.
+ {
+ bucketName: bucketName,
+ objectName: objectName,
+ data: oneKData,
+ dataLen: 1024,
+ chunkSize: 1024,
+ expectedContent: []byte{},
+ expectedRespStatus: http.StatusBadRequest,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ shouldPass: false,
+ fault: tooBigDecodedLength,
+ },
+ // Test case - 12
+ // Set custom content encoding should succeed and save the encoding properly.
+ {
+ bucketName: bucketName,
+ objectName: objectName,
+ data: bytesData,
+ dataLen: len(bytesData),
+ chunkSize: 100 * humanize.KiByte,
+ expectedContent: []byte{},
+ expectedRespStatus: http.StatusOK,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ shouldPass: true,
+ contentEncoding: "aws-chunked,gzip",
+ fault: None,
+ },
+ }
+ // Iterating over the cases, fetching the object validating the response.
+ for i, testCase := range testCases {
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ rec := httptest.NewRecorder()
+ // construct HTTP request for Put Object end point.
+ var req *http.Request
+ switch {
+ case testCase.fault == chunkDateMismatch:
+ req, err = newTestStreamingSignedBadChunkDateRequest(http.MethodPut,
+ getPutObjectURL("", testCase.bucketName, testCase.objectName),
+ int64(testCase.dataLen), testCase.chunkSize, bytes.NewReader(testCase.data),
+ testCase.accessKey, testCase.secretKey)
+ case testCase.contentEncoding == "":
+ req, err = newTestStreamingSignedRequest(http.MethodPut,
+ getPutObjectURL("", testCase.bucketName, testCase.objectName),
+ int64(testCase.dataLen), testCase.chunkSize, bytes.NewReader(testCase.data),
+ testCase.accessKey, testCase.secretKey)
+ case testCase.contentEncoding != "":
+ req, err = newTestStreamingSignedCustomEncodingRequest(http.MethodPut,
+ getPutObjectURL("", testCase.bucketName, testCase.objectName),
+ int64(testCase.dataLen), testCase.chunkSize, bytes.NewReader(testCase.data),
+ testCase.accessKey, testCase.secretKey, testCase.contentEncoding)
+ }
+ if err != nil {
+ t.Fatalf("Test %d: Failed to create HTTP request for Put Object: %v", i+1, err)
+ }
+ // Removes auth header if test case requires it.
+ if testCase.removeAuthHeader {
+ req.Header.Del("Authorization")
+ }
+ switch testCase.fault {
+ case malformedEncoding:
+ req, err = malformChunkSizeSigV4(req, testCase.chunkSize-1)
+ case signatureMismatch:
+ req, err = malformDataSigV4(req, 'z')
+ case unexpectedEOF:
+ req, err = truncateChunkByHalfSigv4(req)
+ case tooBigDecodedLength:
+ // Set decoded length to a large value out of int64 range to simulate parse failure.
+ req.Header.Set("x-amz-decoded-content-length", "9999999999999999999999")
+ }
+
+ if err != nil {
+ t.Fatalf("Error injecting faults into the request: %v.", err)
+ }
+
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler,`func (api objectAPIHandlers) GetObjectHandler` handles the request.
+ apiRouter.ServeHTTP(rec, req)
+ // Assert the response code with the expected status.
+ if rec.Code != testCase.expectedRespStatus {
+ t.Errorf("Test %d %s: Expected the response status to be `%d`, but instead found `%d`: fault case %d",
+ i+1, instanceType, testCase.expectedRespStatus, rec.Code, testCase.fault)
+ }
+ // read the response body.
+ actualContent, err := io.ReadAll(rec.Body)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed parsing response body: %v", i+1, instanceType, err)
+ }
+ opts := ObjectOptions{}
+ if testCase.shouldPass {
+ // Verify whether the bucket obtained object is same as the one created.
+ if !bytes.Equal(testCase.expectedContent, actualContent) {
+ t.Errorf("Test %d: %s: Object content differs from expected value.: %s", i+1, instanceType, string(actualContent))
+ continue
+ }
+ objInfo, err := obj.GetObjectInfo(context.Background(), testCase.bucketName, testCase.objectName, opts)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed to fetch the copied object: %s", i+1, instanceType, err)
+ }
+ if objInfo.ContentEncoding == streamingContentEncoding {
+ t.Fatalf("Test %d: %s: ContentEncoding is set to \"aws-chunked\" which is unexpected", i+1, instanceType)
+ }
+ expectedContentEncoding := trimAwsChunkedContentEncoding(testCase.contentEncoding)
+ if expectedContentEncoding != objInfo.ContentEncoding {
+ t.Fatalf("Test %d: %s: ContentEncoding is set to \"%s\" which is unexpected, expected \"%s\"", i+1, instanceType, objInfo.ContentEncoding, expectedContentEncoding)
+ }
+ buffer := new(bytes.Buffer)
+ r, err := obj.GetObjectNInfo(context.Background(), testCase.bucketName, testCase.objectName, nil, nil, opts)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed to fetch the copied object: %s", i+1, instanceType, err)
+ }
+ if _, err = io.Copy(buffer, r); err != nil {
+ r.Close()
+ t.Fatalf("Test %d: %s: Failed to fetch the copied object: %s", i+1, instanceType, err)
+ }
+ r.Close()
+ if !bytes.Equal(testCase.data, buffer.Bytes()) {
+ t.Errorf("Test %d: %s: Data Mismatch: Data fetched back from the uploaded object doesn't match the original one.", i+1, instanceType)
+ }
+ }
+ }
+}
+
+// Wrapper for calling PutObject API handler tests for both Erasure multiple disks and FS single drive setup.
+func TestAPIPutObjectHandler(t *testing.T) {
+ defer DetectTestLeak(t)()
+ ExecExtendedObjectLayerAPITest(t, testAPIPutObjectHandler, []string{"PutObject"})
+}
+
+func testAPIPutObjectHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ credentials auth.Credentials, t *testing.T,
+) {
+ var err error
+ objectName := "test-object"
+ opts := ObjectOptions{}
+ // byte data for PutObject.
+ bytesData := generateBytesData(6 * humanize.KiByte)
+
+ copySourceHeader := map[string]string{"X-Amz-Copy-Source": "somewhere"}
+ invalidMD5Header := map[string]string{"Content-Md5": "42"}
+ invalidStorageClassHeader := map[string]string{xhttp.AmzStorageClass: "INVALID"}
+
+ addCustomHeaders := func(req *http.Request, customHeaders map[string]string) {
+ for k, value := range customHeaders {
+ req.Header.Set(k, value)
+ }
+ }
+
+ checksumData := func(b []byte, h hash.Hash) string {
+ h.Reset()
+ _, err := h.Write(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return base64.StdEncoding.EncodeToString(h.Sum(nil))
+ }
+ // test cases with inputs and expected result for GetObject.
+ testCases := []struct {
+ bucketName string
+ objectName string
+ headers map[string]string
+ data []byte
+ dataLen int
+ accessKey string
+ secretKey string
+ fault Fault
+ // expected output.
+ expectedRespStatus int // expected response status body.
+ wantAPICode string
+ wantHeaders map[string]string
+ }{
+ // Fetching the entire object and validating its contents.
+ 0: {
+ bucketName: bucketName,
+ objectName: objectName,
+ data: bytesData,
+ dataLen: len(bytesData),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedRespStatus: http.StatusOK,
+ },
+ // Test Case with invalid accessID.
+ 1: {
+ bucketName: bucketName,
+ objectName: objectName,
+ data: bytesData,
+ dataLen: len(bytesData),
+ accessKey: "Wrong-AccessID",
+ secretKey: credentials.SecretKey,
+
+ expectedRespStatus: http.StatusForbidden,
+ wantAPICode: "InvalidAccessKeyId",
+ },
+ // Test Case with invalid header key X-Amz-Copy-Source.
+ 2: {
+ bucketName: bucketName,
+ objectName: objectName,
+ headers: copySourceHeader,
+ data: bytesData,
+ dataLen: len(bytesData),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusBadRequest,
+ wantAPICode: "InvalidArgument",
+ },
+ // Test Case with invalid Content-Md5 value
+ 3: {
+ bucketName: bucketName,
+ objectName: objectName,
+ headers: invalidMD5Header,
+ data: bytesData,
+ dataLen: len(bytesData),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusBadRequest,
+ wantAPICode: "InvalidDigest",
+ },
+ // Test Case with object greater than maximum allowed size.
+ 4: {
+ bucketName: bucketName,
+ objectName: objectName,
+ data: bytesData,
+ dataLen: len(bytesData),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ fault: TooBigObject,
+ expectedRespStatus: http.StatusBadRequest,
+ wantAPICode: "EntityTooLarge",
+ },
+ // Test Case with missing content length
+ 5: {
+ bucketName: bucketName,
+ objectName: objectName,
+ data: bytesData,
+ dataLen: len(bytesData),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ fault: MissingContentLength,
+ expectedRespStatus: http.StatusLengthRequired,
+ wantAPICode: "MissingContentLength",
+ },
+ // Test Case with invalid header key X-Amz-Storage-Class
+ 6: {
+ bucketName: bucketName,
+ objectName: objectName,
+ headers: invalidStorageClassHeader,
+ data: bytesData,
+ dataLen: len(bytesData),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusBadRequest,
+ wantAPICode: "InvalidStorageClass",
+ },
+
+ // Invalid crc32
+ 7: {
+ bucketName: bucketName,
+ objectName: objectName,
+ headers: map[string]string{"x-amz-checksum-crc32": "123"},
+ data: bytesData,
+ dataLen: len(bytesData),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusBadRequest,
+ wantAPICode: "InvalidArgument",
+ },
+ // Wrong crc32
+ 8: {
+ bucketName: bucketName,
+ objectName: objectName,
+ headers: map[string]string{"x-amz-checksum-crc32": "MTIzNA=="},
+ data: bytesData,
+ dataLen: len(bytesData),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusBadRequest,
+ wantAPICode: "XAmzContentChecksumMismatch",
+ },
+ // Correct crc32
+ 9: {
+ bucketName: bucketName,
+ objectName: objectName,
+ headers: map[string]string{"x-amz-checksum-crc32": checksumData(bytesData, crc32.New(crc32.IEEETable))},
+ data: bytesData,
+ dataLen: len(bytesData),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusOK,
+ wantHeaders: map[string]string{"x-amz-checksum-crc32": checksumData(bytesData, crc32.New(crc32.IEEETable))},
+ },
+ // Correct crc32c
+ 10: {
+ bucketName: bucketName,
+ objectName: objectName,
+ headers: map[string]string{"x-amz-checksum-crc32c": checksumData(bytesData, crc32.New(crc32.MakeTable(crc32.Castagnoli)))},
+ data: bytesData,
+ dataLen: len(bytesData),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusOK,
+ wantHeaders: map[string]string{"x-amz-checksum-crc32c": checksumData(bytesData, crc32.New(crc32.MakeTable(crc32.Castagnoli)))},
+ },
+ // CRC32 as CRC32C
+ 11: {
+ bucketName: bucketName,
+ objectName: objectName,
+ headers: map[string]string{"x-amz-checksum-crc32c": checksumData(bytesData, crc32.New(crc32.IEEETable))},
+ data: bytesData,
+ dataLen: len(bytesData),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusBadRequest,
+ wantAPICode: "XAmzContentChecksumMismatch",
+ },
+ // SHA1
+ 12: {
+ bucketName: bucketName,
+ objectName: objectName,
+ headers: map[string]string{"x-amz-checksum-sha1": checksumData(bytesData, sha1.New())},
+ data: bytesData,
+ dataLen: len(bytesData),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusOK,
+ wantHeaders: map[string]string{"x-amz-checksum-sha1": checksumData(bytesData, sha1.New())},
+ },
+ // SHA256
+ 13: {
+ bucketName: bucketName,
+ objectName: objectName,
+ headers: map[string]string{"x-amz-checksum-sha256": checksumData(bytesData, sha256.New())},
+ data: bytesData,
+ dataLen: len(bytesData),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusOK,
+ wantHeaders: map[string]string{"x-amz-checksum-sha256": checksumData(bytesData, sha256.New())},
+ },
+ }
+ // Iterating over the cases, fetching the object validating the response.
+ for i, testCase := range testCases {
+ var req, reqV2 *http.Request
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ rec := httptest.NewRecorder()
+ // construct HTTP request for Get Object end point.
+ req, err = newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", testCase.bucketName, testCase.objectName),
+ int64(testCase.dataLen), bytes.NewReader(testCase.data), testCase.accessKey, testCase.secretKey, testCase.headers)
+ if err != nil {
+ t.Fatalf("Test %d: Failed to create HTTP request for Put Object: %v", i, err)
+ }
+ // Add test case specific headers to the request.
+ addCustomHeaders(req, testCase.headers)
+
+ // Inject faults if specified in testCase.fault
+ switch testCase.fault {
+ case MissingContentLength:
+ req.ContentLength = -1
+ req.TransferEncoding = []string{}
+ case TooBigObject:
+ req.ContentLength = globalMaxObjectSize + 1
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler,`func (api objectAPIHandlers) GetObjectHandler` handles the request.
+ apiRouter.ServeHTTP(rec, req)
+ // Assert the response code with the expected status.
+ if rec.Code != testCase.expectedRespStatus {
+ b, _ := io.ReadAll(rec.Body)
+ t.Fatalf("Test %d: Expected the response status to be `%d`, but instead found `%d`: %s", i, testCase.expectedRespStatus, rec.Code, string(b))
+ }
+ if testCase.expectedRespStatus != http.StatusOK {
+ b, err := io.ReadAll(rec.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var apiErr APIErrorResponse
+ err = xml.Unmarshal(b, &apiErr)
+ if err != nil {
+ t.Fatal(err)
+ }
+ gotErr := apiErr.Code
+ wantErr := testCase.wantAPICode
+ if gotErr != wantErr {
+ t.Errorf("test %d: want api error %q, got %q", i, wantErr, gotErr)
+ }
+ if testCase.wantHeaders != nil {
+ for k, v := range testCase.wantHeaders {
+ got := rec.Header().Get(k)
+ if got != v {
+ t.Errorf("Want header %s = %s, got %#v", k, v, rec.Header())
+ }
+ }
+ }
+ }
+
+ if testCase.expectedRespStatus == http.StatusOK {
+ buffer := new(bytes.Buffer)
+ // Fetch the object to check whether the content is same as the one uploaded via PutObject.
+ gr, err := obj.GetObjectNInfo(context.Background(), testCase.bucketName, testCase.objectName, nil, nil, opts)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed to fetch the copied object: %s", i, instanceType, err)
+ }
+ if _, err = io.Copy(buffer, gr); err != nil {
+ gr.Close()
+ t.Fatalf("Test %d: %s: Failed to fetch the copied object: %s", i, instanceType, err)
+ }
+ gr.Close()
+ if !bytes.Equal(bytesData, buffer.Bytes()) {
+ t.Errorf("Test %d: %s: Data Mismatch: Data fetched back from the uploaded object doesn't match the original one.", i, instanceType)
+ }
+ buffer.Reset()
+ }
+
+ // Verify response of the V2 signed HTTP request.
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ recV2 := httptest.NewRecorder()
+ // construct HTTP request for PUT Object endpoint.
+ reqV2, err = newTestSignedRequestV2(http.MethodPut, getPutObjectURL("", testCase.bucketName, testCase.objectName),
+ int64(testCase.dataLen), bytes.NewReader(testCase.data), testCase.accessKey, testCase.secretKey, testCase.headers)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed to create HTTP request for PutObject: %v", i, instanceType, err)
+ }
+
+ // Add test case specific headers to the request.
+ addCustomHeaders(reqV2, testCase.headers)
+
+ // Inject faults if specified in testCase.fault
+ switch testCase.fault {
+ case MissingContentLength:
+ reqV2.ContentLength = -1
+ reqV2.TransferEncoding = []string{}
+ case TooBigObject:
+ reqV2.ContentLength = globalMaxObjectSize + 1
+ }
+
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler.
+ apiRouter.ServeHTTP(recV2, reqV2)
+ if recV2.Code != testCase.expectedRespStatus {
+ b, _ := io.ReadAll(rec.Body)
+ t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`: %s", i, instanceType, testCase.expectedRespStatus, recV2.Code, string(b))
+ }
+
+ if testCase.expectedRespStatus == http.StatusOK {
+ buffer := new(bytes.Buffer)
+ // Fetch the object to check whether the content is same as the one uploaded via PutObject.
+ gr, err := obj.GetObjectNInfo(context.Background(), testCase.bucketName, testCase.objectName, nil, nil, opts)
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed to fetch the copied object: %s", i, instanceType, err)
+ }
+ if _, err = io.Copy(buffer, gr); err != nil {
+ gr.Close()
+ t.Fatalf("Test %d: %s: Failed to fetch the copied object: %s", i, instanceType, err)
+ }
+ gr.Close()
+ if !bytes.Equal(bytesData, buffer.Bytes()) {
+ t.Errorf("Test %d: %s: Data Mismatch: Data fetched back from the uploaded object doesn't match the original one.", i, instanceType)
+ }
+ buffer.Reset()
+
+ if testCase.wantHeaders != nil {
+ for k, v := range testCase.wantHeaders {
+ got := recV2.Header().Get(k)
+ if got != v {
+ t.Errorf("Want header %s = %s, got %#v", k, v, recV2.Header())
+ }
+ }
+ }
+ }
+ }
+
+ // Test for Anonymous/unsigned http request.
+ anonReq, err := newTestRequest(http.MethodPut, getPutObjectURL("", bucketName, objectName),
+ int64(len("hello")), bytes.NewReader([]byte("hello")))
+ if err != nil {
+ t.Fatalf("MinIO %s: Failed to create an anonymous request for %s/%s: %v",
+ instanceType, bucketName, objectName, err)
+ }
+
+ // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
+ // sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
+ // unsigned request goes through and its validated again.
+ ExecObjectLayerAPIAnonTest(t, obj, "TestAPIPutObjectHandler", bucketName, objectName, instanceType, apiRouter, anonReq, getAnonWriteOnlyObjectPolicy(bucketName, objectName))
+
+ // HTTP request to test the case of `objectLayer` being set to `nil`.
+ // There is no need to use an existing bucket or valid input for creating the request,
+ // since the `objectLayer==nil` check is performed before any other checks inside the handlers.
+ // The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
+ nilBucket := "dummy-bucket"
+ nilObject := "dummy-object"
+
+ nilReq, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", nilBucket, nilObject),
+ 0, nil, "", "", nil)
+ if err != nil {
+ t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
+ }
+ // execute the object layer set to `nil` test.
+ // `ExecObjectLayerAPINilTest` manages the operation.
+ ExecObjectLayerAPINilTest(t, nilBucket, nilObject, instanceType, apiRouter, nilReq)
+}
+
+// Tests sanity of attempting to copying each parts at offsets from an existing
+// file and create a new object. Also validates if the written is same as what we
+// expected.
+func TestAPICopyObjectPartHandlerSanity(t *testing.T) {
+ defer DetectTestLeak(t)()
+ ExecExtendedObjectLayerAPITest(t, testAPICopyObjectPartHandlerSanity, []string{"NewMultipart", "CompleteMultipart", "CopyObjectPart"})
+}
+
+func testAPICopyObjectPartHandlerSanity(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ credentials auth.Credentials, t *testing.T,
+) {
+ objectName := "test-object"
+ var err error
+ // set of byte data for PutObject.
+ // object has to be created before running tests for Copy Object.
+ // this is required even to assert the copied object,
+ bytesData := []struct {
+ byteData []byte
+ }{
+ {generateBytesData(6 * humanize.MiByte)},
+ }
+
+ // set of inputs for uploading the objects before tests for downloading is done.
+ putObjectInputs := []struct {
+ bucketName string
+ objectName string
+ contentLength int64
+ textData []byte
+ metaData map[string]string
+ }{
+ // case - 1.
+ {bucketName, objectName, int64(len(bytesData[0].byteData)), bytesData[0].byteData, make(map[string]string)},
+ }
+ // iterate through the above set of inputs and upload the object.
+ for i, input := range putObjectInputs {
+ // uploading the object.
+ _, err = obj.PutObject(context.Background(), input.bucketName, input.objectName,
+ mustGetPutObjReader(t, bytes.NewReader(input.textData), input.contentLength, input.metaData[""], ""), ObjectOptions{UserDefined: input.metaData})
+ // if object upload fails stop the test.
+ if err != nil {
+ t.Fatalf("Put Object case %d: Error uploading object: %v", i+1, err)
+ }
+ }
+
+ testObject := "testobject"
+
+ // Initiate Multipart upload for testing CopyObjectPartHandler.
+ rec := httptest.NewRecorder()
+ req, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, testObject),
+ 0, nil, credentials.AccessKey, credentials.SecretKey, nil)
+ if err != nil {
+ t.Fatalf("Failed to create HTTP request for NewMultipart Request: %v", err)
+ }
+ apiRouter.ServeHTTP(rec, req)
+ if rec.Code != http.StatusOK {
+ t.Fatalf("%s: Expected the response status to be `%d`, but instead found `%d`", instanceType, http.StatusOK, rec.Code)
+ }
+ decoder := xml.NewDecoder(rec.Body)
+ multipartResponse := &InitiateMultipartUploadResponse{}
+ err = decoder.Decode(multipartResponse)
+ if err != nil {
+ t.Fatalf("Error decoding the recorded response Body")
+ }
+ uploadID := multipartResponse.UploadID
+
+ a := 0
+ b := globalMinPartSize
+ var parts []CompletePart
+ for partNumber := 1; partNumber <= 2; partNumber++ {
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ rec := httptest.NewRecorder()
+ cpPartURL := getCopyObjectPartURL("", bucketName, testObject, uploadID, fmt.Sprintf("%d", partNumber))
+
+ // construct HTTP request for copy object.
+ var req *http.Request
+ req, err = newTestSignedRequestV4(http.MethodPut, cpPartURL, 0, nil, credentials.AccessKey, credentials.SecretKey, nil)
+ if err != nil {
+ t.Fatalf("Test failed to create HTTP request for copy object part: %v", err)
+ }
+
+ // "X-Amz-Copy-Source" header contains the information about the source bucket and the object to copied.
+ req.Header.Set("X-Amz-Copy-Source", url.QueryEscape(pathJoin(bucketName, objectName)))
+ req.Header.Set("X-Amz-Copy-Source-Range", fmt.Sprintf("bytes=%d-%d", a, b))
+
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler, `func (api objectAPIHandlers) CopyObjectHandler` handles the request.
+ a = globalMinPartSize + 1
+ b = len(bytesData[0].byteData) - 1
+ apiRouter.ServeHTTP(rec, req)
+ if rec.Code != http.StatusOK {
+ t.Fatalf("Test failed to create HTTP request for copy %d", rec.Code)
+ }
+
+ resp := &CopyObjectPartResponse{}
+ if err = xmlDecoder(rec.Body, resp, rec.Result().ContentLength); err != nil {
+ t.Fatalf("Test failed to decode XML response: %v", err)
+ }
+
+ parts = append(parts, CompletePart{
+ PartNumber: partNumber,
+ ETag: canonicalizeETag(resp.ETag),
+ })
+ }
+
+ var completeBytes []byte
+ // Complete multipart upload parts.
+ completeUploads := &CompleteMultipartUpload{
+ Parts: parts,
+ }
+ completeBytes, err = xml.Marshal(completeUploads)
+ if err != nil {
+ t.Fatalf("Error XML encoding of parts: %s.", err)
+ }
+ // Indicating that all parts are uploaded and initiating CompleteMultipartUpload.
+ req, err = newTestSignedRequestV4(http.MethodPost, getCompleteMultipartUploadURL("", bucketName, testObject, uploadID),
+ int64(len(completeBytes)), bytes.NewReader(completeBytes), credentials.AccessKey, credentials.SecretKey, nil)
+ if err != nil {
+ t.Fatalf("Failed to create HTTP request for CompleteMultipartUpload: %v", err)
+ }
+
+ rec = httptest.NewRecorder()
+
+ apiRouter.ServeHTTP(rec, req)
+ // Assert the response code with the expected status.
+ if rec.Code != http.StatusOK {
+ t.Errorf("Test %s: Expected the response status to be `%d`, but instead found `%d`", instanceType, http.StatusOK, rec.Code)
+ }
+
+ var buf bytes.Buffer
+ r, err := obj.GetObjectNInfo(context.Background(), bucketName, testObject, nil, nil, ObjectOptions{})
+ if err != nil {
+ t.Fatalf("Test %s: reading completed file failed: %v", instanceType, err)
+ }
+ if _, err = io.Copy(&buf, r); err != nil {
+ r.Close()
+ t.Fatalf("Test %s: Failed to fetch the copied object: %s", instanceType, err)
+ }
+ r.Close()
+ if !bytes.Equal(buf.Bytes(), bytesData[0].byteData) {
+ t.Fatalf("Test %s: returned data is not expected corruption detected:", instanceType)
+ }
+
+ globalCompressConfigMu.Lock()
+ compressEnabled := globalCompressConfig.Enabled
+ globalCompressConfigMu.Unlock()
+ if compressEnabled && !r.ObjInfo.IsCompressed() {
+ t.Errorf("Test %s: object found to be uncompressed though compression was enabled", instanceType)
+ }
+}
+
+// Wrapper for calling Copy Object Part API handler tests for both Erasure multiple disks and single node setup.
+func TestAPICopyObjectPartHandler(t *testing.T) {
+ defer DetectTestLeak(t)()
+ ExecExtendedObjectLayerAPITest(t, testAPICopyObjectPartHandler, []string{"CopyObjectPart"})
+}
+
+func testAPICopyObjectPartHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ credentials auth.Credentials, t *testing.T,
+) {
+ objectName := "test-object"
+ var err error
+ opts := ObjectOptions{}
+ // set of byte data for PutObject.
+ // object has to be created before running tests for Copy Object.
+ // this is required even to assert the copied object,
+ bytesData := []struct {
+ byteData []byte
+ }{
+ {generateBytesData(6 * humanize.KiByte)},
+ }
+
+ // set of inputs for uploading the objects before tests for downloading is done.
+ putObjectInputs := []struct {
+ bucketName string
+ objectName string
+ contentLength int64
+ textData []byte
+ metaData map[string]string
+ }{
+ // case - 1.
+ {bucketName, objectName, int64(len(bytesData[0].byteData)), bytesData[0].byteData, make(map[string]string)},
+ }
+ // iterate through the above set of inputs and upload the object.
+ for i, input := range putObjectInputs {
+ // uploading the object.
+ _, err = obj.PutObject(context.Background(), input.bucketName, input.objectName, mustGetPutObjReader(t, bytes.NewReader(input.textData), input.contentLength, input.metaData[""], ""), ObjectOptions{UserDefined: input.metaData})
+ // if object upload fails stop the test.
+ if err != nil {
+ t.Fatalf("Put Object case %d: Error uploading object: %v", i+1, err)
+ }
+ }
+
+ // Initiate Multipart upload for testing PutObjectPartHandler.
+ testObject := "testobject"
+
+ // PutObjectPart API HTTP Handler has to be tested in isolation,
+ // that is without any other handler being registered,
+ // That's why NewMultipartUpload is initiated using ObjectLayer.
+ res, err := obj.NewMultipartUpload(context.Background(), bucketName, testObject, opts)
+ if err != nil {
+ // Failed to create NewMultipartUpload, abort.
+ t.Fatalf("MinIO %s : %s", instanceType, err)
+ }
+ uploadID := res.UploadID
+
+ // test cases with inputs and expected result for Copy Object.
+ testCases := []struct {
+ bucketName string
+ copySourceHeader string // data for "X-Amz-Copy-Source" header. Contains the object to be copied in the URL.
+ copySourceRange string // data for "X-Amz-Copy-Source-Range" header, contains the byte range offsets of data to be copied.
+ uploadID string // uploadID of the transaction.
+ invalidPartNumber bool // Sets an invalid multipart.
+ maximumPartNumber bool // Sets a maximum parts.
+ accessKey string
+ secretKey string
+ // expected output.
+ expectedRespStatus int
+ }{
+ // Test case - 1, copy part 1 from newObject1, ignore request headers.
+ {
+ bucketName: bucketName,
+ uploadID: uploadID,
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusOK,
+ },
+
+ // Test case - 2.
+ // Test case with invalid source object.
+ {
+ bucketName: bucketName,
+ uploadID: uploadID,
+ copySourceHeader: url.QueryEscape(SlashSeparator),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedRespStatus: http.StatusBadRequest,
+ },
+
+ // Test case - 3.
+ // Test case with new object name is same as object to be copied.
+ // Fail with file not found.
+ {
+ bucketName: bucketName,
+ uploadID: uploadID,
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + testObject),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedRespStatus: http.StatusNotFound,
+ },
+
+ // Test case - 4.
+ // Test case with valid byte range.
+ {
+ bucketName: bucketName,
+ uploadID: uploadID,
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ copySourceRange: "bytes=500-4096",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedRespStatus: http.StatusOK,
+ },
+
+ // Test case - 5.
+ // Test case with invalid byte range.
+ {
+ bucketName: bucketName,
+ uploadID: uploadID,
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ copySourceRange: "bytes=6145-",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedRespStatus: http.StatusBadRequest,
+ },
+
+ // Test case - 6.
+ // Test case with invalid byte range for exceeding source size boundaries.
+ {
+ bucketName: bucketName,
+ uploadID: uploadID,
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ copySourceRange: "bytes=0-6144",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedRespStatus: http.StatusBadRequest,
+ },
+
+ // Test case - 7.
+ // Test case with object name missing from source.
+ // fail with BadRequest.
+ {
+ bucketName: bucketName,
+ uploadID: uploadID,
+ copySourceHeader: url.QueryEscape("//123"),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedRespStatus: http.StatusBadRequest,
+ },
+
+ // Test case - 8.
+ // Test case with non-existent source file.
+ // Case for the purpose of failing `api.ObjectAPI.GetObjectInfo`.
+ // Expecting the response status code to http.StatusNotFound (404).
+ {
+ bucketName: bucketName,
+ uploadID: uploadID,
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + "non-existent-object"),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedRespStatus: http.StatusNotFound,
+ },
+
+ // Test case - 9.
+ // Test case with non-existent source file.
+ // Case for the purpose of failing `api.ObjectAPI.PutObjectPart`.
+ // Expecting the response status code to http.StatusNotFound (404).
+ {
+ bucketName: "non-existent-destination-bucket",
+ uploadID: uploadID,
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedRespStatus: http.StatusNotFound,
+ },
+
+ // Test case - 10.
+ // Case with invalid AccessKey.
+ {
+ bucketName: bucketName,
+ uploadID: uploadID,
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ accessKey: "Invalid-AccessID",
+ secretKey: credentials.SecretKey,
+
+ expectedRespStatus: http.StatusForbidden,
+ },
+
+ // Test case - 11.
+ // Case with non-existent upload id.
+ {
+ bucketName: bucketName,
+ uploadID: "-1",
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedRespStatus: http.StatusNotFound,
+ },
+ // Test case - 12.
+ // invalid part number.
+ {
+ bucketName: bucketName,
+ uploadID: uploadID,
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ invalidPartNumber: true,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusOK,
+ },
+ // Test case - 13.
+ // maximum part number.
+ {
+ bucketName: bucketName,
+ uploadID: uploadID,
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ maximumPartNumber: true,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusOK,
+ },
+ // Test case - 14, copy part 1 from newObject1 with null versionId
+ {
+ bucketName: bucketName,
+ uploadID: uploadID,
+ copySourceHeader: url.QueryEscape(SlashSeparator+bucketName+SlashSeparator+objectName) + "?versionId=null",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusOK,
+ },
+ // Test case - 15, copy part 1 from newObject1 with non null versionId
+ {
+ bucketName: bucketName,
+ uploadID: uploadID,
+ copySourceHeader: url.QueryEscape(SlashSeparator+bucketName+SlashSeparator+objectName) + "?versionId=17",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusNotFound,
+ },
+ // Test case - 16, Test case with invalid byte range empty value.
+ {
+ bucketName: bucketName,
+ uploadID: uploadID,
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ copySourceRange: "empty",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedRespStatus: http.StatusBadRequest,
+ },
+ }
+
+ for i, testCase := range testCases {
+ var req *http.Request
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ rec := httptest.NewRecorder()
+ switch {
+ case !testCase.invalidPartNumber || !testCase.maximumPartNumber:
+ // construct HTTP request for copy object.
+ req, err = newTestSignedRequestV4(http.MethodPut, getCopyObjectPartURL("", testCase.bucketName, testObject, testCase.uploadID, "1"), 0, nil, testCase.accessKey, testCase.secretKey, nil)
+ case testCase.invalidPartNumber:
+ req, err = newTestSignedRequestV4(http.MethodPut, getCopyObjectPartURL("", testCase.bucketName, testObject, testCase.uploadID, "abc"), 0, nil, testCase.accessKey, testCase.secretKey, nil)
+ case testCase.maximumPartNumber:
+ req, err = newTestSignedRequestV4(http.MethodPut, getCopyObjectPartURL("", testCase.bucketName, testObject, testCase.uploadID, "99999"), 0, nil, testCase.accessKey, testCase.secretKey, nil)
+ }
+ if err != nil {
+ t.Fatalf("Test %d: Failed to create HTTP request for copy Object: %v", i+1, err)
+ }
+
+ // "X-Amz-Copy-Source" header contains the information about the source bucket and the object to copied.
+ if testCase.copySourceHeader != "" {
+ req.Header.Set("X-Amz-Copy-Source", testCase.copySourceHeader)
+ }
+ if testCase.copySourceRange != "" {
+ if testCase.copySourceRange == "empty" {
+ req.Header.Set("X-Amz-Copy-Source-Range", "") // specifically test for S3 errors in this scenario.
+ } else {
+ req.Header.Set("X-Amz-Copy-Source-Range", testCase.copySourceRange)
+ }
+ }
+
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler, `func (api objectAPIHandlers) CopyObjectHandler` handles the request.
+ apiRouter.ServeHTTP(rec, req)
+ // Assert the response code with the expected status.
+ if rec.Code != testCase.expectedRespStatus {
+ t.Fatalf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, rec.Code)
+ }
+ if rec.Code == http.StatusOK {
+ // See if the new part has been uploaded.
+ // testing whether the copy was successful.
+ var results ListPartsInfo
+ results, err = obj.ListObjectParts(context.Background(), testCase.bucketName, testObject, testCase.uploadID, 0, 1, ObjectOptions{})
+ if err != nil {
+ t.Fatalf("Test %d: %s: Failed to look for copied object part: %s", i+1, instanceType, err)
+ }
+ if len(results.Parts) != 1 {
+ t.Fatalf("Test %d: %s: Expected only one entry returned %d entries", i+1, instanceType, len(results.Parts))
+ }
+ }
+ }
+
+ // HTTP request for testing when `ObjectLayer` is set to `nil`.
+ // There is no need to use an existing bucket and valid input for creating the request
+ // since the `objectLayer==nil` check is performed before any other checks inside the handlers.
+ // The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
+ nilBucket := "dummy-bucket"
+ nilObject := "dummy-object"
+
+ nilReq, err := newTestSignedRequestV4(http.MethodPut, getCopyObjectPartURL("", nilBucket, nilObject, "0", "0"),
+ 0, bytes.NewReader([]byte("testNilObjLayer")), "", "", nil)
+ if err != nil {
+ t.Errorf("MinIO %s: Failed to create http request for testing the response when object Layer is set to `nil`.", instanceType)
+ }
+
+ // Below is how CopyObjectPartHandler is registered.
+ // bucket.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(api.CopyObjectPartHandler).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
+ // Its necessary to set the "X-Amz-Copy-Source" header for the request to be accepted by the handler.
+ nilReq.Header.Set("X-Amz-Copy-Source", url.QueryEscape(SlashSeparator+nilBucket+SlashSeparator+nilObject))
+
+ // execute the object layer set to `nil` test.
+ // `ExecObjectLayerAPINilTest` manages the operation.
+ ExecObjectLayerAPINilTest(t, nilBucket, nilObject, instanceType, apiRouter, nilReq)
+}
+
+// Wrapper for calling Copy Object API handler tests for both Erasure multiple disks and single node setup.
+func TestAPICopyObjectHandler(t *testing.T) {
+ defer DetectTestLeak(t)()
+ ExecExtendedObjectLayerAPITest(t, testAPICopyObjectHandler, []string{"CopyObject", "PutObject"})
+}
+
+func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
+ credentials auth.Credentials, t *testing.T,
+) {
+ objectName := "test?object" // use file with ? to test URL parsing...
+ if runtime.GOOS == "windows" {
+ objectName = "test-object" // ...except on Windows
+ }
+ // object used for anonymous HTTP request test.
+ anonObject := "anon-object"
+ var err error
+ opts := ObjectOptions{}
+ // set of byte data for PutObject.
+ // object has to be created before running tests for Copy Object.
+ // this is required even to assert the copied object,
+ bytesData := []struct {
+ byteData []byte
+ md5sum string
+ }{
+ {byteData: generateBytesData(6 * humanize.KiByte)},
+ }
+ h := md5.New()
+ h.Write(bytesData[0].byteData)
+ bytesData[0].md5sum = hex.EncodeToString(h.Sum(nil))
+
+ buffers := []*bytes.Buffer{
+ new(bytes.Buffer),
+ new(bytes.Buffer),
+ }
+ bucketInfo, err := obj.GetBucketInfo(context.Background(), bucketName, BucketOptions{})
+ if err != nil {
+ t.Fatalf("Test -1: %s: Failed to get bucket info: %s", instanceType, err)
+ }
+ // set of inputs for uploading the objects before tests for downloading is done.
+ putObjectInputs := []struct {
+ bucketName string
+ objectName string
+ contentLength int64
+ textData []byte
+ md5sum string
+ metaData map[string]string
+ }{
+ // case - 1.
+ {bucketName, objectName, int64(len(bytesData[0].byteData)), bytesData[0].byteData, bytesData[0].md5sum, make(map[string]string)},
+
+ // case - 2.
+ // used for anonymous HTTP request test.
+ {bucketName, anonObject, int64(len(bytesData[0].byteData)), bytesData[0].byteData, bytesData[0].md5sum, make(map[string]string)},
+ }
+ // iterate through the above set of inputs and upload the object.
+ for i, input := range putObjectInputs {
+ rec := httptest.NewRecorder()
+ req, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", input.bucketName, input.objectName),
+ input.contentLength, bytes.NewReader(input.textData), credentials.AccessKey, credentials.SecretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: Failed to create HTTP request for Put Object: %v", i, err)
+ }
+ apiRouter.ServeHTTP(rec, req)
+ if rec.Code != http.StatusOK {
+ b, err := io.ReadAll(rec.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var apiErr APIErrorResponse
+ err = xml.Unmarshal(b, &apiErr)
+ if err != nil {
+ t.Fatal(err)
+ }
+ gotErr := apiErr.Code
+ t.Errorf("test %d: want api got %q", i, gotErr)
+ }
+ }
+ // test cases with inputs and expected result for Copy Object.
+ testCases := []struct {
+ bucketName string
+ newObjectName string // name of the newly copied object.
+ copySourceHeader string // data for "X-Amz-Copy-Source" header. Contains the object to be copied in the URL.
+ copyModifiedHeader string // data for "X-Amz-Copy-Source-If-Modified-Since" header
+ copyUnmodifiedHeader string // data for "X-Amz-Copy-Source-If-Unmodified-Since" header
+ copySourceSame bool
+ metadataGarbage bool
+ metadataReplace bool
+ metadataCopy bool
+ metadata map[string]string
+ accessKey string
+ secretKey string
+ // expected output.
+ expectedRespStatus int
+ }{
+ 0: {
+ expectedRespStatus: http.StatusMethodNotAllowed,
+ },
+ // Test case - 1, copy metadata from newObject1, ignore request headers.
+ 1: {
+ bucketName: bucketName,
+ newObjectName: "newObject1",
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ metadata: map[string]string{
+ "Content-Type": "application/json",
+ },
+ expectedRespStatus: http.StatusOK,
+ },
+
+ // Test case - 2.
+ // Test case with invalid source object.
+ 2: {
+ bucketName: bucketName,
+ newObjectName: "newObject1",
+ copySourceHeader: url.QueryEscape(SlashSeparator),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedRespStatus: http.StatusBadRequest,
+ },
+ // Test case - 3.
+ // Test case with invalid source object.
+ 3: {
+ bucketName: bucketName,
+ newObjectName: "dir//newObject1",
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusBadRequest,
+ },
+ // Test case - 4.
+ // Test case with new object name is same as object to be copied.
+ 4: {
+ bucketName: bucketName,
+ newObjectName: objectName,
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ accessKey: credentials.AccessKey,
+ copySourceSame: true,
+ secretKey: credentials.SecretKey,
+
+ expectedRespStatus: http.StatusBadRequest,
+ },
+
+ // Test case - 5.
+ // Test case with new object name is same as object to be copied.
+ // But source copy is without leading slash
+ 5: {
+ bucketName: bucketName,
+ newObjectName: objectName,
+ copySourceSame: true,
+ copySourceHeader: url.QueryEscape(bucketName + SlashSeparator + objectName),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedRespStatus: http.StatusBadRequest,
+ },
+
+ // Test case - 6.
+ // Test case with new object name is same as object to be copied
+ // but metadata is updated.
+ 6: {
+ bucketName: bucketName,
+ newObjectName: objectName,
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ metadata: map[string]string{
+ "Content-Type": "application/json",
+ },
+ metadataReplace: true,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedRespStatus: http.StatusOK,
+ },
+
+ // Test case - 7.
+ // Test case with invalid metadata-directive.
+ 7: {
+ bucketName: bucketName,
+ newObjectName: "newObject1",
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ metadata: map[string]string{
+ "Content-Type": "application/json",
+ },
+ metadataGarbage: true,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedRespStatus: http.StatusBadRequest,
+ },
+
+ // Test case - 8.
+ // Test case with new object name is same as object to be copied
+ // fail with BadRequest.
+ 8: {
+ bucketName: bucketName,
+ newObjectName: objectName,
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ metadata: map[string]string{
+ "Content-Type": "application/json",
+ },
+ copySourceSame: true,
+ metadataCopy: true,
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedRespStatus: http.StatusBadRequest,
+ },
+
+ // Test case - 9.
+ // Test case with non-existent source file.
+ // Case for the purpose of failing `api.ObjectAPI.GetObjectInfo`.
+ // Expecting the response status code to http.StatusNotFound (404).
+ 9: {
+ bucketName: bucketName,
+ newObjectName: objectName,
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + "non-existent-object"),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedRespStatus: http.StatusNotFound,
+ },
+
+ // Test case - 10.
+ // Test case with non-existent source file.
+ // Case for the purpose of failing `api.ObjectAPI.PutObject`.
+ // Expecting the response status code to http.StatusNotFound (404).
+ 10: {
+ bucketName: "non-existent-destination-bucket",
+ newObjectName: objectName,
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+
+ expectedRespStatus: http.StatusNotFound,
+ },
+
+ // Test case - 11.
+ // Case with invalid AccessKey.
+ 11: {
+ bucketName: bucketName,
+ newObjectName: objectName,
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ accessKey: "Invalid-AccessID",
+ secretKey: credentials.SecretKey,
+
+ expectedRespStatus: http.StatusForbidden,
+ },
+ // Test case - 12, copy metadata from newObject1 with satisfying modified header.
+ 12: {
+ bucketName: bucketName,
+ newObjectName: "newObject1",
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ copyModifiedHeader: "Mon, 02 Jan 2006 15:04:05 GMT",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusOK,
+ },
+ // Test case - 13, copy metadata from newObject1 with unsatisfying modified header.
+ 13: {
+ bucketName: bucketName,
+ newObjectName: "newObject1",
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ copyModifiedHeader: "Mon, 02 Jan 2217 15:04:05 GMT",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusPreconditionFailed,
+ },
+ // Test case - 14, copy metadata from newObject1 with wrong modified header format
+ 14: {
+ bucketName: bucketName,
+ newObjectName: "newObject1",
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ copyModifiedHeader: "Mon, 02 Jan 2217 15:04:05 +00:00",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusOK,
+ },
+ // Test case - 15, copy metadata from newObject1 with satisfying unmodified header.
+ 15: {
+ bucketName: bucketName,
+ newObjectName: "newObject1",
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ copyUnmodifiedHeader: "Mon, 02 Jan 2217 15:04:05 GMT",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusOK,
+ },
+ // Test case - 16, copy metadata from newObject1 with unsatisfying unmodified header.
+ 16: {
+ bucketName: bucketName,
+ newObjectName: "newObject1",
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ copyUnmodifiedHeader: "Mon, 02 Jan 2007 15:04:05 GMT",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusPreconditionFailed,
+ },
+ // Test case - 17, copy metadata from newObject1 with incorrect unmodified header format.
+ 17: {
+ bucketName: bucketName,
+ newObjectName: "newObject1",
+ copySourceHeader: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + objectName),
+ copyUnmodifiedHeader: "Mon, 02 Jan 2007 15:04:05 +00:00",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusOK,
+ },
+ // Test case - 18, copy metadata from newObject1 with null versionId
+ 18: {
+ bucketName: bucketName,
+ newObjectName: "newObject1",
+ copySourceHeader: url.QueryEscape(SlashSeparator+bucketName+SlashSeparator+objectName) + "?versionId=null",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusOK,
+ },
+ // Test case - 19, copy metadata from newObject1 with non null versionId
+ 19: {
+ bucketName: bucketName,
+ newObjectName: "newObject1",
+ copySourceHeader: url.QueryEscape(SlashSeparator+bucketName+SlashSeparator+objectName) + "?versionId=17",
+ accessKey: credentials.AccessKey,
+ secretKey: credentials.SecretKey,
+ expectedRespStatus: http.StatusNotFound,
+ },
+ }
+
+ for i, testCase := range testCases {
+ if bucketInfo.Versioning {
+ if strings.Contains(testCase.copySourceHeader, "versionId=null") {
+ testCase.expectedRespStatus = http.StatusNotFound
+ }
+ }
+ values := url.Values{}
+ if testCase.expectedRespStatus == http.StatusOK {
+ r, err := obj.GetObjectNInfo(context.Background(), testCase.bucketName, objectName, nil, nil, opts)
+ if err != nil {
+ t.Fatalf("Test %d: %s reading completed file failed: %v", i, instanceType, err)
+ }
+ r.Close()
+ if r.ObjInfo.VersionID != "" {
+ values.Set(xhttp.VersionID, r.ObjInfo.VersionID)
+ }
+ }
+ var req *http.Request
+ var reqV2 *http.Request
+ // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
+ rec := httptest.NewRecorder()
+ // construct HTTP request for copy object.
+ req, err = newTestSignedRequestV4(http.MethodPut, getCopyObjectURL("", testCase.bucketName, testCase.newObjectName),
+ 0, nil, testCase.accessKey, testCase.secretKey, nil)
+ if err != nil {
+ t.Fatalf("Test %d: Failed to create HTTP request for copy Object: %v", i, err)
+ }
+ // "X-Amz-Copy-Source" header contains the information about the source bucket and the object to copied.
+ if testCase.copySourceHeader != "" {
+ if values.Encode() != "" && !strings.Contains(testCase.copySourceHeader, "?") {
+ req.Header.Set("X-Amz-Copy-Source", testCase.copySourceHeader+"?"+values.Encode())
+ } else {
+ req.Header.Set("X-Amz-Copy-Source", testCase.copySourceHeader)
+ }
+ }
+ if testCase.copyModifiedHeader != "" {
+ req.Header.Set("X-Amz-Copy-Source-If-Modified-Since", testCase.copyModifiedHeader)
+ }
+ if testCase.copyUnmodifiedHeader != "" {
+ req.Header.Set("X-Amz-Copy-Source-If-Unmodified-Since", testCase.copyUnmodifiedHeader)
+ }
+ // Add custom metadata.
+ for k, v := range testCase.metadata {
+ req.Header.Set(k, v)
+ }
+ if testCase.metadataReplace {
+ req.Header.Set("X-Amz-Metadata-Directive", "REPLACE")
+ }
+ if testCase.metadataCopy {
+ req.Header.Set("X-Amz-Metadata-Directive", "COPY")
+ }
+ if testCase.metadataGarbage {
+ req.Header.Set("X-Amz-Metadata-Directive", "Unknown")
+ }
+ // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
+ // Call the ServeHTTP to execute the handler, `func (api objectAPIHandlers) CopyObjectHandler` handles the request.
+ apiRouter.ServeHTTP(rec, req)
+ // Assert the response code with the expected status.
+ if rec.Code != testCase.expectedRespStatus {
+ if testCase.copySourceSame {
+ // encryption will rotate creds, so fail only for non-encryption scenario.
+ if GlobalKMS == nil {
+ t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i, instanceType, testCase.expectedRespStatus, rec.Code)
+ continue
+ }
+ } else {
+ t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i, instanceType, testCase.expectedRespStatus, rec.Code)
+ continue
+ }
+ }
+ if rec.Code == http.StatusOK {
+ var cpObjResp CopyObjectResponse
+ if err = xml.Unmarshal(rec.Body.Bytes(), &cpObjResp); err != nil {
+ t.Fatalf("Test %d: %s: Failed to parse the CopyObjectResult response: %s", i, instanceType, err)
+ }
+
+ // See if the new object is formed.
+ // testing whether the copy was successful.
+ // Note that this goes directly to the file system,
+ // so encryption/compression may interfere at some point.
+ buffers[0].Reset()
+ r, err := obj.GetObjectNInfo(context.Background(), testCase.bucketName, testCase.newObjectName, nil, nil, opts)
+ if err != nil {
+ t.Fatalf("Test %d: %s reading completed file failed: %v", i, instanceType, err)
+ }
+ if _, err = io.Copy(buffers[0], r); err != nil {
+ r.Close()
+ t.Fatalf("Test %d %s: Failed to fetch the copied object: